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": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||
"ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117,
|
||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
||||
"DispelType": 4
|
||||
|
|
@ -95,5 +95,14 @@
|
|||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"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": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||
"ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 124,
|
||||
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215,
|
||||
"CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40,
|
||||
"DispelType": 3
|
||||
|
|
@ -111,5 +111,14 @@
|
|||
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
||||
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
||||
"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": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||
"ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117,
|
||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
||||
"DispelType": 4
|
||||
|
|
@ -108,5 +108,14 @@
|
|||
"Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33,
|
||||
"Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37,
|
||||
"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": {
|
||||
"ID": 0, "Attributes": 4, "IconID": 133,
|
||||
"ID": 0, "Attributes": 4, "AttributesEx": 5, "IconID": 133,
|
||||
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225,
|
||||
"PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49,
|
||||
"DispelType": 2
|
||||
|
|
@ -116,5 +116,14 @@
|
|||
},
|
||||
"LFGDungeons": {
|
||||
"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_;
|
||||
}
|
||||
|
||||
// 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)
|
||||
uint32_t getLatencyMs() const { return lastLatency; }
|
||||
|
||||
|
|
@ -709,6 +718,8 @@ public:
|
|||
void dismissPet();
|
||||
void renamePet(const std::string& newName);
|
||||
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_; }
|
||||
|
||||
// ---- Pet state (populated by SMSG_PET_SPELLS / SMSG_PET_MODE) ----
|
||||
|
|
@ -798,6 +809,7 @@ public:
|
|||
uint32_t spellId = 0;
|
||||
float timeRemaining = 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)
|
||||
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
||||
|
|
@ -819,6 +831,10 @@ public:
|
|||
auto* s = getUnitCastState(targetGuid);
|
||||
return s ? s->timeRemaining : 0.0f;
|
||||
}
|
||||
bool isTargetCastInterruptible() const {
|
||||
auto* s = getUnitCastState(targetGuid);
|
||||
return s ? s->interruptible : true;
|
||||
}
|
||||
|
||||
// Talents
|
||||
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
||||
|
|
@ -1160,6 +1176,11 @@ public:
|
|||
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
|
||||
void confirmTalentWipe();
|
||||
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). */
|
||||
bool canReclaimCorpse() const;
|
||||
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
|
||||
|
|
@ -1389,6 +1410,10 @@ public:
|
|||
const LootResponseData& getCurrentLoot() const { return currentLoot; }
|
||||
void setAutoLoot(bool enabled) { autoLoot_ = enabled; }
|
||||
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)
|
||||
const std::vector<uint64_t>& getMasterLootCandidates() const { return masterLootCandidates_; }
|
||||
|
|
@ -1435,6 +1460,9 @@ public:
|
|||
void acceptQuest();
|
||||
void declineQuest();
|
||||
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; }
|
||||
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
||||
bool isQuestDetailsOpen() {
|
||||
|
|
@ -1919,6 +1947,11 @@ public:
|
|||
float x = 0, y = 0, z = 0;
|
||||
};
|
||||
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;
|
||||
bool taxiNpcHasRoutes(uint64_t guid) const {
|
||||
auto it = taxiNpcHasRoutes_.find(guid);
|
||||
|
|
@ -2051,6 +2084,13 @@ public:
|
|||
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)
|
||||
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 {
|
||||
std::string name;
|
||||
|
|
@ -2717,6 +2757,7 @@ private:
|
|||
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 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::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||||
|
||||
|
|
@ -2759,6 +2800,9 @@ private:
|
|||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||||
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)
|
||||
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
||||
|
||||
|
|
@ -2862,6 +2906,8 @@ private:
|
|||
// ---- Phase 5: Loot ----
|
||||
bool lootWindowOpen = false;
|
||||
bool autoLoot_ = false;
|
||||
bool autoSellGrey_ = false;
|
||||
bool autoRepair_ = false;
|
||||
LootResponseData currentLoot;
|
||||
std::vector<uint64_t> masterLootCandidates_; // from SMSG_LOOT_MASTER_LIST
|
||||
|
||||
|
|
@ -3054,7 +3100,7 @@ private:
|
|||
// Trainer
|
||||
bool trainerWindowOpen_ = false;
|
||||
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_;
|
||||
bool spellNameCacheLoaded_ = false;
|
||||
|
||||
|
|
@ -3267,6 +3313,10 @@ private:
|
|||
bool talentWipePending_ = false;
|
||||
uint64_t talentWipeNpcGuid_ = 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
|
||||
uint64_t resurrectCasterGuid_ = 0;
|
||||
std::string resurrectCasterName_;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ struct CombatTextEntry {
|
|||
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
||||
EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
||||
ENERGIZE, POWER_DRAIN, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER,
|
||||
DISPEL, STEAL, INTERRUPT, INSTAKILL
|
||||
DISPEL, STEAL, INTERRUPT, INSTAKILL, HONOR_GAIN, GLANCING, CRUSHING
|
||||
};
|
||||
Type type;
|
||||
int32_t amount = 0;
|
||||
|
|
|
|||
|
|
@ -147,9 +147,18 @@ private:
|
|||
uint32_t heapSize_; // Heap size
|
||||
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_;
|
||||
|
||||
// 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
|
||||
std::map<uint32_t, size_t> allocations_;
|
||||
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
|
||||
WardenFuncList funcList_; // Callback functions
|
||||
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
|
||||
bool verifyMD5(const std::vector<uint8_t>& data,
|
||||
|
|
|
|||
|
|
@ -1719,8 +1719,10 @@ struct AttackerStateUpdateData {
|
|||
uint32_t blocked = 0;
|
||||
|
||||
bool isValid() const { return attackerGuid != 0; }
|
||||
bool isCrit() const { return (hitInfo & 0x200) != 0; }
|
||||
bool isMiss() const { return (hitInfo & 0x10) != 0; }
|
||||
bool isCrit() const { return (hitInfo & 0x0200) != 0; }
|
||||
bool isMiss() const { return (hitInfo & 0x0010) != 0; }
|
||||
bool isGlancing() const { return (hitInfo & 0x0800) != 0; }
|
||||
bool isCrushing() const { return (hitInfo & 0x1000) != 0; }
|
||||
};
|
||||
|
||||
class AttackerStateUpdateParser {
|
||||
|
|
@ -1873,6 +1875,7 @@ struct SpellGoData {
|
|||
std::vector<uint64_t> hitTargets;
|
||||
uint8_t missCount = 0;
|
||||
std::vector<SpellGoMissEntry> missTargets;
|
||||
uint64_t targetGuid = 0; ///< Primary target GUID from SpellCastTargets (0 = none/AoE)
|
||||
|
||||
bool isValid() const { return spellId != 0; }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public:
|
|||
}
|
||||
|
||||
void reset();
|
||||
void resetAngles();
|
||||
void teleportTo(const glm::vec3& pos);
|
||||
void setOnlineMode(bool online) { onlineMode = online; }
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <vector>
|
||||
#include <future>
|
||||
#include <cstddef>
|
||||
#include <unordered_map>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
|
@ -152,6 +153,11 @@ public:
|
|||
void playEmote(const std::string& emoteName);
|
||||
void triggerLevelUpEffect(const glm::vec3& position);
|
||||
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; }
|
||||
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
|
||||
static uint32_t getEmoteDbcId(const std::string& emoteName);
|
||||
|
|
@ -323,6 +329,19 @@ private:
|
|||
glm::mat4 computeLightSpaceMatrix();
|
||||
|
||||
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;
|
||||
std::string currentZoneName;
|
||||
bool inTavern_ = false;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@ struct WorldMapPartyDot {
|
|||
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 {
|
||||
uint32_t wmaID = 0;
|
||||
uint32_t areaID = 0; // 0 = continent level
|
||||
|
|
@ -57,6 +66,14 @@ public:
|
|||
void setMapName(const std::string& name);
|
||||
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||
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; }
|
||||
void close() { open = false; }
|
||||
|
||||
|
|
@ -127,6 +144,14 @@ private:
|
|||
// Party member dots (set each frame from the UI layer)
|
||||
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
|
||||
std::vector<uint32_t> serverExplorationMask;
|
||||
bool hasServerExplorationMask = false;
|
||||
|
|
|
|||
|
|
@ -195,6 +195,8 @@ private:
|
|||
bool pendingSeparateBags = true;
|
||||
bool pendingShowKeyring = true;
|
||||
bool pendingAutoLoot = false;
|
||||
bool pendingAutoSellGrey = false;
|
||||
bool pendingAutoRepair = false;
|
||||
|
||||
// Keybinding customization
|
||||
int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index
|
||||
|
|
@ -317,6 +319,7 @@ private:
|
|||
|
||||
// ---- New UI renders ----
|
||||
void renderActionBar(game::GameHandler& gameHandler);
|
||||
void renderStanceBar(game::GameHandler& gameHandler);
|
||||
void renderBagBar(game::GameHandler& gameHandler);
|
||||
void renderXpBar(game::GameHandler& gameHandler);
|
||||
void renderRepBar(game::GameHandler& gameHandler);
|
||||
|
|
@ -355,6 +358,7 @@ private:
|
|||
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
||||
void renderResurrectDialog(game::GameHandler& gameHandler);
|
||||
void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler);
|
||||
void renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler);
|
||||
void renderEscapeMenu();
|
||||
void renderSettingsWindow();
|
||||
void applyGraphicsPreset(GraphicsPreset preset);
|
||||
|
|
@ -375,7 +379,6 @@ private:
|
|||
void renderGuildBankWindow(game::GameHandler& gameHandler);
|
||||
void renderAuctionHouseWindow(game::GameHandler& gameHandler);
|
||||
void renderDungeonFinderWindow(game::GameHandler& gameHandler);
|
||||
void renderObjectiveTracker(game::GameHandler& gameHandler);
|
||||
void renderInstanceLockouts(game::GameHandler& gameHandler);
|
||||
void renderNameplates(game::GameHandler& gameHandler);
|
||||
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
||||
|
|
@ -435,6 +438,10 @@ private:
|
|||
char achievementSearchBuf_[128] = {};
|
||||
void renderAchievementWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Skills / Professions window (K key)
|
||||
bool showSkillsWindow_ = false;
|
||||
void renderSkillsWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Titles window
|
||||
bool showTitlesWindow_ = false;
|
||||
void renderTitlesWindow(game::GameHandler& gameHandler);
|
||||
|
|
@ -633,7 +640,9 @@ private:
|
|||
float zoneTextTimer_ = 0.0f;
|
||||
std::string zoneTextName_;
|
||||
std::string lastKnownZoneName_;
|
||||
void renderZoneText();
|
||||
uint32_t lastKnownWorldStateZoneId_ = 0;
|
||||
void renderZoneText(game::GameHandler& gameHandler);
|
||||
void renderWeatherOverlay(game::GameHandler& gameHandler);
|
||||
|
||||
// Cooldown tracker
|
||||
bool showCooldownTracker_ = false;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public:
|
|||
TOGGLE_NAMEPLATES,
|
||||
TOGGLE_RAID_FRAMES,
|
||||
TOGGLE_ACHIEVEMENTS,
|
||||
TOGGLE_SKILLS,
|
||||
ACTION_COUNT
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1849,12 +1849,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
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.");
|
||||
break;
|
||||
case Opcode::SMSG_CHAT_NOT_IN_PARTY:
|
||||
addUIError("You are not in a party.");
|
||||
addSystemChatMessage("You are not in a party.");
|
||||
break;
|
||||
case Opcode::SMSG_CHAT_RESTRICTED:
|
||||
addUIError("You cannot send chat messages in this area.");
|
||||
addSystemChatMessage("You cannot send chat messages in this area.");
|
||||
break;
|
||||
|
||||
|
|
@ -2049,6 +2052,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint8_t reason = packet.readUInt8();
|
||||
const char* msg = (reason < 8) ? reasons[reason] : "Unknown reason";
|
||||
std::string s = std::string("Failed to tame: ") + msg;
|
||||
addUIError(s);
|
||||
addSystemChatMessage(s);
|
||||
}
|
||||
break;
|
||||
|
|
@ -2168,6 +2172,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
std::dec, " rank=", rank);
|
||||
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
|
||||
addSystemChatMessage(msg);
|
||||
if (honor > 0)
|
||||
addCombatText(CombatTextEntry::HONOR_GAIN, static_cast<int32_t>(honor), 0, true);
|
||||
if (pvpHonorCallback_) {
|
||||
pvpHonorCallback_(honor, victimGuid, rank);
|
||||
}
|
||||
|
|
@ -2369,7 +2375,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case 0x06: msg = "Pet retrieved from stable."; break;
|
||||
case 0x07: msg = "Stable slot purchased."; 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;
|
||||
}
|
||||
if (msg) addSystemChatMessage(msg);
|
||||
|
|
@ -2525,8 +2532,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
"Character name does not meet requirements.", // 7
|
||||
};
|
||||
const char* errMsg = (result < 8) ? kRenameErrors[result] : nullptr;
|
||||
addSystemChatMessage(errMsg ? std::string("Rename failed: ") + errMsg
|
||||
: "Character rename failed.");
|
||||
std::string renameErr = errMsg ? std::string("Rename failed: ") + errMsg
|
||||
: "Character rename failed.";
|
||||
addUIError(renameErr);
|
||||
addSystemChatMessage(renameErr);
|
||||
}
|
||||
LOG_INFO("SMSG_CHAR_RENAME: result=", result, " newName=", newName);
|
||||
}
|
||||
|
|
@ -2539,6 +2548,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (result == 0) {
|
||||
addSystemChatMessage("Your home is now set to this location.");
|
||||
} else {
|
||||
addUIError("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"
|
||||
};
|
||||
const char* msg = (result < 8) ? reasons[result] : "Difficulty change failed.";
|
||||
addUIError(std::string("Cannot change difficulty: ") + msg);
|
||||
addSystemChatMessage(std::string("Cannot change difficulty: ") + msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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.");
|
||||
break;
|
||||
case Opcode::SMSG_CROSSED_INEBRIATION_THRESHOLD: {
|
||||
|
|
@ -2815,7 +2827,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint32_t result = packet.readUInt32();
|
||||
if (result != 4) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -2823,7 +2837,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
// uint32 result: 0=ok, others=error
|
||||
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||
uint32_t result = packet.readUInt32();
|
||||
if (result != 0) addSystemChatMessage("Cannot dismount here.");
|
||||
if (result != 0) { addUIError("Cannot dismount here."); addSystemChatMessage("Cannot dismount here."); }
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -3251,10 +3265,24 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleSpellDamageLog(packet);
|
||||
break;
|
||||
case Opcode::SMSG_PLAY_SPELL_VISUAL: {
|
||||
// Minimal parse: uint64 casterGuid, uint32 visualId
|
||||
// uint64 casterGuid + uint32 visualId
|
||||
if (packet.getSize() - packet.getReadPos() < 12) break;
|
||||
packet.readUInt64();
|
||||
packet.readUInt32();
|
||||
uint64_t casterGuid = packet.readUInt64();
|
||||
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;
|
||||
}
|
||||
case Opcode::SMSG_SPELLHEALLOG:
|
||||
|
|
@ -3455,6 +3483,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
std::string msg = areaName.empty()
|
||||
? std::string("A zone is under attack!")
|
||||
: (areaName + " is under attack!");
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
}
|
||||
break;
|
||||
|
|
@ -3549,6 +3578,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
char buf[80];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"You have lost %u%% of your gear's durability due to death.", pct);
|
||||
addUIError(buf);
|
||||
addSystemChatMessage(buf);
|
||||
}
|
||||
break;
|
||||
|
|
@ -3586,6 +3616,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
partyData.members.clear();
|
||||
partyData.memberCount = 0;
|
||||
partyData.leaderGuid = 0;
|
||||
addUIError("Your party has been disbanded.");
|
||||
addSystemChatMessage("Your party has been disbanded.");
|
||||
LOG_INFO("SMSG_GROUP_DESTROYED: party cleared");
|
||||
break;
|
||||
|
|
@ -3955,6 +3986,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
else if (errorCode == 2) msg += " (already known)";
|
||||
else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")";
|
||||
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
// Play error sound so the player notices the failure
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
|
|
@ -4473,6 +4505,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
/*uint32_t len =*/ packet.readUInt32();
|
||||
std::string msg = packet.readString();
|
||||
if (!msg.empty()) {
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
areaTriggerMsgs_.push_back(msg);
|
||||
}
|
||||
|
|
@ -4578,6 +4611,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
"Unknown error", "Only empty bag"
|
||||
};
|
||||
const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error";
|
||||
addUIError(std::string("Sell failed: ") + msg);
|
||||
addSystemChatMessage(std::string("Sell failed: ") + msg);
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* sfx = renderer->getUiSoundManager())
|
||||
|
|
@ -4611,8 +4645,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (requiredLevel > 0) {
|
||||
std::snprintf(levelBuf, sizeof(levelBuf),
|
||||
"You must reach level %u to use that item.", requiredLevel);
|
||||
addUIError(levelBuf);
|
||||
addSystemChatMessage(levelBuf);
|
||||
} else {
|
||||
addUIError("You must reach a higher level to use that item.");
|
||||
addSystemChatMessage("You must reach a higher level to use that item.");
|
||||
}
|
||||
break;
|
||||
|
|
@ -4675,6 +4711,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
default: break;
|
||||
}
|
||||
std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ").";
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
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;
|
||||
default: break;
|
||||
}
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
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 == 3) ? "You must stand up to use the barber shop."
|
||||
: "Barber shop unavailable.";
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
}
|
||||
LOG_DEBUG("SMSG_BARBER_SHOP_RESULT: result=", result);
|
||||
|
|
@ -4911,6 +4950,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (result == 0) {
|
||||
addSystemChatMessage("Gems socketed successfully.");
|
||||
} else {
|
||||
addUIError("Failed to socket gems.");
|
||||
addSystemChatMessage("Failed to socket gems.");
|
||||
}
|
||||
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."
|
||||
: (reason == 2) ? "Cannot resurrect in this area."
|
||||
: "Resurrection failed.";
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
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* info = getCachedGameObjectInfo(go->getEntry());
|
||||
if (info && info->type == 17) { // GO_TYPE_FISHINGNODE
|
||||
addUIError("A fish is on your line!");
|
||||
addSystemChatMessage("A fish is on your line!");
|
||||
// Play a distinctive UI sound to alert the player
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
|
|
@ -5438,6 +5480,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
case Opcode::SMSG_QUESTLOG_FULL:
|
||||
// Zero-payload notification: the player's quest log is full (25 quests).
|
||||
addUIError("Your quest log is full.");
|
||||
addSystemChatMessage("Your quest log is full.");
|
||||
LOG_INFO("SMSG_QUESTLOG_FULL: quest log is at capacity");
|
||||
break;
|
||||
|
|
@ -5503,6 +5546,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case 0x0C: abortMsg = "Transfer aborted."; break;
|
||||
default: abortMsg = "Transfer aborted."; break;
|
||||
}
|
||||
addUIError(abortMsg);
|
||||
addSystemChatMessage(abortMsg);
|
||||
break;
|
||||
}
|
||||
|
|
@ -5540,12 +5584,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleBattlefieldList(packet);
|
||||
break;
|
||||
case Opcode::SMSG_BATTLEFIELD_PORT_DENIED:
|
||||
addUIError("Battlefield port denied.");
|
||||
addSystemChatMessage("Battlefield port denied.");
|
||||
break;
|
||||
case Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS:
|
||||
// Optional map position updates for BG objectives/players.
|
||||
packet.setReadPos(packet.getSize());
|
||||
case Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS: {
|
||||
bgPlayerPositions_.clear();
|
||||
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;
|
||||
}
|
||||
case Opcode::SMSG_REMOVED_FROM_PVP_QUEUE:
|
||||
addSystemChatMessage("You have been removed from the PvP queue.");
|
||||
break;
|
||||
|
|
@ -5635,6 +5692,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
const char* reasonMsg = (reason < 5) ? resetFailReasons[reason] : "Unknown reason.";
|
||||
std::string mapLabel = getMapName(mapId);
|
||||
if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId);
|
||||
addUIError("Cannot reset " + mapLabel + ": " + reasonMsg);
|
||||
addSystemChatMessage("Cannot reset " + mapLabel + ": " + reasonMsg);
|
||||
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.
|
||||
learnedTalents_[0].clear();
|
||||
learnedTalents_[1].clear();
|
||||
addUIError("Your talents have been reset by the server.");
|
||||
addSystemChatMessage("Your talents have been reset by the server.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -6183,7 +6242,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_EQUIPMENT_SET_USE_RESULT: {
|
||||
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -6218,12 +6277,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint32_t result = packet.readUInt32();
|
||||
(void)result;
|
||||
}
|
||||
addUIError("Dungeon Finder: Auto-join failed.");
|
||||
addSystemChatMessage("Dungeon Finder: Auto-join failed.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER:
|
||||
// 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.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -6737,6 +6798,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim);
|
||||
} else if (ikVictim == playerGuid) {
|
||||
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.");
|
||||
}
|
||||
LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster,
|
||||
|
|
@ -7070,6 +7132,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
s.spellId = chanSpellId;
|
||||
s.timeTotal = chanTotalMs / 1000.0f;
|
||||
s.timeRemaining = s.timeTotal;
|
||||
s.interruptible = isSpellInterruptible(chanSpellId);
|
||||
}
|
||||
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
|
||||
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
||||
|
|
@ -7256,16 +7319,20 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_PLAYERBINDERROR: {
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t error = packet.readUInt32();
|
||||
if (error == 0)
|
||||
if (error == 0) {
|
||||
addUIError("Your hearthstone is not bound.");
|
||||
addSystemChatMessage("Your hearthstone is not bound.");
|
||||
else
|
||||
} else {
|
||||
addUIError("Hearthstone bind failed.");
|
||||
addSystemChatMessage("Hearthstone bind failed.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Instance/raid errors ----
|
||||
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.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -7273,13 +7340,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_RAID_READY_CHECK_ERROR: {
|
||||
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||
uint8_t err = packet.readUInt8();
|
||||
if (err == 0) addSystemChatMessage("Ready check failed: not in a group.");
|
||||
else if (err == 1) addSystemChatMessage("Ready check failed: in instance.");
|
||||
else addSystemChatMessage("Ready check failed.");
|
||||
if (err == 0) { addUIError("Ready check failed: not in a group."); addSystemChatMessage("Ready check failed: not in a group."); }
|
||||
else if (err == 1) { addUIError("Ready check failed: in instance."); addSystemChatMessage("Ready check failed: in instance."); }
|
||||
else { addUIError("Ready check failed."); addSystemChatMessage("Ready check failed."); }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_RESET_FAILED_NOTIFY: {
|
||||
addUIError("Cannot reset instance: another player is still inside.");
|
||||
addSystemChatMessage("Cannot reset instance: another player is still inside.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -7344,12 +7412,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// ---- Play object/spell sounds ----
|
||||
case Opcode::SMSG_PLAY_OBJECT_SOUND:
|
||||
case Opcode::SMSG_PLAY_SPELL_IMPACT:
|
||||
if (packet.getSize() - packet.getReadPos() >= 12) {
|
||||
// uint32 soundId + uint64 sourceGuid
|
||||
uint32_t soundId = packet.readUInt32();
|
||||
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);
|
||||
else if (playSoundCallback_) playSoundCallback_(soundId);
|
||||
} else if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
|
|
@ -7358,6 +7425,28 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
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 ----
|
||||
case Opcode::SMSG_RESISTLOG: {
|
||||
|
|
@ -7407,6 +7496,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
case Opcode::SMSG_READ_ITEM_FAILED:
|
||||
addUIError("You cannot read this item.");
|
||||
addSystemChatMessage("You cannot read this item.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -7474,6 +7564,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// ---- NPC not responding ----
|
||||
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.");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -7569,12 +7660,26 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_PET_GUIDS:
|
||||
case Opcode::SMSG_PET_DISMISS_SOUND:
|
||||
case Opcode::SMSG_PET_ACTION_SOUND:
|
||||
case Opcode::SMSG_PET_UNLEARN_CONFIRM:
|
||||
case Opcode::SMSG_PET_RENAMEABLE:
|
||||
case Opcode::SMSG_PET_UNLEARN_CONFIRM: {
|
||||
// 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:
|
||||
packet.setReadPos(packet.getSize());
|
||||
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:
|
||||
addUIError("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());
|
||||
break;
|
||||
|
|
@ -16179,6 +16284,7 @@ void GameHandler::handleLfgJoinResult(network::Packet& packet) {
|
|||
} else {
|
||||
const char* msg = lfgJoinResultString(result);
|
||||
std::string errMsg = std::string("Dungeon Finder: ") + (msg ? msg : "Join failed.");
|
||||
addUIError(errMsg);
|
||||
addSystemChatMessage(errMsg);
|
||||
LOG_INFO("SMSG_LFG_JOIN_RESULT: result=", static_cast<int>(result),
|
||||
" state=", static_cast<int>(state));
|
||||
|
|
@ -16224,6 +16330,7 @@ void GameHandler::handleLfgProposalUpdate(network::Packet& packet) {
|
|||
case 0:
|
||||
lfgState_ = LfgState::Queued;
|
||||
lfgProposalId_ = 0;
|
||||
addUIError("Dungeon Finder: Group proposal failed.");
|
||||
addSystemChatMessage("Dungeon Finder: Group proposal failed.");
|
||||
break;
|
||||
case 1: {
|
||||
|
|
@ -16267,6 +16374,7 @@ void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) {
|
|||
LOG_INFO("LFG role check finished");
|
||||
} else if (roleCheckState == 3) {
|
||||
lfgState_ = LfgState::None;
|
||||
addUIError("Dungeon Finder: Role check failed — missing required role.");
|
||||
addSystemChatMessage("Dungeon Finder: Role check failed — missing required role.");
|
||||
} else if (roleCheckState == 2) {
|
||||
lfgState_ = LfgState::RoleCheck;
|
||||
|
|
@ -17555,6 +17663,12 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) {
|
|||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
||||
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)
|
||||
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).
|
||||
addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
||||
} 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);
|
||||
// Show partial absorb/resist from sub-damage entries
|
||||
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;
|
||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
if (auto playerUnit = std::dynamic_pointer_cast<Unit>(playerEntity)) {
|
||||
powerType = playerUnit->getPowerType();
|
||||
}
|
||||
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;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
if (reason) {
|
||||
msg.message = reason;
|
||||
} else {
|
||||
msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")";
|
||||
}
|
||||
msg.message = errMsg;
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
|
|
@ -18194,6 +18315,7 @@ void GameHandler::handleSpellStart(network::Packet& packet) {
|
|||
s.spellId = data.spellId;
|
||||
s.timeTotal = data.castTime / 1000.0f;
|
||||
s.timeRemaining = s.timeTotal;
|
||||
s.interruptible = isSpellInterruptible(data.spellId);
|
||||
// Trigger cast animation on the casting unit
|
||||
if (spellCastAnimCallback_) {
|
||||
spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
|
|
@ -18760,6 +18882,20 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) {
|
|||
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() {
|
||||
if (!talentWipePending_) return;
|
||||
talentWipePending_ = false;
|
||||
|
|
@ -18873,6 +19009,7 @@ void GameHandler::handleGroupUninvite(network::Packet& packet) {
|
|||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You have been removed from the group.";
|
||||
addUIError("You have been removed from the group.");
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
|
|
@ -18907,6 +19044,7 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) {
|
|||
static_cast<uint32_t>(data.result));
|
||||
}
|
||||
|
||||
addUIError(buf);
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
|
|
@ -20420,6 +20558,34 @@ void GameHandler::closeGossip() {
|
|||
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) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
buybackItems_.clear();
|
||||
|
|
@ -21104,6 +21270,86 @@ void GameHandler::handleListInventory(network::Packet& packet) {
|
|||
vendorWindowOpen = true;
|
||||
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
|
||||
if (npcVendorCallback_ && currentVendorItems.vendorGuid != 0) {
|
||||
auto entity = entityManager.getEntity(currentVendorItems.vendorGuid);
|
||||
|
|
@ -21229,6 +21475,14 @@ void GameHandler::loadSpellNameCache() {
|
|||
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
|
||||
uint32_t tooltipField = 0xFFFFFFFF;
|
||||
if (spellL) {
|
||||
|
|
@ -21243,7 +21497,7 @@ void GameHandler::loadSpellNameCache() {
|
|||
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
||||
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
||||
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) {
|
||||
entry.description = dbc->getString(i, tooltipField);
|
||||
}
|
||||
|
|
@ -21258,6 +21512,9 @@ void GameHandler::loadSpellNameCache() {
|
|||
if (hasDispelField) {
|
||||
entry.dispelType = static_cast<uint8_t>(dbc->getUInt32(i, dispelField));
|
||||
}
|
||||
if (hasAttrExField) {
|
||||
entry.attrEx = dbc->getUInt32(i, attrExField);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
auto slIt = spellToSkillLine_.find(spellId);
|
||||
if (slIt == spellToSkillLine_.end()) return EMPTY_STRING;
|
||||
|
|
@ -23936,6 +24209,7 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) {
|
|||
"DB error", "Restricted account"};
|
||||
const char* errName = (result.errorCode < 9) ? errors[result.errorCode] : "Unknown";
|
||||
std::string msg = std::string("Auction ") + actionName + " failed: " + errName;
|
||||
addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
}
|
||||
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.castTime = packet.readUInt32();
|
||||
|
||||
// SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK)
|
||||
if (rem() < 2) {
|
||||
LOG_WARNING("[Classic] Spell start: missing targetFlags");
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
}
|
||||
uint16_t targetFlags = packet.readUInt16();
|
||||
// TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID
|
||||
if ((targetFlags & 0x02) || (targetFlags & 0x800)) {
|
||||
if (!hasFullPackedGuid(packet)) {
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
}
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
// SpellCastTargets: consume ALL target payload types so subsequent reads stay aligned.
|
||||
// Previously only UNIT(0x02)/OBJECT(0x800) were handled; DEST_LOCATION(0x40),
|
||||
// SOURCE_LOCATION(0x20), and ITEM(0x10) bytes were silently skipped, corrupting
|
||||
// castFlags/castTime for every AOE/ground-targeted spell (Rain of Fire, Blizzard, etc.).
|
||||
{
|
||||
uint64_t targetGuid = 0;
|
||||
// skipClassicSpellCastTargets reads uint16 targetFlags and all payloads.
|
||||
// Non-fatal on truncation: self-cast spells have zero-byte targets.
|
||||
skipClassicSpellCastTargets(packet, &targetGuid);
|
||||
data.targetGuid = targetGuid;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -765,6 +762,10 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
|
|||
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,
|
||||
" misses=", (int)data.missCount);
|
||||
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
|
||||
//
|
||||
// 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) {
|
||||
data = SpellStartData{};
|
||||
const size_t startPos = packet.getReadPos();
|
||||
if (packet.getSize() - packet.getReadPos() < 22) return false;
|
||||
|
||||
data.casterGuid = packet.readUInt64(); // full GUID (object)
|
||||
|
|
@ -1253,23 +1312,19 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData&
|
|||
data.castFlags = packet.readUInt32();
|
||||
data.castTime = packet.readUInt32();
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) {
|
||||
LOG_WARNING("[TBC] Spell start: missing targetFlags");
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
// SpellCastTargets: consume ALL target payload types to keep the read position
|
||||
// aligned for any bytes the caller may parse after this (ammo, etc.).
|
||||
// The previous code only read UNIT(0x02)/OBJECT(0x800) target GUIDs and left
|
||||
// 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();
|
||||
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT
|
||||
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");
|
||||
LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms",
|
||||
" targetGuid=0x", std::hex, data.targetGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1368,6 +1423,10 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data)
|
|||
}
|
||||
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,
|
||||
" misses=", (int)data.missCount);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ WardenEmulator::WardenEmulator()
|
|||
, heapBase_(HEAP_BASE)
|
||||
, heapSize_(HEAP_SIZE)
|
||||
, apiStubBase_(API_STUB_BASE)
|
||||
, nextApiStubAddr_(API_STUB_BASE)
|
||||
, apiCodeHookRegistered_(false)
|
||||
, nextHeapAddr_(HEAP_BASE)
|
||||
{
|
||||
}
|
||||
|
|
@ -51,8 +53,11 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
allocations_.clear();
|
||||
freeBlocks_.clear();
|
||||
apiAddresses_.clear();
|
||||
apiHandlers_.clear();
|
||||
hooks_.clear();
|
||||
nextHeapAddr_ = heapBase_;
|
||||
nextApiStubAddr_ = apiStubBase_;
|
||||
apiCodeHookRegistered_ = false;
|
||||
|
||||
{
|
||||
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);
|
||||
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];
|
||||
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,
|
||||
const std::string& functionName,
|
||||
[[maybe_unused]] std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler) {
|
||||
// Allocate address for this API stub
|
||||
static uint32_t nextStubAddr = API_STUB_BASE;
|
||||
uint32_t stubAddr = nextStubAddr;
|
||||
nextStubAddr += 16; // Space for stub code
|
||||
std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler) {
|
||||
// Allocate address for this API stub (16 bytes each)
|
||||
uint32_t stubAddr = nextApiStubAddr_;
|
||||
nextApiStubAddr_ += 16;
|
||||
|
||||
// Store mapping
|
||||
// Store address mapping for IAT patching
|
||||
apiAddresses_[dllName][functionName] = stubAddr;
|
||||
|
||||
{
|
||||
char hBuf[32];
|
||||
std::snprintf(hBuf, sizeof(hBuf), "0x%X", stubAddr);
|
||||
LOG_DEBUG("WardenEmulator: Hooked ", dllName, "!", functionName, " at ", hBuf);
|
||||
// Determine stdcall arg count from known Windows APIs so the hook can
|
||||
// clean up the stack correctly (RETN N convention).
|
||||
static const std::pair<const char*, int> knownArgCounts[] = {
|
||||
{"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
|
||||
// For now, just return the address for IAT patching
|
||||
// Store the handler so hookCode() can dispatch to it
|
||||
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;
|
||||
}
|
||||
|
|
@ -503,8 +537,40 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
|
|||
// Unicorn Callbacks
|
||||
// ============================================================================
|
||||
|
||||
void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) {
|
||||
(void)address; // Trace disabled by default to avoid log spam
|
||||
void WardenEmulator::hookCode(uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, void* userData) {
|
||||
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) {
|
||||
|
|
@ -533,7 +599,8 @@ WardenEmulator::WardenEmulator()
|
|||
: uc_(nullptr), moduleBase_(0), moduleSize_(0)
|
||||
, stackBase_(0), stackSize_(0)
|
||||
, heapBase_(0), heapSize_(0)
|
||||
, apiStubBase_(0), nextHeapAddr_(0) {}
|
||||
, apiStubBase_(0), nextApiStubAddr_(0), apiCodeHookRegistered_(false)
|
||||
, nextHeapAddr_(0) {}
|
||||
WardenEmulator::~WardenEmulator() {}
|
||||
bool WardenEmulator::initialize(const void*, size_t, uint32_t) { return false; }
|
||||
uint32_t WardenEmulator::hookAPI(const std::string&, const std::string&,
|
||||
|
|
|
|||
|
|
@ -161,24 +161,53 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
|||
}
|
||||
|
||||
try {
|
||||
// Call module's PacketHandler
|
||||
// void PacketHandler(uint8_t* checkData, size_t checkSize,
|
||||
// uint8_t* responseOut, size_t* responseSizeOut)
|
||||
LOG_INFO("WardenModule: Calling PacketHandler...");
|
||||
|
||||
// For now, this is a placeholder - actual calling would depend on
|
||||
// the module's exact function signature
|
||||
LOG_WARNING("WardenModule: PacketHandler execution stubbed");
|
||||
LOG_INFO("WardenModule: Would call emulated function to process checks");
|
||||
LOG_INFO("WardenModule: This would generate REAL responses (not fakes!)");
|
||||
|
||||
// Clean up
|
||||
if (emulatedPacketHandlerAddr_ == 0) {
|
||||
LOG_ERROR("WardenModule: PacketHandler address not set (module not fully initialized)");
|
||||
emulator_->freeMemory(checkDataAddr);
|
||||
emulator_->freeMemory(responseAddr);
|
||||
|
||||
// For now, return false to use fake responses
|
||||
// Once we have a real module, we'd read the response from responseAddr
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate uint32_t for responseSizeOut in emulated memory
|
||||
uint32_t initialSize = 1024;
|
||||
uint32_t responseSizeAddr = emulator_->writeData(&initialSize, sizeof(uint32_t));
|
||||
if (responseSizeAddr == 0) {
|
||||
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);
|
||||
}
|
||||
|
||||
emulator_->freeMemory(checkDataAddr);
|
||||
emulator_->freeMemory(responseAddr);
|
||||
return !responseOut.empty();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("WardenModule: Exception during PacketHandler: ", e.what());
|
||||
|
|
@ -196,25 +225,18 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
|||
return false;
|
||||
}
|
||||
|
||||
uint32_t WardenModule::tick([[maybe_unused]] uint32_t deltaMs) {
|
||||
uint32_t WardenModule::tick(uint32_t deltaMs) {
|
||||
if (!loaded_ || !funcList_.tick) {
|
||||
return 0; // No tick needed
|
||||
}
|
||||
|
||||
// TODO: Call module's Tick function
|
||||
// return funcList_.tick(deltaMs);
|
||||
|
||||
return 0;
|
||||
}
|
||||
return funcList_.tick(deltaMs);
|
||||
}
|
||||
|
||||
void WardenModule::generateRC4Keys([[maybe_unused]] uint8_t* packet) {
|
||||
void WardenModule::generateRC4Keys(uint8_t* packet) {
|
||||
if (!loaded_ || !funcList_.generateRC4Keys) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Call module's GenerateRC4Keys function
|
||||
// This re-keys the Warden crypto stream
|
||||
// funcList_.generateRC4Keys(packet);
|
||||
funcList_.generateRC4Keys(packet);
|
||||
}
|
||||
|
||||
void WardenModule::unload() {
|
||||
|
|
@ -222,8 +244,7 @@ void WardenModule::unload() {
|
|||
// Call module's Unload() function if loaded
|
||||
if (loaded_ && funcList_.unload) {
|
||||
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
|
||||
|
|
@ -240,6 +261,7 @@ void WardenModule::unload() {
|
|||
|
||||
// Clear function pointers
|
||||
funcList_ = {};
|
||||
emulatedPacketHandlerAddr_ = 0;
|
||||
|
||||
loaded_ = false;
|
||||
moduleData_.clear();
|
||||
|
|
@ -961,7 +983,12 @@ bool WardenModule::initializeModule() {
|
|||
}
|
||||
|
||||
// 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] = {};
|
||||
if (emulator_->readMemory(result, funcAddrs, 16)) {
|
||||
char fb[4][32];
|
||||
|
|
@ -973,11 +1000,48 @@ bool WardenModule::initializeModule() {
|
|||
LOG_INFO("WardenModule: packetHandler: ", fb[2]);
|
||||
LOG_INFO("WardenModule: tick: ", fb[3]);
|
||||
|
||||
// Store function addresses for later use
|
||||
// funcList_.generateRC4Keys = ... (would wrap emulator calls)
|
||||
// funcList_.unload = ...
|
||||
// funcList_.packetHandler = ...
|
||||
// funcList_.tick = ...
|
||||
// Wrap emulated function addresses into std::function dispatchers
|
||||
WardenEmulator* emu = emulator_.get();
|
||||
|
||||
if (funcAddrs[0]) {
|
||||
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!");
|
||||
|
|
|
|||
|
|
@ -3780,14 +3780,44 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
|
|||
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();
|
||||
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT
|
||||
if (needsTargetGuid) {
|
||||
if (!hasFullPackedGuid(packet)) {
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
|
||||
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 (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
|
||||
}
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
// 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");
|
||||
|
|
@ -3901,6 +3931,50 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
|||
}
|
||||
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,
|
||||
" misses=", (int)data.missCount);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -388,10 +388,11 @@ void CameraController::update(float deltaTime) {
|
|||
if (mounted_) sitting = false;
|
||||
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);
|
||||
if (rDown && !rKeyWasDown) {
|
||||
reset();
|
||||
resetAngles();
|
||||
}
|
||||
rKeyWasDown = rDown;
|
||||
|
||||
|
|
@ -1941,6 +1942,14 @@ void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) {
|
|||
mouseButtonDown = anyDown;
|
||||
}
|
||||
|
||||
void CameraController::resetAngles() {
|
||||
if (!camera) return;
|
||||
yaw = defaultYaw;
|
||||
facingYaw = defaultYaw;
|
||||
pitch = defaultPitch;
|
||||
camera->setRotation(yaw, pitch);
|
||||
}
|
||||
|
||||
void CameraController::reset() {
|
||||
if (!camera) {
|
||||
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() {
|
||||
if (!characterRenderer || characterInstanceId == 0) return;
|
||||
if (meleeSwingCooldown > 0.0f) return;
|
||||
|
|
@ -3012,6 +3196,8 @@ void Renderer::update(float deltaTime) {
|
|||
if (chargeEffect) {
|
||||
chargeEffect->update(deltaTime);
|
||||
}
|
||||
// Update transient spell visual instances
|
||||
updateSpellVisuals(deltaTime);
|
||||
|
||||
|
||||
// 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,
|
||||
", 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
|
||||
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
||||
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 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)
|
||||
ImGui::BeginGroup();
|
||||
if (totalArmor > 0) {
|
||||
ImGui::TextColored(gold, "Armor: %d", totalArmor);
|
||||
} else {
|
||||
ImGui::TextColored(gray, "Armor: 0");
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextWrapped("Reduces damage taken from physical attacks.");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
if (serverStats) {
|
||||
// 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) {
|
||||
int32_t total = serverStats[i];
|
||||
int32_t bonus = itemBonuses[i];
|
||||
ImGui::BeginGroup();
|
||||
if (bonus > 0) {
|
||||
ImGui::TextColored(white, "%s: %d", statNames[i], total);
|
||||
ImGui::SameLine();
|
||||
|
|
@ -1760,12 +1776,19 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
} else {
|
||||
ImGui::TextColored(gray, "%s: %d", statNames[i], total);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextWrapped("%s", kStatTooltips[i]);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: estimated base (20 + level) plus item query bonuses.
|
||||
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;
|
||||
ImGui::BeginGroup();
|
||||
if (equipBonus > 0) {
|
||||
ImGui::TextColored(white, "%s: %d", name, total);
|
||||
ImGui::SameLine();
|
||||
|
|
@ -1773,12 +1796,18 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
} else {
|
||||
ImGui::TextColored(gray, "%s: %d", name, total);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextWrapped("%s", tooltip);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
};
|
||||
renderStat("Strength", itemStr);
|
||||
renderStat("Agility", itemAgi);
|
||||
renderStat("Stamina", itemSta);
|
||||
renderStat("Intellect", itemInt);
|
||||
renderStat("Spirit", itemSpi);
|
||||
renderStat("Strength", itemStr, kStatTooltips[0]);
|
||||
renderStat("Agility", itemAgi, kStatTooltips[1]);
|
||||
renderStat("Stamina", itemSta, kStatTooltips[2]);
|
||||
renderStat("Intellect", itemInt, kStatTooltips[3]);
|
||||
renderStat("Spirit", itemSpi, kStatTooltips[4]);
|
||||
}
|
||||
|
||||
// Secondary stats from equipped items
|
||||
|
|
@ -1789,27 +1818,34 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
if (hasSecondary) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
auto renderSecondary = [&](const char* name, int32_t val) {
|
||||
auto renderSecondary = [&](const char* name, int32_t val, const char* tooltip) {
|
||||
if (val > 0) {
|
||||
ImGui::BeginGroup();
|
||||
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("Spell Power", itemSP);
|
||||
renderSecondary("Hit Rating", itemHit);
|
||||
renderSecondary("Crit Rating", itemCrit);
|
||||
renderSecondary("Haste Rating", itemHaste);
|
||||
renderSecondary("Resilience", itemResil);
|
||||
renderSecondary("Expertise", itemExpertise);
|
||||
renderSecondary("Defense Rating", itemDefense);
|
||||
renderSecondary("Dodge Rating", itemDodge);
|
||||
renderSecondary("Parry Rating", itemParry);
|
||||
renderSecondary("Block Rating", itemBlock);
|
||||
renderSecondary("Block Value", itemBlockVal);
|
||||
renderSecondary("Armor Penetration",itemArmorPen);
|
||||
renderSecondary("Spell Penetration",itemSpellPen);
|
||||
renderSecondary("Mana per 5 sec", itemMp5);
|
||||
renderSecondary("Health per 5 sec", itemHp5);
|
||||
renderSecondary("Attack Power", itemAP, "Increases the damage of your melee and ranged attacks.");
|
||||
renderSecondary("Spell Power", itemSP, "Increases the damage and healing of your spells.");
|
||||
renderSecondary("Hit Rating", itemHit, "Reduces the chance your attacks will miss.");
|
||||
renderSecondary("Crit Rating", itemCrit, "Increases your critical strike chance.");
|
||||
renderSecondary("Haste Rating", itemHaste, "Increases attack speed and spell casting speed.");
|
||||
renderSecondary("Resilience", itemResil, "Reduces the chance you will be critically hit.\nReduces damage taken from critical hits.");
|
||||
renderSecondary("Expertise", itemExpertise,"Reduces the chance your attacks will be dodged or parried.");
|
||||
renderSecondary("Defense Rating", itemDefense, "Reduces the chance enemies will critically hit you.");
|
||||
renderSecondary("Dodge Rating", itemDodge, "Increases your chance to dodge attacks.");
|
||||
renderSecondary("Parry Rating", itemParry, "Increases your chance to parry attacks.");
|
||||
renderSecondary("Block Rating", itemBlock, "Increases your chance to block attacks with your shield.");
|
||||
renderSecondary("Block Value", itemBlockVal, "Increases the amount of damage your shield blocks.");
|
||||
renderSecondary("Armor Penetration",itemArmorPen, "Reduces the armor of your target.");
|
||||
renderSecondary("Spell Penetration",itemSpellPen, "Reduces your target's resistance to your spells.");
|
||||
renderSecondary("Mana per 5 sec", itemMp5, "Restores mana every 5 seconds, even while casting.");
|
||||
renderSecondary("Health per 5 sec", itemHp5, "Restores health every 5 seconds.");
|
||||
}
|
||||
|
||||
// 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) {
|
||||
LOG_INFO("Right-click backpack item: name='", item.name,
|
||||
"' inventoryType=", (int)item.inventoryType,
|
||||
" itemId=", item.itemId);
|
||||
if (item.inventoryType > 0) {
|
||||
" itemId=", item.itemId,
|
||||
" 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);
|
||||
} else {
|
||||
gameHandler_->useItemBySlot(backpackIndex);
|
||||
|
|
@ -2308,8 +2348,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
LOG_INFO("Right-click bag item: name='", item.name,
|
||||
"' inventoryType=", (int)item.inventoryType,
|
||||
" bagIndex=", bagIndex, " slotIndex=", bagSlotIndex);
|
||||
if (item.inventoryType > 0) {
|
||||
" bagIndex=", bagIndex, " slotIndex=", bagSlotIndex,
|
||||
" 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);
|
||||
} else {
|
||||
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_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_SKILLS)] = ImGuiKey_K; // WoW standard: K opens Skills/Professions
|
||||
}
|
||||
|
||||
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_RAID_FRAMES: return "Raid Frames";
|
||||
case Action::TOGGLE_ACHIEVEMENTS: return "Achievements";
|
||||
case Action::TOGGLE_SKILLS: return "Skills / Professions";
|
||||
case Action::ACTION_COUNT: break;
|
||||
}
|
||||
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_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_skills") actionIdx = static_cast<int>(Action::TOGGLE_SKILLS);
|
||||
|
||||
if (actionIdx < 0) continue;
|
||||
|
||||
|
|
@ -254,6 +257,7 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
|
|||
{Action::TOGGLE_NAMEPLATES, "toggle_nameplates"},
|
||||
{Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"},
|
||||
{Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"},
|
||||
{Action::TOGGLE_SKILLS, "toggle_skills"},
|
||||
};
|
||||
|
||||
for (const auto& [action, nameStr] : actionMap) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue