mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
71 commits
785df23f1b
...
34bab8edd6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34bab8edd6 | ||
|
|
caf0d18393 | ||
|
|
7bbf2c7769 | ||
|
|
bae3477c94 | ||
|
|
458a95ae8e | ||
|
|
1ff48259cc | ||
|
|
9f8a0907c4 | ||
|
|
373dbbf95d | ||
|
|
2e38a9af65 | ||
|
|
53d144c51e | ||
|
|
2f3f9f1a21 | ||
|
|
fcb133dbbe | ||
|
|
63c09163dc | ||
|
|
56588e0dad | ||
|
|
59597ff39e | ||
|
|
068b6bc2cb | ||
|
|
62b7622f75 | ||
|
|
5fcf71e3ff | ||
|
|
321aaeae54 | ||
|
|
6075207d94 | ||
|
|
5004929f07 | ||
|
|
76bd6b409e | ||
|
|
f53f16a59b | ||
|
|
dce8a4e442 | ||
|
|
30058a8df5 | ||
|
|
1793549550 | ||
|
|
67db7383ad | ||
|
|
0afa41e908 | ||
|
|
094ef88e57 | ||
|
|
bee4dde9b7 | ||
|
|
ec5e7c66c3 | ||
|
|
1a370fef76 | ||
|
|
942df21c66 | ||
|
|
4987388ce7 | ||
|
|
df47d425f4 | ||
|
|
60ebb565bb | ||
|
|
920d6ac120 | ||
|
|
132598fc88 | ||
|
|
a9ddfe70c2 | ||
|
|
e2f65dfc59 | ||
|
|
a33f635490 | ||
|
|
23293d6453 | ||
|
|
56ec49f837 | ||
|
|
a1ee9827d8 | ||
|
|
27d18b2189 | ||
|
|
1853e8aa56 | ||
|
|
0b99cbafb2 | ||
|
|
701cb94ba6 | ||
|
|
f2337aeaa7 | ||
|
|
dd6f6d1174 | ||
|
|
21604461fc | ||
|
|
ea291179dd | ||
|
|
9291637977 | ||
|
|
4cf73a6def | ||
|
|
70abb12398 | ||
|
|
c622fde7be | ||
|
|
dd3f9e5b9e | ||
|
|
8856af6b2d | ||
|
|
acbfe99401 | ||
|
|
2717018631 | ||
|
|
8a20ccb69d | ||
|
|
30a65320fb | ||
|
|
a33119c070 | ||
|
|
1180f0227c | ||
|
|
8152314ba8 | ||
|
|
cfc6dc37c8 | ||
|
|
84558fda69 | ||
|
|
c72186fd11 | ||
|
|
b3441ee9ce | ||
|
|
ca141bb131 | ||
|
|
71cabddbd6 |
29 changed files with 2138 additions and 363 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1
|
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||||
|
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33
|
||||||
},
|
},
|
||||||
|
"SpellRange": { "MaxRange": 2 },
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@
|
||||||
"PLAYER_END": 1282,
|
"PLAYER_END": 1282,
|
||||||
"GAMEOBJECT_DISPLAYID": 8,
|
"GAMEOBJECT_DISPLAYID": 8,
|
||||||
"ITEM_FIELD_STACK_COUNT": 14,
|
"ITEM_FIELD_STACK_COUNT": 14,
|
||||||
|
"ITEM_FIELD_DURABILITY": 48,
|
||||||
|
"ITEM_FIELD_MAXDURABILITY": 49,
|
||||||
"CONTAINER_FIELD_NUM_SLOTS": 48,
|
"CONTAINER_FIELD_NUM_SLOTS": 48,
|
||||||
"CONTAINER_FIELD_SLOT_1": 50
|
"CONTAINER_FIELD_SLOT_1": 50
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||||
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215
|
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215,
|
||||||
|
"CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40
|
||||||
},
|
},
|
||||||
|
"SpellRange": { "MaxRange": 4 },
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@
|
||||||
"PLAYER_EXPLORED_ZONES_START": 1312,
|
"PLAYER_EXPLORED_ZONES_START": 1312,
|
||||||
"GAMEOBJECT_DISPLAYID": 8,
|
"GAMEOBJECT_DISPLAYID": 8,
|
||||||
"ITEM_FIELD_STACK_COUNT": 14,
|
"ITEM_FIELD_STACK_COUNT": 14,
|
||||||
|
"ITEM_FIELD_DURABILITY": 60,
|
||||||
|
"ITEM_FIELD_MAXDURABILITY": 61,
|
||||||
"CONTAINER_FIELD_NUM_SLOTS": 64,
|
"CONTAINER_FIELD_NUM_SLOTS": 64,
|
||||||
"CONTAINER_FIELD_SLOT_1": 66
|
"CONTAINER_FIELD_SLOT_1": 66
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1
|
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||||
|
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33
|
||||||
},
|
},
|
||||||
|
"SpellRange": { "MaxRange": 2 },
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@
|
||||||
"PLAYER_END": 1282,
|
"PLAYER_END": 1282,
|
||||||
"GAMEOBJECT_DISPLAYID": 8,
|
"GAMEOBJECT_DISPLAYID": 8,
|
||||||
"ITEM_FIELD_STACK_COUNT": 14,
|
"ITEM_FIELD_STACK_COUNT": 14,
|
||||||
|
"ITEM_FIELD_DURABILITY": 48,
|
||||||
|
"ITEM_FIELD_MAXDURABILITY": 49,
|
||||||
"CONTAINER_FIELD_NUM_SLOTS": 48,
|
"CONTAINER_FIELD_NUM_SLOTS": 48,
|
||||||
"CONTAINER_FIELD_SLOT_1": 50
|
"CONTAINER_FIELD_SLOT_1": 50
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 4, "IconID": 133,
|
"ID": 0, "Attributes": 4, "IconID": 133,
|
||||||
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225
|
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225,
|
||||||
|
"PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49
|
||||||
},
|
},
|
||||||
|
"SpellRange": { "MaxRange": 4 },
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@
|
||||||
"PLAYER_EXPLORED_ZONES_START": 1041,
|
"PLAYER_EXPLORED_ZONES_START": 1041,
|
||||||
"GAMEOBJECT_DISPLAYID": 8,
|
"GAMEOBJECT_DISPLAYID": 8,
|
||||||
"ITEM_FIELD_STACK_COUNT": 14,
|
"ITEM_FIELD_STACK_COUNT": 14,
|
||||||
|
"ITEM_FIELD_DURABILITY": 60,
|
||||||
|
"ITEM_FIELD_MAXDURABILITY": 61,
|
||||||
"CONTAINER_FIELD_NUM_SLOTS": 64,
|
"CONTAINER_FIELD_NUM_SLOTS": 64,
|
||||||
"CONTAINER_FIELD_SLOT_1": 66
|
"CONTAINER_FIELD_SLOT_1": 66
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
|
||||||
- **Gossip** -- NPC interaction, dialogue options
|
- **Gossip** -- NPC interaction, dialogue options
|
||||||
- **Chat** -- Tabs/channels, emotes, chat bubbles, clickable URLs, clickable item links with tooltips
|
- **Chat** -- Tabs/channels, emotes, chat bubbles, clickable URLs, clickable item links with tooltips
|
||||||
- **Party** -- Group invites, party list, out-of-range member health via SMSG_PARTY_MEMBER_STATS
|
- **Party** -- Group invites, party list, out-of-range member health via SMSG_PARTY_MEMBER_STATS
|
||||||
- **Pets** -- Pet tracking via SMSG_PET_SPELLS, dismiss pet button
|
- **Pets** -- Pet tracking via SMSG_PET_SPELLS, action bar (10 slots with icon/autocast tinting/tooltips), dismiss button
|
||||||
- **Map Exploration** -- Subzone-level fog-of-war reveal matching retail behavior
|
- **Map Exploration** -- Subzone-level fog-of-war reveal matching retail behavior
|
||||||
- **Warden** -- Warden anti-cheat module execution via Unicorn Engine x86 emulation (cross-platform, no Wine)
|
- **Warden** -- Warden anti-cheat module execution via Unicorn Engine x86 emulation (cross-platform, no Wine)
|
||||||
- **UI** -- Loading screens with progress bar, settings window (shadow distance slider), minimap with zoom/rotation/square mode, top-right minimap mute speaker, separate bag windows with compact-empty mode (aggregate view)
|
- **UI** -- Loading screens with progress bar, settings window (shadow distance slider), minimap with zoom/rotation/square mode, top-right minimap mute speaker, separate bag windows with compact-empty mode (aggregate view)
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,13 @@ private:
|
||||||
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
|
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
|
||||||
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
||||||
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
|
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
|
||||||
std::unordered_map<uint64_t, bool> creatureWasMoving_; // guid -> previous-frame movement state
|
std::unordered_map<uint64_t, bool> creatureWasMoving_; // guid -> previous-frame movement state
|
||||||
|
std::unordered_map<uint64_t, bool> creatureWasSwimming_; // guid -> previous-frame swim state (for anim transition detection)
|
||||||
|
std::unordered_map<uint64_t, bool> creatureWasFlying_; // guid -> previous-frame flying state (for anim transition detection)
|
||||||
|
std::unordered_map<uint64_t, bool> creatureWasWalking_; // guid -> previous-frame walking state (walk vs run transition detection)
|
||||||
std::unordered_map<uint64_t, bool> creatureSwimmingState_; // guid -> currently in swim mode (SWIMMING flag)
|
std::unordered_map<uint64_t, bool> creatureSwimmingState_; // guid -> currently in swim mode (SWIMMING flag)
|
||||||
std::unordered_map<uint64_t, bool> creatureWalkingState_; // guid -> walking (WALKING flag, selects Walk(4) vs Run(5))
|
std::unordered_map<uint64_t, bool> creatureWalkingState_; // guid -> walking (WALKING flag, selects Walk(4) vs Run(5))
|
||||||
|
std::unordered_map<uint64_t, bool> creatureFlyingState_; // guid -> currently flying (FLYING flag)
|
||||||
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
|
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
|
||||||
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
|
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
|
||||||
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check
|
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check
|
||||||
|
|
|
||||||
|
|
@ -588,10 +588,14 @@ public:
|
||||||
const std::unordered_map<uint32_t, TalentTabEntry>& getAllTalentTabs() const { return talentTabCache_; }
|
const std::unordered_map<uint32_t, TalentTabEntry>& getAllTalentTabs() const { return talentTabCache_; }
|
||||||
void loadTalentDbc();
|
void loadTalentDbc();
|
||||||
|
|
||||||
// Action bar — 2 bars × 12 slots = 24 total
|
// Action bar — 4 bars × 12 slots = 48 total
|
||||||
|
// Bar 0 (slots 0-11): main bottom bar (1-0, -, =)
|
||||||
|
// Bar 1 (slots 12-23): second bar above main (Shift+1 ... Shift+=)
|
||||||
|
// Bar 2 (slots 24-35): right side vertical bar
|
||||||
|
// Bar 3 (slots 36-47): left side vertical bar
|
||||||
static constexpr int SLOTS_PER_BAR = 12;
|
static constexpr int SLOTS_PER_BAR = 12;
|
||||||
static constexpr int ACTION_BARS = 2;
|
static constexpr int ACTION_BARS = 4;
|
||||||
static constexpr int ACTION_BAR_SLOTS = SLOTS_PER_BAR * ACTION_BARS; // 24
|
static constexpr int ACTION_BAR_SLOTS = SLOTS_PER_BAR * ACTION_BARS; // 48
|
||||||
std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() { return actionBar; }
|
std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() { return actionBar; }
|
||||||
const std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() const { return actionBar; }
|
const std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() const { return actionBar; }
|
||||||
void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id);
|
void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id);
|
||||||
|
|
@ -693,6 +697,11 @@ public:
|
||||||
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z, bool isInitialEntry)>;
|
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z, bool isInitialEntry)>;
|
||||||
void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); }
|
void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
// Knockback callback: called when server sends SMSG_MOVE_KNOCK_BACK for the player.
|
||||||
|
// Parameters: vcos, vsin (render-space direction), hspeed, vspeed (raw from packet).
|
||||||
|
using KnockBackCallback = std::function<void(float vcos, float vsin, float hspeed, float vspeed)>;
|
||||||
|
void setKnockBackCallback(KnockBackCallback cb) { knockBackCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// Unstuck callback (resets player Z to floor height)
|
// Unstuck callback (resets player Z to floor height)
|
||||||
using UnstuckCallback = std::function<void()>;
|
using UnstuckCallback = std::function<void()>;
|
||||||
void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); }
|
void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); }
|
||||||
|
|
@ -818,6 +827,22 @@ public:
|
||||||
|
|
||||||
// Player GUID
|
// Player GUID
|
||||||
uint64_t getPlayerGuid() const { return playerGuid; }
|
uint64_t getPlayerGuid() const { return playerGuid; }
|
||||||
|
|
||||||
|
// Look up a display name for any guid: checks playerNameCache then entity manager.
|
||||||
|
// Returns empty string if unknown. Used by chat display to resolve names at render time.
|
||||||
|
const std::string& lookupName(uint64_t guid) const {
|
||||||
|
static const std::string kEmpty;
|
||||||
|
auto it = playerNameCache.find(guid);
|
||||||
|
if (it != playerNameCache.end()) return it->second;
|
||||||
|
auto entity = entityManager.getEntity(guid);
|
||||||
|
if (entity) {
|
||||||
|
if (auto* unit = dynamic_cast<const Unit*>(entity.get())) {
|
||||||
|
if (!unit->getName().empty()) return unit->getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t getPlayerClass() const {
|
uint8_t getPlayerClass() const {
|
||||||
const Character* ch = getActiveCharacter();
|
const Character* ch = getActiveCharacter();
|
||||||
return ch ? static_cast<uint8_t>(ch->characterClass) : 0;
|
return ch ? static_cast<uint8_t>(ch->characterClass) : 0;
|
||||||
|
|
@ -830,6 +855,10 @@ public:
|
||||||
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
|
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
|
||||||
bool showResurrectDialog() const { return resurrectRequestPending_; }
|
bool showResurrectDialog() const { return resurrectRequestPending_; }
|
||||||
const std::string& getResurrectCasterName() const { return resurrectCasterName_; }
|
const std::string& getResurrectCasterName() const { return resurrectCasterName_; }
|
||||||
|
bool showTalentWipeConfirmDialog() const { return talentWipePending_; }
|
||||||
|
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
|
||||||
|
void confirmTalentWipe();
|
||||||
|
void cancelTalentWipe() { talentWipePending_ = false; }
|
||||||
/** True when ghost is within 40 yards of corpse position (same map). */
|
/** True when ghost is within 40 yards of corpse position (same map). */
|
||||||
bool canReclaimCorpse() const;
|
bool canReclaimCorpse() const;
|
||||||
/** Send CMSG_RECLAIM_CORPSE; noop if not a ghost or not near corpse. */
|
/** Send CMSG_RECLAIM_CORPSE; noop if not a ghost or not near corpse. */
|
||||||
|
|
@ -944,6 +973,7 @@ public:
|
||||||
void lfgJoin(uint32_t dungeonId, uint8_t roles);
|
void lfgJoin(uint32_t dungeonId, uint8_t roles);
|
||||||
void lfgLeave();
|
void lfgLeave();
|
||||||
void lfgAcceptProposal(uint32_t proposalId, bool accept);
|
void lfgAcceptProposal(uint32_t proposalId, bool accept);
|
||||||
|
void lfgSetBootVote(bool vote);
|
||||||
void lfgTeleport(bool toLfgDungeon = true);
|
void lfgTeleport(bool toLfgDungeon = true);
|
||||||
LfgState getLfgState() const { return lfgState_; }
|
LfgState getLfgState() const { return lfgState_; }
|
||||||
bool isLfgQueued() const { return lfgState_ == LfgState::Queued; }
|
bool isLfgQueued() const { return lfgState_ == LfgState::Queued; }
|
||||||
|
|
@ -1142,6 +1172,39 @@ public:
|
||||||
bool isMounted() const { return currentMountDisplayId_ != 0; }
|
bool isMounted() const { return currentMountDisplayId_ != 0; }
|
||||||
bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
|
bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
|
||||||
float getServerRunSpeed() const { return serverRunSpeed_; }
|
float getServerRunSpeed() const { return serverRunSpeed_; }
|
||||||
|
float getServerWalkSpeed() const { return serverWalkSpeed_; }
|
||||||
|
float getServerSwimSpeed() const { return serverSwimSpeed_; }
|
||||||
|
float getServerSwimBackSpeed() const { return serverSwimBackSpeed_; }
|
||||||
|
float getServerFlightSpeed() const { return serverFlightSpeed_; }
|
||||||
|
float getServerFlightBackSpeed() const { return serverFlightBackSpeed_; }
|
||||||
|
float getServerRunBackSpeed() const { return serverRunBackSpeed_; }
|
||||||
|
float getServerTurnRate() const { return serverTurnRate_; }
|
||||||
|
bool isPlayerRooted() const {
|
||||||
|
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::ROOT)) != 0;
|
||||||
|
}
|
||||||
|
bool isGravityDisabled() const {
|
||||||
|
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::LEVITATING)) != 0;
|
||||||
|
}
|
||||||
|
bool isFeatherFalling() const {
|
||||||
|
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::FEATHER_FALL)) != 0;
|
||||||
|
}
|
||||||
|
bool isWaterWalking() const {
|
||||||
|
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::WATER_WALK)) != 0;
|
||||||
|
}
|
||||||
|
bool isPlayerFlying() const {
|
||||||
|
const uint32_t flyMask = static_cast<uint32_t>(MovementFlags::CAN_FLY) |
|
||||||
|
static_cast<uint32_t>(MovementFlags::FLYING);
|
||||||
|
return (movementInfo.flags & flyMask) == flyMask;
|
||||||
|
}
|
||||||
|
bool isHovering() const {
|
||||||
|
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::HOVER)) != 0;
|
||||||
|
}
|
||||||
|
bool isSwimming() const {
|
||||||
|
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::SWIMMING)) != 0;
|
||||||
|
}
|
||||||
|
// Set the character pitch angle (radians) for movement packets (flight / swimming).
|
||||||
|
// Positive = nose up, negative = nose down.
|
||||||
|
void setMovementPitch(float radians) { movementInfo.pitch = radians; }
|
||||||
void dismount();
|
void dismount();
|
||||||
|
|
||||||
// Taxi / Flight Paths
|
// Taxi / Flight Paths
|
||||||
|
|
@ -1195,6 +1258,8 @@ public:
|
||||||
uint32_t count = 1;
|
uint32_t count = 1;
|
||||||
};
|
};
|
||||||
void buyBackItem(uint32_t buybackSlot);
|
void buyBackItem(uint32_t buybackSlot);
|
||||||
|
void repairItem(uint64_t vendorGuid, uint64_t itemGuid);
|
||||||
|
void repairAll(uint64_t vendorGuid, bool useGuildBank = false);
|
||||||
const std::deque<BuybackItem>& getBuybackItems() const { return buybackItems_; }
|
const std::deque<BuybackItem>& getBuybackItems() const { return buybackItems_; }
|
||||||
void autoEquipItemBySlot(int backpackIndex);
|
void autoEquipItemBySlot(int backpackIndex);
|
||||||
void autoEquipItemInBag(int bagIndex, int slotIndex);
|
void autoEquipItemInBag(int bagIndex, int slotIndex);
|
||||||
|
|
@ -1206,6 +1271,7 @@ public:
|
||||||
void useItemById(uint32_t itemId);
|
void useItemById(uint32_t itemId);
|
||||||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||||
|
void setVendorCanRepair(bool v) { currentVendorItems.canRepair = v; }
|
||||||
|
|
||||||
// Mail
|
// Mail
|
||||||
bool isMailboxOpen() const { return mailboxOpen_; }
|
bool isMailboxOpen() const { return mailboxOpen_; }
|
||||||
|
|
@ -1535,6 +1601,7 @@ private:
|
||||||
void handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage);
|
void handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage);
|
||||||
void handleForceMoveRootState(network::Packet& packet, bool rooted);
|
void handleForceMoveRootState(network::Packet& packet, bool rooted);
|
||||||
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
|
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
|
||||||
|
void handleMoveSetCollisionHeight(network::Packet& packet);
|
||||||
void handleMoveKnockBack(network::Packet& packet);
|
void handleMoveKnockBack(network::Packet& packet);
|
||||||
|
|
||||||
// ---- Area trigger detection ----
|
// ---- Area trigger detection ----
|
||||||
|
|
@ -1679,6 +1746,12 @@ private:
|
||||||
uint32_t lastMovementTimestampMs_ = 0;
|
uint32_t lastMovementTimestampMs_ = 0;
|
||||||
bool serverMovementAllowed_ = true;
|
bool serverMovementAllowed_ = true;
|
||||||
|
|
||||||
|
// Fall/jump tracking for movement packet correctness.
|
||||||
|
// fallTime must be the elapsed ms since the FALLING flag was set; the server
|
||||||
|
// uses it for fall-damage calculations and anti-cheat validation.
|
||||||
|
bool isFalling_ = false;
|
||||||
|
uint32_t fallStartMs_ = 0; // movementInfo.time value when FALLING started
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
Inventory inventory;
|
Inventory inventory;
|
||||||
|
|
||||||
|
|
@ -1761,6 +1834,8 @@ private:
|
||||||
struct OnlineItemInfo {
|
struct OnlineItemInfo {
|
||||||
uint32_t entry = 0;
|
uint32_t entry = 0;
|
||||||
uint32_t stackCount = 1;
|
uint32_t stackCount = 1;
|
||||||
|
uint32_t curDurability = 0;
|
||||||
|
uint32_t maxDurability = 0;
|
||||||
};
|
};
|
||||||
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
|
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
|
||||||
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
|
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
|
||||||
|
|
@ -1810,6 +1885,7 @@ private:
|
||||||
|
|
||||||
// ---- Phase 3: Spells ----
|
// ---- Phase 3: Spells ----
|
||||||
WorldEntryCallback worldEntryCallback_;
|
WorldEntryCallback worldEntryCallback_;
|
||||||
|
KnockBackCallback knockBackCallback_;
|
||||||
UnstuckCallback unstuckCallback_;
|
UnstuckCallback unstuckCallback_;
|
||||||
UnstuckCallback unstuckGyCallback_;
|
UnstuckCallback unstuckGyCallback_;
|
||||||
UnstuckCallback unstuckHearthCallback_;
|
UnstuckCallback unstuckHearthCallback_;
|
||||||
|
|
@ -2312,6 +2388,10 @@ private:
|
||||||
uint64_t pendingSpiritHealerGuid_ = 0;
|
uint64_t pendingSpiritHealerGuid_ = 0;
|
||||||
bool resurrectPending_ = false;
|
bool resurrectPending_ = false;
|
||||||
bool resurrectRequestPending_ = false;
|
bool resurrectRequestPending_ = false;
|
||||||
|
// ---- Talent wipe confirm dialog ----
|
||||||
|
bool talentWipePending_ = false;
|
||||||
|
uint64_t talentWipeNpcGuid_ = 0;
|
||||||
|
uint32_t talentWipeCost_ = 0;
|
||||||
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
|
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
|
||||||
uint64_t resurrectCasterGuid_ = 0;
|
uint64_t resurrectCasterGuid_ = 0;
|
||||||
std::string resurrectCasterName_;
|
std::string resurrectCasterName_;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|
@ -46,6 +47,16 @@ struct ItemDef {
|
||||||
int32_t spirit = 0;
|
int32_t spirit = 0;
|
||||||
uint32_t displayInfoId = 0;
|
uint32_t displayInfoId = 0;
|
||||||
uint32_t sellPrice = 0;
|
uint32_t sellPrice = 0;
|
||||||
|
uint32_t curDurability = 0;
|
||||||
|
uint32_t maxDurability = 0;
|
||||||
|
uint32_t itemLevel = 0;
|
||||||
|
uint32_t requiredLevel = 0;
|
||||||
|
uint32_t bindType = 0; // 0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ
|
||||||
|
std::string description; // Flavor/lore text shown in tooltip (italic yellow)
|
||||||
|
// Generic stat pairs for non-primary stats (hit, crit, haste, AP, SP, etc.)
|
||||||
|
struct ExtraStat { uint32_t statType = 0; int32_t statValue = 0; };
|
||||||
|
std::vector<ExtraStat> extraStats;
|
||||||
|
uint32_t startQuestId = 0; // Non-zero: item begins a quest
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ItemSlot {
|
struct ItemSlot {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ enum class UF : uint16_t {
|
||||||
|
|
||||||
// Item fields
|
// Item fields
|
||||||
ITEM_FIELD_STACK_COUNT,
|
ITEM_FIELD_STACK_COUNT,
|
||||||
|
ITEM_FIELD_DURABILITY,
|
||||||
|
ITEM_FIELD_MAXDURABILITY,
|
||||||
|
|
||||||
// Container fields
|
// Container fields
|
||||||
CONTAINER_FIELD_NUM_SLOTS,
|
CONTAINER_FIELD_NUM_SLOTS,
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,8 @@ enum class MovementFlags : uint32_t {
|
||||||
ROOT = 0x00000800,
|
ROOT = 0x00000800,
|
||||||
FALLING = 0x00001000,
|
FALLING = 0x00001000,
|
||||||
FALLINGFAR = 0x00002000,
|
FALLINGFAR = 0x00002000,
|
||||||
|
FEATHER_FALL = 0x00004000, // Slow fall / Parachute
|
||||||
|
WATER_WALK = 0x00008000, // Walk on water surface
|
||||||
SWIMMING = 0x00200000,
|
SWIMMING = 0x00200000,
|
||||||
ASCENDING = 0x00400000,
|
ASCENDING = 0x00400000,
|
||||||
CAN_FLY = 0x00800000,
|
CAN_FLY = 0x00800000,
|
||||||
|
|
@ -613,7 +615,11 @@ enum class ChatType : uint8_t {
|
||||||
MONSTER_WHISPER = 42,
|
MONSTER_WHISPER = 42,
|
||||||
RAID_BOSS_WHISPER = 43,
|
RAID_BOSS_WHISPER = 43,
|
||||||
RAID_BOSS_EMOTE = 44,
|
RAID_BOSS_EMOTE = 44,
|
||||||
MONSTER_PARTY = 50
|
MONSTER_PARTY = 50,
|
||||||
|
// BG/Arena system messages (WoW 3.3.5a — no sender, treated as SYSTEM in display)
|
||||||
|
BG_SYSTEM_NEUTRAL = 82,
|
||||||
|
BG_SYSTEM_ALLIANCE = 83,
|
||||||
|
BG_SYSTEM_HORDE = 84
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1546,6 +1552,8 @@ struct ItemQueryResponseData {
|
||||||
int32_t intellect = 0;
|
int32_t intellect = 0;
|
||||||
int32_t spirit = 0;
|
int32_t spirit = 0;
|
||||||
uint32_t sellPrice = 0;
|
uint32_t sellPrice = 0;
|
||||||
|
uint32_t itemLevel = 0;
|
||||||
|
uint32_t requiredLevel = 0;
|
||||||
std::string subclassName;
|
std::string subclassName;
|
||||||
// Item spells (up to 5)
|
// Item spells (up to 5)
|
||||||
struct ItemSpell {
|
struct ItemSpell {
|
||||||
|
|
@ -1553,6 +1561,12 @@ struct ItemQueryResponseData {
|
||||||
uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn
|
uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn
|
||||||
};
|
};
|
||||||
std::array<ItemSpell, 5> spells{};
|
std::array<ItemSpell, 5> spells{};
|
||||||
|
uint32_t bindType = 0; // 0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ
|
||||||
|
std::string description; // Flavor/lore text
|
||||||
|
// Generic stat pairs for non-primary stats (hit, crit, haste, AP, SP, etc.)
|
||||||
|
struct ExtraStat { uint32_t statType = 0; int32_t statValue = 0; };
|
||||||
|
std::vector<ExtraStat> extraStats;
|
||||||
|
uint32_t startQuestId = 0; // Non-zero: item begins a quest
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2072,6 +2086,14 @@ public:
|
||||||
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Reward item entry (shared by quest detail/offer windows) */
|
||||||
|
struct QuestRewardItem {
|
||||||
|
uint32_t itemId = 0;
|
||||||
|
uint32_t count = 0;
|
||||||
|
uint32_t displayInfoId = 0;
|
||||||
|
uint32_t choiceSlot = 0; // Original reward slot index from server payload
|
||||||
|
};
|
||||||
|
|
||||||
/** SMSG_QUESTGIVER_QUEST_DETAILS data (simplified) */
|
/** SMSG_QUESTGIVER_QUEST_DETAILS data (simplified) */
|
||||||
struct QuestDetailsData {
|
struct QuestDetailsData {
|
||||||
uint64_t npcGuid = 0;
|
uint64_t npcGuid = 0;
|
||||||
|
|
@ -2082,6 +2104,8 @@ struct QuestDetailsData {
|
||||||
uint32_t suggestedPlayers = 0;
|
uint32_t suggestedPlayers = 0;
|
||||||
uint32_t rewardMoney = 0;
|
uint32_t rewardMoney = 0;
|
||||||
uint32_t rewardXp = 0;
|
uint32_t rewardXp = 0;
|
||||||
|
std::vector<QuestRewardItem> rewardChoiceItems; // Player picks one of these
|
||||||
|
std::vector<QuestRewardItem> rewardItems; // These are always given
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SMSG_QUESTGIVER_QUEST_DETAILS parser */
|
/** SMSG_QUESTGIVER_QUEST_DETAILS parser */
|
||||||
|
|
@ -2090,14 +2114,6 @@ public:
|
||||||
static bool parse(network::Packet& packet, QuestDetailsData& data);
|
static bool parse(network::Packet& packet, QuestDetailsData& data);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Reward item entry (shared by quest detail/offer windows) */
|
|
||||||
struct QuestRewardItem {
|
|
||||||
uint32_t itemId = 0;
|
|
||||||
uint32_t count = 0;
|
|
||||||
uint32_t displayInfoId = 0;
|
|
||||||
uint32_t choiceSlot = 0; // Original reward slot index from server payload
|
|
||||||
};
|
|
||||||
|
|
||||||
/** SMSG_QUESTGIVER_REQUEST_ITEMS data (turn-in progress check) */
|
/** SMSG_QUESTGIVER_REQUEST_ITEMS data (turn-in progress check) */
|
||||||
struct QuestRequestItemsData {
|
struct QuestRequestItemsData {
|
||||||
uint64_t npcGuid = 0;
|
uint64_t npcGuid = 0;
|
||||||
|
|
@ -2173,6 +2189,7 @@ struct VendorItem {
|
||||||
struct ListInventoryData {
|
struct ListInventoryData {
|
||||||
uint64_t vendorGuid = 0;
|
uint64_t vendorGuid = 0;
|
||||||
std::vector<VendorItem> items;
|
std::vector<VendorItem> items;
|
||||||
|
bool canRepair = false; // Set when vendor was opened via GOSSIP_OPTION_ARMORER
|
||||||
|
|
||||||
bool isValid() const { return true; }
|
bool isValid() const { return true; }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,21 @@ public:
|
||||||
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); }
|
||||||
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
void setUseWoWSpeed(bool use) { useWoWSpeed = use; }
|
||||||
void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; }
|
void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; }
|
||||||
|
void setWalkSpeedOverride(float speed) { walkSpeedOverride_ = speed; }
|
||||||
|
void setSwimSpeedOverride(float speed) { swimSpeedOverride_ = speed; }
|
||||||
|
void setSwimBackSpeedOverride(float speed) { swimBackSpeedOverride_ = speed; }
|
||||||
|
void setFlightSpeedOverride(float speed) { flightSpeedOverride_ = speed; }
|
||||||
|
void setFlightBackSpeedOverride(float speed) { flightBackSpeedOverride_ = speed; }
|
||||||
|
void setRunBackSpeedOverride(float speed) { runBackSpeedOverride_ = speed; }
|
||||||
|
// Server turn rate in rad/s (SMSG_FORCE_TURN_RATE_CHANGE); 0 = use WOW_TURN_SPEED default
|
||||||
|
void setTurnRateOverride(float rateRadS) { turnRateOverride_ = rateRadS; }
|
||||||
|
void setMovementRooted(bool rooted) { movementRooted_ = rooted; }
|
||||||
|
bool isMovementRooted() const { return movementRooted_; }
|
||||||
|
void setGravityDisabled(bool disabled) { gravityDisabled_ = disabled; }
|
||||||
|
void setFeatherFallActive(bool active) { featherFallActive_ = active; }
|
||||||
|
void setWaterWalkActive(bool active) { waterWalkActive_ = active; }
|
||||||
|
void setFlyingActive(bool active) { flyingActive_ = active; }
|
||||||
|
void setHoverActive(bool active) { hoverActive_ = active; }
|
||||||
void setMounted(bool m) { mounted_ = m; }
|
void setMounted(bool m) { mounted_ = m; }
|
||||||
void setMountHeightOffset(float offset) { mountHeightOffset_ = offset; }
|
void setMountHeightOffset(float offset) { mountHeightOffset_ = offset; }
|
||||||
void setExternalFollow(bool enabled) { externalFollow_ = enabled; }
|
void setExternalFollow(bool enabled) { externalFollow_ = enabled; }
|
||||||
|
|
@ -103,6 +118,12 @@ public:
|
||||||
// Trigger mount jump (applies vertical velocity for physics hop)
|
// Trigger mount jump (applies vertical velocity for physics hop)
|
||||||
void triggerMountJump();
|
void triggerMountJump();
|
||||||
|
|
||||||
|
// Apply server-driven knockback impulse.
|
||||||
|
// dir: render-space 2D direction unit vector (from vcos/vsin in packet)
|
||||||
|
// hspeed: horizontal speed magnitude (units/s)
|
||||||
|
// vspeed: raw packet vspeed field (server sends negative for upward launch)
|
||||||
|
void applyKnockBack(float vcos, float vsin, float hspeed, float vspeed);
|
||||||
|
|
||||||
// For first-person player hiding
|
// For first-person player hiding
|
||||||
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||||
characterRenderer = cr;
|
characterRenderer = cr;
|
||||||
|
|
@ -235,6 +256,8 @@ private:
|
||||||
bool wasTurningRight = false;
|
bool wasTurningRight = false;
|
||||||
bool wasJumping = false;
|
bool wasJumping = false;
|
||||||
bool wasFalling = false;
|
bool wasFalling = false;
|
||||||
|
bool wasAscending_ = false; // Space held while flyingActive_
|
||||||
|
bool wasDescending_ = false; // X held while flyingActive_
|
||||||
bool moveForwardActive = false;
|
bool moveForwardActive = false;
|
||||||
bool moveBackwardActive = false;
|
bool moveBackwardActive = false;
|
||||||
bool strafeLeftActive = false;
|
bool strafeLeftActive = false;
|
||||||
|
|
@ -260,8 +283,27 @@ private:
|
||||||
return std::sqrt(2.0f * std::abs(MOUNT_GRAVITY) * MOUNT_JUMP_HEIGHT);
|
return std::sqrt(2.0f * std::abs(MOUNT_GRAVITY) * MOUNT_JUMP_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-driven run speed override (0 = use default WOW_RUN_SPEED)
|
// Server-driven speed overrides (0 = use hardcoded default)
|
||||||
float runSpeedOverride_ = 0.0f;
|
float runSpeedOverride_ = 0.0f;
|
||||||
|
float walkSpeedOverride_ = 0.0f;
|
||||||
|
float swimSpeedOverride_ = 0.0f;
|
||||||
|
float swimBackSpeedOverride_ = 0.0f;
|
||||||
|
float flightSpeedOverride_ = 0.0f;
|
||||||
|
float flightBackSpeedOverride_ = 0.0f;
|
||||||
|
float runBackSpeedOverride_ = 0.0f;
|
||||||
|
float turnRateOverride_ = 0.0f; // rad/s; 0 = WOW_TURN_SPEED default (π rad/s)
|
||||||
|
// Server-driven root state: when true, block all horizontal movement input.
|
||||||
|
bool movementRooted_ = false;
|
||||||
|
// Server-driven gravity disable (levitate/hover): skip gravity accumulation.
|
||||||
|
bool gravityDisabled_ = false;
|
||||||
|
// Server-driven feather fall: cap downward velocity to slow-fall terminal.
|
||||||
|
bool featherFallActive_ = false;
|
||||||
|
// Server-driven water walk: treat water surface as ground (don't swim).
|
||||||
|
bool waterWalkActive_ = false;
|
||||||
|
// Player-controlled flight (CAN_FLY + FLYING): 3D movement, no gravity.
|
||||||
|
bool flyingActive_ = false;
|
||||||
|
// Server-driven hover (HOVER flag): float at fixed height above ground.
|
||||||
|
bool hoverActive_ = false;
|
||||||
bool mounted_ = false;
|
bool mounted_ = false;
|
||||||
float mountHeightOffset_ = 0.0f;
|
float mountHeightOffset_ = 0.0f;
|
||||||
bool externalMoving_ = false;
|
bool externalMoving_ = false;
|
||||||
|
|
@ -313,6 +355,14 @@ private:
|
||||||
float cachedFloorHeight_ = 0.0f;
|
float cachedFloorHeight_ = 0.0f;
|
||||||
bool hasCachedFloor_ = false;
|
bool hasCachedFloor_ = false;
|
||||||
static constexpr float COLLISION_CACHE_DISTANCE = 0.15f; // Re-check every 15cm
|
static constexpr float COLLISION_CACHE_DISTANCE = 0.15f; // Re-check every 15cm
|
||||||
|
|
||||||
|
// Server-driven knockback state.
|
||||||
|
// When the server sends SMSG_MOVE_KNOCK_BACK, we apply horizontal + vertical
|
||||||
|
// impulse here and let the normal physics loop (gravity, collision) resolve it.
|
||||||
|
bool knockbackActive_ = false;
|
||||||
|
glm::vec2 knockbackHorizVel_ = glm::vec2(0.0f); // render-space horizontal velocity (units/s)
|
||||||
|
// Horizontal velocity decays via WoW-like drag so the player doesn't slide forever.
|
||||||
|
static constexpr float KNOCKBACK_HORIZ_DRAG = 4.5f; // exponential decay rate (1/s)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,8 @@ public:
|
||||||
*/
|
*/
|
||||||
/** Pre-update mutable state (frame ID, material UBOs) on main thread before parallel render. */
|
/** Pre-update mutable state (frame ID, material UBOs) on main thread before parallel render. */
|
||||||
void prepareRender();
|
void prepareRender();
|
||||||
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera);
|
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera,
|
||||||
|
const glm::vec3* viewerPos = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize shadow pipeline (Phase 7)
|
* Initialize shadow pipeline (Phase 7)
|
||||||
|
|
@ -696,7 +697,7 @@ private:
|
||||||
// Rendering state
|
// Rendering state
|
||||||
bool wireframeMode = false;
|
bool wireframeMode = false;
|
||||||
bool frustumCulling = true;
|
bool frustumCulling = true;
|
||||||
bool portalCulling = false; // Disabled by default - needs debugging
|
bool portalCulling = true; // AABB transform bug fixed; conservative frustum test (no plane-side check) is visually safe
|
||||||
bool distanceCulling = false; // Disabled - causes ground to disappear
|
bool distanceCulling = false; // Disabled - causes ground to disappear
|
||||||
float maxGroupDistance = 500.0f;
|
float maxGroupDistance = 500.0f;
|
||||||
float maxGroupDistanceSq = 250000.0f; // maxGroupDistance^2
|
float maxGroupDistanceSq = 250000.0f; // maxGroupDistance^2
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,13 @@ private:
|
||||||
bool pendingSeparateBags = true;
|
bool pendingSeparateBags = true;
|
||||||
bool pendingAutoLoot = false;
|
bool pendingAutoLoot = false;
|
||||||
bool pendingUseOriginalSoundtrack = true;
|
bool pendingUseOriginalSoundtrack = true;
|
||||||
|
bool pendingShowActionBar2 = true; // Show second action bar above main bar
|
||||||
|
float pendingActionBar2OffsetX = 0.0f; // Horizontal offset from default center position
|
||||||
|
float pendingActionBar2OffsetY = 0.0f; // Vertical offset from default (above bar 1)
|
||||||
|
bool pendingShowRightBar = false; // Right-edge vertical action bar (bar 3, slots 24-35)
|
||||||
|
bool pendingShowLeftBar = false; // Left-edge vertical action bar (bar 4, slots 36-47)
|
||||||
|
float pendingRightBarOffsetY = 0.0f; // Vertical offset from screen center
|
||||||
|
float pendingLeftBarOffsetY = 0.0f; // Vertical offset from screen center
|
||||||
int pendingGroundClutterDensity = 100;
|
int pendingGroundClutterDensity = 100;
|
||||||
int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x
|
int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x
|
||||||
bool pendingNormalMapping = true; // on by default
|
bool pendingNormalMapping = true; // on by default
|
||||||
|
|
@ -232,6 +239,7 @@ private:
|
||||||
void renderDeathScreen(game::GameHandler& gameHandler);
|
void renderDeathScreen(game::GameHandler& gameHandler);
|
||||||
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
||||||
void renderResurrectDialog(game::GameHandler& gameHandler);
|
void renderResurrectDialog(game::GameHandler& gameHandler);
|
||||||
|
void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler);
|
||||||
void renderEscapeMenu();
|
void renderEscapeMenu();
|
||||||
void renderSettingsWindow();
|
void renderSettingsWindow();
|
||||||
void renderQuestMarkers(game::GameHandler& gameHandler);
|
void renderQuestMarkers(game::GameHandler& gameHandler);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ struct SpellInfo {
|
||||||
uint32_t manaCost = 0; // Mana cost
|
uint32_t manaCost = 0; // Mana cost
|
||||||
uint32_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy
|
uint32_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy
|
||||||
uint32_t rangeIndex = 0; // Range index from SpellRange.dbc
|
uint32_t rangeIndex = 0; // Range index from SpellRange.dbc
|
||||||
|
uint32_t schoolMask = 0; // School bitmask (1=phys,2=holy,4=fire,8=nature,16=frost,32=shadow,64=arcane)
|
||||||
bool isPassive() const { return (attributes & 0x40) != 0; }
|
bool isPassive() const { return (attributes & 0x40) != 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -43,6 +44,11 @@ public:
|
||||||
// Spell name lookup — triggers DBC load if needed, used by action bar tooltips
|
// Spell name lookup — triggers DBC load if needed, used by action bar tooltips
|
||||||
std::string lookupSpellName(uint32_t spellId, pipeline::AssetManager* assetManager);
|
std::string lookupSpellName(uint32_t spellId, pipeline::AssetManager* assetManager);
|
||||||
|
|
||||||
|
// Rich tooltip — renders a full spell tooltip (inside an already-open BeginTooltip block).
|
||||||
|
// Triggers DBC load if needed. Returns true if spell data was found.
|
||||||
|
bool renderSpellInfoTooltip(uint32_t spellId, game::GameHandler& gameHandler,
|
||||||
|
pipeline::AssetManager* assetManager);
|
||||||
|
|
||||||
// Drag-and-drop state for action bar assignment
|
// Drag-and-drop state for action bar assignment
|
||||||
bool isDraggingSpell() const { return draggingSpell_; }
|
bool isDraggingSpell() const { return draggingSpell_; }
|
||||||
uint32_t getDragSpellId() const { return dragSpellId_; }
|
uint32_t getDragSpellId() const { return dragSpellId_; }
|
||||||
|
|
|
||||||
|
|
@ -636,6 +636,11 @@ void Application::setState(AppState newState) {
|
||||||
renderer->triggerMeleeSwing();
|
renderer->triggerMeleeSwing();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
gameHandler->setKnockBackCallback([this](float vcos, float vsin, float hspeed, float vspeed) {
|
||||||
|
if (renderer && renderer->getCameraController()) {
|
||||||
|
renderer->getCameraController()->applyKnockBack(vcos, vsin, hspeed, vspeed);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Load quest marker models
|
// Load quest marker models
|
||||||
loadQuestMarkerModels();
|
loadQuestMarkerModels();
|
||||||
|
|
@ -750,8 +755,12 @@ void Application::logoutToLogin() {
|
||||||
creatureWeaponsAttached_.clear();
|
creatureWeaponsAttached_.clear();
|
||||||
creatureWeaponAttachAttempts_.clear();
|
creatureWeaponAttachAttempts_.clear();
|
||||||
creatureWasMoving_.clear();
|
creatureWasMoving_.clear();
|
||||||
|
creatureWasSwimming_.clear();
|
||||||
|
creatureWasFlying_.clear();
|
||||||
|
creatureWasWalking_.clear();
|
||||||
creatureSwimmingState_.clear();
|
creatureSwimmingState_.clear();
|
||||||
creatureWalkingState_.clear();
|
creatureWalkingState_.clear();
|
||||||
|
creatureFlyingState_.clear();
|
||||||
deadCreatureGuids_.clear();
|
deadCreatureGuids_.clear();
|
||||||
nonRenderableCreatureDisplayIds_.clear();
|
nonRenderableCreatureDisplayIds_.clear();
|
||||||
creaturePermanentFailureGuids_.clear();
|
creaturePermanentFailureGuids_.clear();
|
||||||
|
|
@ -1000,6 +1009,42 @@ void Application::update(float deltaTime) {
|
||||||
runInGameStage("post-update sync", [&] {
|
runInGameStage("post-update sync", [&] {
|
||||||
if (renderer && gameHandler && renderer->getCameraController()) {
|
if (renderer && gameHandler && renderer->getCameraController()) {
|
||||||
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
||||||
|
renderer->getCameraController()->setWalkSpeedOverride(gameHandler->getServerWalkSpeed());
|
||||||
|
renderer->getCameraController()->setSwimSpeedOverride(gameHandler->getServerSwimSpeed());
|
||||||
|
renderer->getCameraController()->setSwimBackSpeedOverride(gameHandler->getServerSwimBackSpeed());
|
||||||
|
renderer->getCameraController()->setFlightSpeedOverride(gameHandler->getServerFlightSpeed());
|
||||||
|
renderer->getCameraController()->setFlightBackSpeedOverride(gameHandler->getServerFlightBackSpeed());
|
||||||
|
renderer->getCameraController()->setRunBackSpeedOverride(gameHandler->getServerRunBackSpeed());
|
||||||
|
renderer->getCameraController()->setTurnRateOverride(gameHandler->getServerTurnRate());
|
||||||
|
renderer->getCameraController()->setMovementRooted(gameHandler->isPlayerRooted());
|
||||||
|
renderer->getCameraController()->setGravityDisabled(gameHandler->isGravityDisabled());
|
||||||
|
renderer->getCameraController()->setFeatherFallActive(gameHandler->isFeatherFalling());
|
||||||
|
renderer->getCameraController()->setWaterWalkActive(gameHandler->isWaterWalking());
|
||||||
|
renderer->getCameraController()->setFlyingActive(gameHandler->isPlayerFlying());
|
||||||
|
renderer->getCameraController()->setHoverActive(gameHandler->isHovering());
|
||||||
|
|
||||||
|
// Sync camera forward pitch to movement packets during flight / swimming.
|
||||||
|
// The server writes the pitch field when FLYING or SWIMMING flags are set;
|
||||||
|
// without this sync it would always be 0 (horizontal), causing other
|
||||||
|
// players to see the character flying flat even when pitching up/down.
|
||||||
|
if (gameHandler->isPlayerFlying() || gameHandler->isSwimming()) {
|
||||||
|
if (auto* cam = renderer->getCamera()) {
|
||||||
|
glm::vec3 fwd = cam->getForward();
|
||||||
|
float len = glm::length(fwd);
|
||||||
|
if (len > 1e-4f) {
|
||||||
|
float pitchRad = std::asin(std::clamp(fwd.z / len, -1.0f, 1.0f));
|
||||||
|
gameHandler->setMovementPitch(pitchRad);
|
||||||
|
// Tilt the mount/character model to match flight direction
|
||||||
|
// (taxi flight uses setTaxiOrientationCallback for this instead)
|
||||||
|
if (gameHandler->isPlayerFlying() && gameHandler->isMounted()) {
|
||||||
|
renderer->setMountPitchRoll(pitchRad, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (gameHandler->isMounted()) {
|
||||||
|
// Reset mount pitch when not flying
|
||||||
|
renderer->setMountPitchRoll(0.0f, 0.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool onTaxi = gameHandler &&
|
bool onTaxi = gameHandler &&
|
||||||
|
|
@ -1381,14 +1426,20 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
// Distance check uses getLatestX/Y/Z (server-authoritative destination) to
|
||||||
|
// avoid false-culling entities that moved while getX/Y/Z was stale.
|
||||||
|
// Position sync still uses getX/Y/Z to preserve smooth interpolation for
|
||||||
|
// nearby entities; distant entities (> 150u) have planarDist≈0 anyway
|
||||||
|
// so the renderer remains driven correctly by creatureMoveCallback_.
|
||||||
|
glm::vec3 latestCanonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||||
float canonDistSq = 0.0f;
|
float canonDistSq = 0.0f;
|
||||||
if (havePlayerPos) {
|
if (havePlayerPos) {
|
||||||
glm::vec3 d = canonical - playerPos;
|
glm::vec3 d = latestCanonical - playerPos;
|
||||||
canonDistSq = glm::dot(d, d);
|
canonDistSq = glm::dot(d, d);
|
||||||
if (canonDistSq > syncRadiusSq) continue;
|
if (canonDistSq > syncRadiusSq) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||||
|
|
||||||
// Visual collision guard: keep hostile melee units from rendering inside the
|
// Visual collision guard: keep hostile melee units from rendering inside the
|
||||||
|
|
@ -1469,13 +1520,22 @@ void Application::update(float deltaTime) {
|
||||||
auto unitPtr = std::static_pointer_cast<game::Unit>(entity);
|
auto unitPtr = std::static_pointer_cast<game::Unit>(entity);
|
||||||
const bool deadOrCorpse = unitPtr->getHealth() == 0;
|
const bool deadOrCorpse = unitPtr->getHealth() == 0;
|
||||||
const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f);
|
const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f);
|
||||||
const bool isMovingNow = !deadOrCorpse && (planarDist > 0.03f || dz > 0.08f);
|
// isEntityMoving() reflects server-authoritative move state set by
|
||||||
|
// startMoveTo() in handleMonsterMove, regardless of distance-cull.
|
||||||
|
// This correctly detects movement for distant creatures (> 150u)
|
||||||
|
// where updateMovement() is not called and getX/Y/Z() stays stale.
|
||||||
|
const bool entityIsMoving = entity->isEntityMoving();
|
||||||
|
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f);
|
||||||
if (deadOrCorpse || largeCorrection) {
|
if (deadOrCorpse || largeCorrection) {
|
||||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||||
} else if (isMovingNow) {
|
} else if (planarDist > 0.03f || dz > 0.08f) {
|
||||||
|
// Position changed in entity coords → drive renderer toward it.
|
||||||
float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f);
|
float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f);
|
||||||
charRenderer->moveInstanceTo(instanceId, renderPos, duration);
|
charRenderer->moveInstanceTo(instanceId, renderPos, duration);
|
||||||
}
|
}
|
||||||
|
// When entity is moving but getX/Y/Z is stale (distance-culled),
|
||||||
|
// don't call moveInstanceTo — creatureMoveCallback_ already drove
|
||||||
|
// the renderer to the correct destination via the spline packet.
|
||||||
posIt->second = renderPos;
|
posIt->second = renderPos;
|
||||||
|
|
||||||
// Drive movement animation: Walk/Run/Swim (4/5/42) when moving,
|
// Drive movement animation: Walk/Run/Swim (4/5/42) when moving,
|
||||||
|
|
@ -1485,17 +1545,37 @@ void Application::update(float deltaTime) {
|
||||||
// Don't override Death (1) animation.
|
// Don't override Death (1) animation.
|
||||||
const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0;
|
const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0;
|
||||||
const bool isWalkingNow = creatureWalkingState_.count(guid) > 0;
|
const bool isWalkingNow = creatureWalkingState_.count(guid) > 0;
|
||||||
bool prevMoving = creatureWasMoving_[guid];
|
const bool isFlyingNow = creatureFlyingState_.count(guid) > 0;
|
||||||
if (isMovingNow != prevMoving) {
|
bool prevMoving = creatureWasMoving_[guid];
|
||||||
creatureWasMoving_[guid] = isMovingNow;
|
bool prevSwimming = creatureWasSwimming_[guid];
|
||||||
|
bool prevFlying = creatureWasFlying_[guid];
|
||||||
|
bool prevWalking = creatureWasWalking_[guid];
|
||||||
|
// Trigger animation update on any locomotion-state transition, not just
|
||||||
|
// moving/idle — e.g. creature lands while still moving → FlyForward→Run,
|
||||||
|
// or server changes WALKING flag while creature is already running → Walk.
|
||||||
|
const bool stateChanged = (isMovingNow != prevMoving) ||
|
||||||
|
(isSwimmingNow != prevSwimming) ||
|
||||||
|
(isFlyingNow != prevFlying) ||
|
||||||
|
(isWalkingNow != prevWalking && isMovingNow);
|
||||||
|
if (stateChanged) {
|
||||||
|
creatureWasMoving_[guid] = isMovingNow;
|
||||||
|
creatureWasSwimming_[guid] = isSwimmingNow;
|
||||||
|
creatureWasFlying_[guid] = isFlyingNow;
|
||||||
|
creatureWasWalking_[guid] = isWalkingNow;
|
||||||
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||||
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
|
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||||
if (!gotState || curAnimId != 1 /*Death*/) {
|
if (!gotState || curAnimId != 1 /*Death*/) {
|
||||||
uint32_t targetAnim;
|
uint32_t targetAnim;
|
||||||
if (isMovingNow)
|
if (isMovingNow) {
|
||||||
targetAnim = isSwimmingNow ? 42u : (isWalkingNow ? 4u : 5u); // Swim/Walk/Run
|
if (isFlyingNow) targetAnim = 159u; // FlyForward
|
||||||
else
|
else if (isSwimmingNow) targetAnim = 42u; // Swim
|
||||||
targetAnim = isSwimmingNow ? 41u : 0u; // SwimIdle vs Stand
|
else if (isWalkingNow) targetAnim = 4u; // Walk
|
||||||
|
else targetAnim = 5u; // Run
|
||||||
|
} else {
|
||||||
|
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
|
||||||
|
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
|
||||||
|
else targetAnim = 0u; // Stand
|
||||||
|
}
|
||||||
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
|
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2810,10 +2890,13 @@ void Application::setupUICallbacks() {
|
||||||
gameHandler->setUnitMoveFlagsCallback([this](uint64_t guid, uint32_t moveFlags) {
|
gameHandler->setUnitMoveFlagsCallback([this](uint64_t guid, uint32_t moveFlags) {
|
||||||
const bool isSwimming = (moveFlags & static_cast<uint32_t>(game::MovementFlags::SWIMMING)) != 0;
|
const bool isSwimming = (moveFlags & static_cast<uint32_t>(game::MovementFlags::SWIMMING)) != 0;
|
||||||
const bool isWalking = (moveFlags & static_cast<uint32_t>(game::MovementFlags::WALKING)) != 0;
|
const bool isWalking = (moveFlags & static_cast<uint32_t>(game::MovementFlags::WALKING)) != 0;
|
||||||
|
const bool isFlying = (moveFlags & static_cast<uint32_t>(game::MovementFlags::FLYING)) != 0;
|
||||||
if (isSwimming) creatureSwimmingState_[guid] = true;
|
if (isSwimming) creatureSwimmingState_[guid] = true;
|
||||||
else creatureSwimmingState_.erase(guid);
|
else creatureSwimmingState_.erase(guid);
|
||||||
if (isWalking) creatureWalkingState_[guid] = true;
|
if (isWalking) creatureWalkingState_[guid] = true;
|
||||||
else creatureWalkingState_.erase(guid);
|
else creatureWalkingState_.erase(guid);
|
||||||
|
if (isFlying) creatureFlyingState_[guid] = true;
|
||||||
|
else creatureFlyingState_.erase(guid);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emote animation callback — play server-driven emote animations on NPCs and other players
|
// Emote animation callback — play server-driven emote animations on NPCs and other players
|
||||||
|
|
@ -6934,6 +7017,11 @@ void Application::despawnOnlinePlayer(uint64_t guid) {
|
||||||
pendingOnlinePlayerEquipment_.erase(guid);
|
pendingOnlinePlayerEquipment_.erase(guid);
|
||||||
creatureSwimmingState_.erase(guid);
|
creatureSwimmingState_.erase(guid);
|
||||||
creatureWalkingState_.erase(guid);
|
creatureWalkingState_.erase(guid);
|
||||||
|
creatureFlyingState_.erase(guid);
|
||||||
|
creatureWasMoving_.erase(guid);
|
||||||
|
creatureWasSwimming_.erase(guid);
|
||||||
|
creatureWasFlying_.erase(guid);
|
||||||
|
creatureWasWalking_.erase(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||||
|
|
@ -8527,8 +8615,12 @@ void Application::despawnOnlineCreature(uint64_t guid) {
|
||||||
creatureWeaponsAttached_.erase(guid);
|
creatureWeaponsAttached_.erase(guid);
|
||||||
creatureWeaponAttachAttempts_.erase(guid);
|
creatureWeaponAttachAttempts_.erase(guid);
|
||||||
creatureWasMoving_.erase(guid);
|
creatureWasMoving_.erase(guid);
|
||||||
|
creatureWasSwimming_.erase(guid);
|
||||||
|
creatureWasFlying_.erase(guid);
|
||||||
|
creatureWasWalking_.erase(guid);
|
||||||
creatureSwimmingState_.erase(guid);
|
creatureSwimmingState_.erase(guid);
|
||||||
creatureWalkingState_.erase(guid);
|
creatureWalkingState_.erase(guid);
|
||||||
|
creatureFlyingState_.erase(guid);
|
||||||
|
|
||||||
LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec);
|
LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1530,10 +1530,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
// Classic 1.12 and TBC friend list (WotLK uses SMSG_CONTACT_LIST instead)
|
// Classic 1.12 and TBC friend list (WotLK uses SMSG_CONTACT_LIST instead)
|
||||||
handleFriendList(packet);
|
handleFriendList(packet);
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_IGNORE_LIST:
|
case Opcode::SMSG_IGNORE_LIST: {
|
||||||
// Ignore list: consume to avoid spurious warnings; not parsed.
|
// uint8 count + count × (uint64 guid + string name)
|
||||||
packet.setReadPos(packet.getSize());
|
// Populate ignoreCache so /unignore works for pre-existing ignores.
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 1) break;
|
||||||
|
uint8_t ignCount = packet.readUInt8();
|
||||||
|
for (uint8_t i = 0; i < ignCount; ++i) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 8) break;
|
||||||
|
uint64_t ignGuid = packet.readUInt64();
|
||||||
|
std::string ignName = packet.readString();
|
||||||
|
if (!ignName.empty() && ignGuid != 0) {
|
||||||
|
ignoreCache[ignName] = ignGuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_DEBUG("SMSG_IGNORE_LIST: loaded ", (int)ignCount, " ignored players");
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case Opcode::MSG_RANDOM_ROLL:
|
case Opcode::MSG_RANDOM_ROLL:
|
||||||
if (state == WorldState::IN_WORLD) {
|
if (state == WorldState::IN_WORLD) {
|
||||||
|
|
@ -2389,9 +2401,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) break;
|
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||||
float speed = packet.readFloat();
|
float speed = packet.readFloat();
|
||||||
if (guid == playerGuid && std::isfinite(speed) && speed > 0.1f && speed < 100.0f &&
|
if (guid == playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f) {
|
||||||
*logicalOp == Opcode::SMSG_SPLINE_SET_RUN_SPEED) {
|
if (*logicalOp == Opcode::SMSG_SPLINE_SET_RUN_SPEED)
|
||||||
serverRunSpeed_ = speed;
|
serverRunSpeed_ = speed;
|
||||||
|
else if (*logicalOp == Opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED)
|
||||||
|
serverRunBackSpeed_ = speed;
|
||||||
|
else if (*logicalOp == Opcode::SMSG_SPLINE_SET_SWIM_SPEED)
|
||||||
|
serverSwimSpeed_ = speed;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -2443,10 +2459,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
static_cast<uint32_t>(MovementFlags::CAN_FLY), false);
|
static_cast<uint32_t>(MovementFlags::CAN_FLY), false);
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_MOVE_FEATHER_FALL:
|
case Opcode::SMSG_MOVE_FEATHER_FALL:
|
||||||
handleForceMoveFlagChange(packet, "FEATHER_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK, 0, true);
|
handleForceMoveFlagChange(packet, "FEATHER_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::FEATHER_FALL), true);
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_MOVE_WATER_WALK:
|
case Opcode::SMSG_MOVE_WATER_WALK:
|
||||||
handleForceMoveFlagChange(packet, "WATER_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK, 0, true);
|
handleForceMoveFlagChange(packet, "WATER_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::WATER_WALK), true);
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_MOVE_SET_HOVER:
|
case Opcode::SMSG_MOVE_SET_HOVER:
|
||||||
handleForceMoveFlagChange(packet, "SET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK,
|
handleForceMoveFlagChange(packet, "SET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK,
|
||||||
|
|
@ -3385,15 +3403,28 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case Opcode::SMSG_ACTION_BUTTONS: {
|
case Opcode::SMSG_ACTION_BUTTONS: {
|
||||||
// uint8 mode (0=initial, 1=update) + 144 × uint32 packed buttons
|
|
||||||
// packed: bits 0-23 = actionId, bits 24-31 = type
|
// packed: bits 0-23 = actionId, bits 24-31 = type
|
||||||
// 0x00 = spell (when id != 0), 0x80 = item, 0x40 = macro (skip)
|
// 0x00 = spell (when id != 0), 0x80 = item, 0x40 = macro (skip)
|
||||||
|
// Format differences:
|
||||||
|
// Classic 1.12: no mode byte, 120 slots (480 bytes)
|
||||||
|
// TBC 2.4.3: no mode byte, 132 slots (528 bytes)
|
||||||
|
// WotLK 3.3.5a: uint8 mode + 144 slots (577 bytes)
|
||||||
size_t rem = packet.getSize() - packet.getReadPos();
|
size_t rem = packet.getSize() - packet.getReadPos();
|
||||||
if (rem < 1) break;
|
const bool hasModeByteExp = isActiveExpansion("wotlk");
|
||||||
/*uint8_t mode =*/ packet.readUInt8();
|
int serverBarSlots;
|
||||||
rem--;
|
if (isClassicLikeExpansion()) {
|
||||||
constexpr int SERVER_BAR_SLOTS = 144;
|
serverBarSlots = 120;
|
||||||
for (int i = 0; i < SERVER_BAR_SLOTS; ++i) {
|
} else if (isActiveExpansion("tbc")) {
|
||||||
|
serverBarSlots = 132;
|
||||||
|
} else {
|
||||||
|
serverBarSlots = 144;
|
||||||
|
}
|
||||||
|
if (hasModeByteExp) {
|
||||||
|
if (rem < 1) break;
|
||||||
|
/*uint8_t mode =*/ packet.readUInt8();
|
||||||
|
rem--;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < serverBarSlots; ++i) {
|
||||||
if (rem < 4) break;
|
if (rem < 4) break;
|
||||||
uint32_t packed = packet.readUInt32();
|
uint32_t packed = packet.readUInt32();
|
||||||
rem -= 4;
|
rem -= 4;
|
||||||
|
|
@ -3585,10 +3616,29 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
uint8_t error = packet.readUInt8();
|
uint8_t error = packet.readUInt8();
|
||||||
if (error != 0) {
|
if (error != 0) {
|
||||||
LOG_WARNING("SMSG_INVENTORY_CHANGE_FAILURE: error=", (int)error);
|
LOG_WARNING("SMSG_INVENTORY_CHANGE_FAILURE: error=", (int)error);
|
||||||
|
// After error byte: item_guid1(8) + item_guid2(8) + bag_slot(1) = 17 bytes
|
||||||
|
uint32_t requiredLevel = 0;
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 17) {
|
||||||
|
packet.readUInt64(); // item_guid1
|
||||||
|
packet.readUInt64(); // item_guid2
|
||||||
|
packet.readUInt8(); // bag_slot
|
||||||
|
// Error 1 = EQUIP_ERR_LEVEL_REQ: server appends required level as uint32
|
||||||
|
if (error == 1 && packet.getSize() - packet.getReadPos() >= 4)
|
||||||
|
requiredLevel = packet.readUInt32();
|
||||||
|
}
|
||||||
// InventoryResult enum (AzerothCore 3.3.5a)
|
// InventoryResult enum (AzerothCore 3.3.5a)
|
||||||
const char* errMsg = nullptr;
|
const char* errMsg = nullptr;
|
||||||
|
char levelBuf[64];
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case 1: errMsg = "You must reach level %d to use that item."; break;
|
case 1:
|
||||||
|
if (requiredLevel > 0) {
|
||||||
|
std::snprintf(levelBuf, sizeof(levelBuf),
|
||||||
|
"You must reach level %u to use that item.", requiredLevel);
|
||||||
|
addSystemChatMessage(levelBuf);
|
||||||
|
} else {
|
||||||
|
addSystemChatMessage("You must reach a higher level to use that item.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 2: errMsg = "You don't have the required skill."; break;
|
case 2: errMsg = "You don't have the required skill."; break;
|
||||||
case 3: errMsg = "That item doesn't go in that slot."; break;
|
case 3: errMsg = "That item doesn't go in that slot."; break;
|
||||||
case 4: errMsg = "That bag is full."; break;
|
case 4: errMsg = "That bag is full."; break;
|
||||||
|
|
@ -4452,10 +4502,20 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::MSG_INSPECT_ARENA_TEAMS:
|
case Opcode::MSG_INSPECT_ARENA_TEAMS:
|
||||||
LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS");
|
LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS");
|
||||||
break;
|
break;
|
||||||
case Opcode::MSG_TALENT_WIPE_CONFIRM:
|
case Opcode::MSG_TALENT_WIPE_CONFIRM: {
|
||||||
// Talent reset confirmation payload is not needed client-side right now.
|
// Server sends: uint64 npcGuid + uint32 cost
|
||||||
packet.setReadPos(packet.getSize());
|
// Client must respond with the same opcode containing uint64 npcGuid to confirm.
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 12) {
|
||||||
|
packet.setReadPos(packet.getSize());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
talentWipeNpcGuid_ = packet.readUInt64();
|
||||||
|
talentWipeCost_ = packet.readUInt32();
|
||||||
|
talentWipePending_ = true;
|
||||||
|
LOG_INFO("MSG_TALENT_WIPE_CONFIRM: npc=0x", std::hex, talentWipeNpcGuid_,
|
||||||
|
std::dec, " cost=", talentWipeCost_);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- MSG_MOVE_* opcodes (server relays other players' movement) ----
|
// ---- MSG_MOVE_* opcodes (server relays other players' movement) ----
|
||||||
case Opcode::MSG_MOVE_START_FORWARD:
|
case Opcode::MSG_MOVE_START_FORWARD:
|
||||||
|
|
@ -4480,6 +4540,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::MSG_MOVE_STOP_PITCH:
|
case Opcode::MSG_MOVE_STOP_PITCH:
|
||||||
case Opcode::MSG_MOVE_START_ASCEND:
|
case Opcode::MSG_MOVE_START_ASCEND:
|
||||||
case Opcode::MSG_MOVE_STOP_ASCEND:
|
case Opcode::MSG_MOVE_STOP_ASCEND:
|
||||||
|
case Opcode::MSG_MOVE_START_DESCEND:
|
||||||
|
case Opcode::MSG_MOVE_SET_PITCH:
|
||||||
|
case Opcode::MSG_MOVE_GRAVITY_CHNG:
|
||||||
|
case Opcode::MSG_MOVE_UPDATE_CAN_FLY:
|
||||||
|
case Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY:
|
||||||
|
case Opcode::MSG_MOVE_ROOT:
|
||||||
|
case Opcode::MSG_MOVE_UNROOT:
|
||||||
if (state == WorldState::IN_WORLD) {
|
if (state == WorldState::IN_WORLD) {
|
||||||
handleOtherPlayerMovement(packet);
|
handleOtherPlayerMovement(packet);
|
||||||
}
|
}
|
||||||
|
|
@ -5158,17 +5225,45 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED:
|
case Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED:
|
||||||
case Opcode::SMSG_SPLINE_SET_WALK_SPEED:
|
case Opcode::SMSG_SPLINE_SET_WALK_SPEED:
|
||||||
case Opcode::SMSG_SPLINE_SET_TURN_RATE:
|
case Opcode::SMSG_SPLINE_SET_TURN_RATE:
|
||||||
case Opcode::SMSG_SPLINE_SET_PITCH_RATE:
|
case Opcode::SMSG_SPLINE_SET_PITCH_RATE: {
|
||||||
packet.setReadPos(packet.getSize());
|
// Minimal parse: PackedGuid + float speed
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 5) break;
|
||||||
|
uint64_t sGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||||
|
float sSpeed = packet.readFloat();
|
||||||
|
if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) {
|
||||||
|
if (*logicalOp == Opcode::SMSG_SPLINE_SET_FLIGHT_SPEED)
|
||||||
|
serverFlightSpeed_ = sSpeed;
|
||||||
|
else if (*logicalOp == Opcode::SMSG_SPLINE_SET_FLIGHT_BACK_SPEED)
|
||||||
|
serverFlightBackSpeed_ = sSpeed;
|
||||||
|
else if (*logicalOp == Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED)
|
||||||
|
serverSwimBackSpeed_ = sSpeed;
|
||||||
|
else if (*logicalOp == Opcode::SMSG_SPLINE_SET_WALK_SPEED)
|
||||||
|
serverWalkSpeed_ = sSpeed;
|
||||||
|
else if (*logicalOp == Opcode::SMSG_SPLINE_SET_TURN_RATE)
|
||||||
|
serverTurnRate_ = sSpeed; // rad/s
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Spline move flag changes for other units ----
|
// ---- Spline move flag changes for other units ----
|
||||||
case Opcode::SMSG_SPLINE_MOVE_UNROOT:
|
case Opcode::SMSG_SPLINE_MOVE_UNROOT:
|
||||||
case Opcode::SMSG_SPLINE_MOVE_UNSET_FLYING:
|
|
||||||
case Opcode::SMSG_SPLINE_MOVE_UNSET_HOVER:
|
case Opcode::SMSG_SPLINE_MOVE_UNSET_HOVER:
|
||||||
case Opcode::SMSG_SPLINE_MOVE_WATER_WALK:
|
case Opcode::SMSG_SPLINE_MOVE_WATER_WALK: {
|
||||||
packet.setReadPos(packet.getSize());
|
// Minimal parse: PackedGuid only — no animation-relevant state change.
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||||
|
(void)UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case Opcode::SMSG_SPLINE_MOVE_UNSET_FLYING: {
|
||||||
|
// PackedGuid + synthesised move-flags=0 → clears flying animation.
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 1) break;
|
||||||
|
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (guid == 0 || guid == playerGuid || !unitMoveFlagsCallback_) break;
|
||||||
|
unitMoveFlagsCallback_(guid, 0u); // clear flying/CAN_FLY
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Quest failure notification ----
|
// ---- Quest failure notification ----
|
||||||
case Opcode::SMSG_QUESTGIVER_QUEST_FAILED: {
|
case Opcode::SMSG_QUESTGIVER_QUEST_FAILED: {
|
||||||
|
|
@ -5533,15 +5628,39 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
|
|
||||||
// ---- Player movement flag changes (server-pushed) ----
|
// ---- Player movement flag changes (server-pushed) ----
|
||||||
case Opcode::SMSG_MOVE_GRAVITY_DISABLE:
|
case Opcode::SMSG_MOVE_GRAVITY_DISABLE:
|
||||||
|
handleForceMoveFlagChange(packet, "GRAVITY_DISABLE", Opcode::CMSG_MOVE_GRAVITY_DISABLE_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::LEVITATING), true);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_GRAVITY_ENABLE:
|
case Opcode::SMSG_MOVE_GRAVITY_ENABLE:
|
||||||
|
handleForceMoveFlagChange(packet, "GRAVITY_ENABLE", Opcode::CMSG_MOVE_GRAVITY_ENABLE_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::LEVITATING), false);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_LAND_WALK:
|
case Opcode::SMSG_MOVE_LAND_WALK:
|
||||||
|
handleForceMoveFlagChange(packet, "LAND_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::WATER_WALK), false);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_NORMAL_FALL:
|
case Opcode::SMSG_MOVE_NORMAL_FALL:
|
||||||
|
handleForceMoveFlagChange(packet, "NORMAL_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::FEATHER_FALL), false);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY:
|
case Opcode::SMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY:
|
||||||
|
handleForceMoveFlagChange(packet, "SET_CAN_TRANSITION_SWIM_FLY",
|
||||||
|
Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, true);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_UNSET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY:
|
case Opcode::SMSG_MOVE_UNSET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY:
|
||||||
|
handleForceMoveFlagChange(packet, "UNSET_CAN_TRANSITION_SWIM_FLY",
|
||||||
|
Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, false);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_SET_COLLISION_HGT:
|
case Opcode::SMSG_MOVE_SET_COLLISION_HGT:
|
||||||
|
handleMoveSetCollisionHeight(packet);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_SET_FLIGHT:
|
case Opcode::SMSG_MOVE_SET_FLIGHT:
|
||||||
|
handleForceMoveFlagChange(packet, "SET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::FLYING), true);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_MOVE_UNSET_FLIGHT:
|
case Opcode::SMSG_MOVE_UNSET_FLIGHT:
|
||||||
packet.setReadPos(packet.getSize());
|
handleForceMoveFlagChange(packet, "UNSET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK,
|
||||||
|
static_cast<uint32_t>(MovementFlags::FLYING), false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -6069,6 +6188,13 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
||||||
movementClockStart_ = std::chrono::steady_clock::now();
|
movementClockStart_ = std::chrono::steady_clock::now();
|
||||||
lastMovementTimestampMs_ = 0;
|
lastMovementTimestampMs_ = 0;
|
||||||
movementInfo.time = nextMovementTimestampMs();
|
movementInfo.time = nextMovementTimestampMs();
|
||||||
|
isFalling_ = false;
|
||||||
|
fallStartMs_ = 0;
|
||||||
|
movementInfo.fallTime = 0;
|
||||||
|
movementInfo.jumpVelocity = 0.0f;
|
||||||
|
movementInfo.jumpSinAngle = 0.0f;
|
||||||
|
movementInfo.jumpCosAngle = 0.0f;
|
||||||
|
movementInfo.jumpXYSpeed = 0.0f;
|
||||||
resurrectPending_ = false;
|
resurrectPending_ = false;
|
||||||
resurrectRequestPending_ = false;
|
resurrectRequestPending_ = false;
|
||||||
onTaxiFlight_ = false;
|
onTaxiFlight_ = false;
|
||||||
|
|
@ -7186,6 +7312,31 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
break;
|
break;
|
||||||
case Opcode::MSG_MOVE_JUMP:
|
case Opcode::MSG_MOVE_JUMP:
|
||||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FALLING);
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FALLING);
|
||||||
|
// Record fall start and capture horizontal velocity for jump fields.
|
||||||
|
isFalling_ = true;
|
||||||
|
fallStartMs_ = movementInfo.time;
|
||||||
|
movementInfo.fallTime = 0;
|
||||||
|
// jumpVelocity: WoW convention is the upward speed at launch.
|
||||||
|
movementInfo.jumpVelocity = 7.96f; // WOW_JUMP_VELOCITY from CameraController
|
||||||
|
{
|
||||||
|
// Facing direction encodes the horizontal movement direction at launch.
|
||||||
|
const float facingRad = movementInfo.orientation;
|
||||||
|
movementInfo.jumpCosAngle = std::cos(facingRad);
|
||||||
|
movementInfo.jumpSinAngle = std::sin(facingRad);
|
||||||
|
// Horizontal speed: only non-zero when actually moving at jump time.
|
||||||
|
const uint32_t horizFlags =
|
||||||
|
static_cast<uint32_t>(MovementFlags::FORWARD) |
|
||||||
|
static_cast<uint32_t>(MovementFlags::BACKWARD) |
|
||||||
|
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
||||||
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
|
||||||
|
const bool movingHoriz = (movementInfo.flags & horizFlags) != 0;
|
||||||
|
if (movingHoriz) {
|
||||||
|
const bool isWalking = (movementInfo.flags & static_cast<uint32_t>(MovementFlags::WALKING)) != 0;
|
||||||
|
movementInfo.jumpXYSpeed = isWalking ? 2.5f : (serverRunSpeed_ > 0.0f ? serverRunSpeed_ : 7.0f);
|
||||||
|
} else {
|
||||||
|
movementInfo.jumpXYSpeed = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Opcode::MSG_MOVE_START_TURN_LEFT:
|
case Opcode::MSG_MOVE_START_TURN_LEFT:
|
||||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_LEFT);
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_LEFT);
|
||||||
|
|
@ -7199,14 +7350,50 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
break;
|
break;
|
||||||
case Opcode::MSG_MOVE_FALL_LAND:
|
case Opcode::MSG_MOVE_FALL_LAND:
|
||||||
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::FALLING);
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::FALLING);
|
||||||
|
isFalling_ = false;
|
||||||
|
fallStartMs_ = 0;
|
||||||
|
movementInfo.fallTime = 0;
|
||||||
|
movementInfo.jumpVelocity = 0.0f;
|
||||||
|
movementInfo.jumpSinAngle = 0.0f;
|
||||||
|
movementInfo.jumpCosAngle = 0.0f;
|
||||||
|
movementInfo.jumpXYSpeed = 0.0f;
|
||||||
break;
|
break;
|
||||||
case Opcode::MSG_MOVE_HEARTBEAT:
|
case Opcode::MSG_MOVE_HEARTBEAT:
|
||||||
// No flag changes — just sends current position
|
// No flag changes — just sends current position
|
||||||
break;
|
break;
|
||||||
|
case Opcode::MSG_MOVE_START_ASCEND:
|
||||||
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ASCENDING);
|
||||||
|
break;
|
||||||
|
case Opcode::MSG_MOVE_STOP_ASCEND:
|
||||||
|
// Clears ascending (and descending) — one stop opcode for both directions
|
||||||
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ASCENDING);
|
||||||
|
break;
|
||||||
|
case Opcode::MSG_MOVE_START_DESCEND:
|
||||||
|
// Descending: no separate flag; clear ASCENDING so they don't conflict
|
||||||
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ASCENDING);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep fallTime current: it must equal the elapsed milliseconds since FALLING
|
||||||
|
// was set, so the server can compute fall damage correctly.
|
||||||
|
if (isFalling_ && movementInfo.hasFlag(MovementFlags::FALLING)) {
|
||||||
|
// movementInfo.time is the strictly-increasing client clock (ms).
|
||||||
|
// Subtract fallStartMs_ to get elapsed fall time; clamp to non-negative.
|
||||||
|
uint32_t elapsed = (movementInfo.time >= fallStartMs_)
|
||||||
|
? (movementInfo.time - fallStartMs_)
|
||||||
|
: 0u;
|
||||||
|
movementInfo.fallTime = elapsed;
|
||||||
|
} else if (!movementInfo.hasFlag(MovementFlags::FALLING)) {
|
||||||
|
// Ensure fallTime is zeroed whenever we're not falling.
|
||||||
|
if (isFalling_) {
|
||||||
|
isFalling_ = false;
|
||||||
|
fallStartMs_ = 0;
|
||||||
|
}
|
||||||
|
movementInfo.fallTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (onTaxiFlight_ || taxiMountActive_ || taxiActivatePending_ || taxiClientActive_) {
|
if (onTaxiFlight_ || taxiMountActive_ || taxiActivatePending_ || taxiClientActive_) {
|
||||||
sanitizeMovementForTaxi();
|
sanitizeMovementForTaxi();
|
||||||
}
|
}
|
||||||
|
|
@ -7775,10 +7962,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (ghostStateCallback_) ghostStateCallback_(true);
|
if (ghostStateCallback_) ghostStateCallback_(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Determine hostility from faction template for online creatures
|
// Determine hostility from faction template for online creatures.
|
||||||
if (unit->getFactionTemplate() != 0) {
|
// Always call isHostileFaction — factionTemplate=0 defaults to hostile
|
||||||
unit->setHostile(isHostileFaction(unit->getFactionTemplate()));
|
// in the lookup rather than silently staying at the struct default (false).
|
||||||
}
|
unit->setHostile(isHostileFaction(unit->getFactionTemplate()));
|
||||||
// Trigger creature spawn callback for units/players with displayId
|
// Trigger creature spawn callback for units/players with displayId
|
||||||
if (block.objectType == ObjectType::UNIT && unit->getDisplayId() == 0) {
|
if (block.objectType == ObjectType::UNIT && unit->getDisplayId() == 0) {
|
||||||
LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec,
|
LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec,
|
||||||
|
|
@ -7868,10 +8055,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (block.objectType == ObjectType::ITEM || block.objectType == ObjectType::CONTAINER) {
|
if (block.objectType == ObjectType::ITEM || block.objectType == ObjectType::CONTAINER) {
|
||||||
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||||
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
|
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
|
||||||
|
auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY));
|
||||||
|
auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY));
|
||||||
if (entryIt != block.fields.end() && entryIt->second != 0) {
|
if (entryIt != block.fields.end() && entryIt->second != 0) {
|
||||||
OnlineItemInfo info;
|
// Preserve existing info when doing partial updates
|
||||||
|
OnlineItemInfo info = onlineItems_.count(block.guid)
|
||||||
|
? onlineItems_[block.guid] : OnlineItemInfo{};
|
||||||
info.entry = entryIt->second;
|
info.entry = entryIt->second;
|
||||||
info.stackCount = (stackIt != block.fields.end()) ? stackIt->second : 1;
|
if (stackIt != block.fields.end()) info.stackCount = stackIt->second;
|
||||||
|
if (durIt != block.fields.end()) info.curDurability = durIt->second;
|
||||||
|
if (maxDurIt!= block.fields.end()) info.maxDurability = maxDurIt->second;
|
||||||
bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end());
|
bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end());
|
||||||
onlineItems_[block.guid] = info;
|
onlineItems_[block.guid] = info;
|
||||||
if (isNew) newItemCreated = true;
|
if (isNew) newItemCreated = true;
|
||||||
|
|
@ -7931,6 +8124,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec,
|
LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec,
|
||||||
" bankBagSlots=", static_cast<int>(bankBagSlots));
|
" bankBagSlots=", static_cast<int>(bankBagSlots));
|
||||||
inventory.setPurchasedBankBagSlots(bankBagSlots);
|
inventory.setPurchasedBankBagSlots(bankBagSlots);
|
||||||
|
// Byte 3 (bits 24-31): REST_STATE — bit 0 set means in inn/city
|
||||||
|
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||||
|
isResting_ = (restStateByte & 0x01) != 0;
|
||||||
}
|
}
|
||||||
// Do not synthesize quest-log entries from raw update-field slots.
|
// Do not synthesize quest-log entries from raw update-field slots.
|
||||||
// Slot layouts differ on some classic-family realms and can produce
|
// Slot layouts differ on some classic-family realms and can produce
|
||||||
|
|
@ -8199,6 +8395,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
bool slotsChanged = false;
|
bool slotsChanged = false;
|
||||||
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
|
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
|
||||||
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
|
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
|
||||||
|
const uint16_t ufPlayerRestedXpV = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE);
|
||||||
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||||
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
|
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
|
||||||
|
|
@ -8213,6 +8410,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
playerNextLevelXp_ = val;
|
playerNextLevelXp_ = val;
|
||||||
LOG_DEBUG("Next level XP updated: ", val);
|
LOG_DEBUG("Next level XP updated: ", val);
|
||||||
}
|
}
|
||||||
|
else if (ufPlayerRestedXpV != 0xFFFF && key == ufPlayerRestedXpV) {
|
||||||
|
playerRestedXp_ = val;
|
||||||
|
}
|
||||||
else if (key == ufPlayerLevel) {
|
else if (key == ufPlayerLevel) {
|
||||||
serverPlayerLevel_ = val;
|
serverPlayerLevel_ = val;
|
||||||
LOG_DEBUG("Level updated: ", val);
|
LOG_DEBUG("Level updated: ", val);
|
||||||
|
|
@ -8235,6 +8435,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
|
LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
|
||||||
" bankBagSlots=", static_cast<int>(bankBagSlots));
|
" bankBagSlots=", static_cast<int>(bankBagSlots));
|
||||||
inventory.setPurchasedBankBagSlots(bankBagSlots);
|
inventory.setPurchasedBankBagSlots(bankBagSlots);
|
||||||
|
// Byte 3 (bits 24-31): REST_STATE — bit 0 set means in inn/city
|
||||||
|
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||||
|
isResting_ = (restStateByte & 0x01) != 0;
|
||||||
}
|
}
|
||||||
else if (key == ufPlayerFlags) {
|
else if (key == ufPlayerFlags) {
|
||||||
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
|
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
|
||||||
|
|
@ -8262,19 +8465,31 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
extractExploredZoneFields(lastPlayerFields_);
|
extractExploredZoneFields(lastPlayerFields_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update item stack count for online items
|
// Update item stack count / durability for online items
|
||||||
if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) {
|
if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) {
|
||||||
bool inventoryChanged = false;
|
bool inventoryChanged = false;
|
||||||
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
|
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
|
||||||
|
const uint16_t itemDurField = fieldIndex(UF::ITEM_FIELD_DURABILITY);
|
||||||
|
const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY);
|
||||||
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
|
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
|
||||||
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
|
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
|
||||||
for (const auto& [key, val] : block.fields) {
|
for (const auto& [key, val] : block.fields) {
|
||||||
|
auto it = onlineItems_.find(block.guid);
|
||||||
if (key == itemStackField) {
|
if (key == itemStackField) {
|
||||||
auto it = onlineItems_.find(block.guid);
|
|
||||||
if (it != onlineItems_.end() && it->second.stackCount != val) {
|
if (it != onlineItems_.end() && it->second.stackCount != val) {
|
||||||
it->second.stackCount = val;
|
it->second.stackCount = val;
|
||||||
inventoryChanged = true;
|
inventoryChanged = true;
|
||||||
}
|
}
|
||||||
|
} else if (key == itemDurField) {
|
||||||
|
if (it != onlineItems_.end() && it->second.curDurability != val) {
|
||||||
|
it->second.curDurability = val;
|
||||||
|
inventoryChanged = true;
|
||||||
|
}
|
||||||
|
} else if (key == itemMaxDurField) {
|
||||||
|
if (it != onlineItems_.end() && it->second.maxDurability != val) {
|
||||||
|
it->second.maxDurability = val;
|
||||||
|
inventoryChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update container slot GUIDs on bag content changes
|
// Update container slot GUIDs on bag content changes
|
||||||
|
|
@ -10554,6 +10769,8 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
ItemDef def;
|
ItemDef def;
|
||||||
def.itemId = itemIt->second.entry;
|
def.itemId = itemIt->second.entry;
|
||||||
def.stackCount = itemIt->second.stackCount;
|
def.stackCount = itemIt->second.stackCount;
|
||||||
|
def.curDurability = itemIt->second.curDurability;
|
||||||
|
def.maxDurability = itemIt->second.maxDurability;
|
||||||
def.maxStack = 1;
|
def.maxStack = 1;
|
||||||
|
|
||||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||||
|
|
@ -10573,6 +10790,15 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
def.agility = infoIt->second.agility;
|
def.agility = infoIt->second.agility;
|
||||||
def.intellect = infoIt->second.intellect;
|
def.intellect = infoIt->second.intellect;
|
||||||
def.spirit = infoIt->second.spirit;
|
def.spirit = infoIt->second.spirit;
|
||||||
|
def.sellPrice = infoIt->second.sellPrice;
|
||||||
|
def.itemLevel = infoIt->second.itemLevel;
|
||||||
|
def.requiredLevel = infoIt->second.requiredLevel;
|
||||||
|
def.bindType = infoIt->second.bindType;
|
||||||
|
def.description = infoIt->second.description;
|
||||||
|
def.startQuestId = infoIt->second.startQuestId;
|
||||||
|
def.extraStats.clear();
|
||||||
|
for (const auto& es : infoIt->second.extraStats)
|
||||||
|
def.extraStats.push_back({es.statType, es.statValue});
|
||||||
} else {
|
} else {
|
||||||
def.name = "Item " + std::to_string(def.itemId);
|
def.name = "Item " + std::to_string(def.itemId);
|
||||||
queryItemInfo(def.itemId, guid);
|
queryItemInfo(def.itemId, guid);
|
||||||
|
|
@ -10592,6 +10818,8 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
ItemDef def;
|
ItemDef def;
|
||||||
def.itemId = itemIt->second.entry;
|
def.itemId = itemIt->second.entry;
|
||||||
def.stackCount = itemIt->second.stackCount;
|
def.stackCount = itemIt->second.stackCount;
|
||||||
|
def.curDurability = itemIt->second.curDurability;
|
||||||
|
def.maxDurability = itemIt->second.maxDurability;
|
||||||
def.maxStack = 1;
|
def.maxStack = 1;
|
||||||
|
|
||||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||||
|
|
@ -10611,6 +10839,15 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
def.agility = infoIt->second.agility;
|
def.agility = infoIt->second.agility;
|
||||||
def.intellect = infoIt->second.intellect;
|
def.intellect = infoIt->second.intellect;
|
||||||
def.spirit = infoIt->second.spirit;
|
def.spirit = infoIt->second.spirit;
|
||||||
|
def.sellPrice = infoIt->second.sellPrice;
|
||||||
|
def.itemLevel = infoIt->second.itemLevel;
|
||||||
|
def.requiredLevel = infoIt->second.requiredLevel;
|
||||||
|
def.bindType = infoIt->second.bindType;
|
||||||
|
def.description = infoIt->second.description;
|
||||||
|
def.startQuestId = infoIt->second.startQuestId;
|
||||||
|
def.extraStats.clear();
|
||||||
|
for (const auto& es : infoIt->second.extraStats)
|
||||||
|
def.extraStats.push_back({es.statType, es.statValue});
|
||||||
} else {
|
} else {
|
||||||
def.name = "Item " + std::to_string(def.itemId);
|
def.name = "Item " + std::to_string(def.itemId);
|
||||||
queryItemInfo(def.itemId, guid);
|
queryItemInfo(def.itemId, guid);
|
||||||
|
|
@ -10665,6 +10902,8 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
ItemDef def;
|
ItemDef def;
|
||||||
def.itemId = itemIt->second.entry;
|
def.itemId = itemIt->second.entry;
|
||||||
def.stackCount = itemIt->second.stackCount;
|
def.stackCount = itemIt->second.stackCount;
|
||||||
|
def.curDurability = itemIt->second.curDurability;
|
||||||
|
def.maxDurability = itemIt->second.maxDurability;
|
||||||
def.maxStack = 1;
|
def.maxStack = 1;
|
||||||
|
|
||||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||||
|
|
@ -10684,6 +10923,15 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
def.agility = infoIt->second.agility;
|
def.agility = infoIt->second.agility;
|
||||||
def.intellect = infoIt->second.intellect;
|
def.intellect = infoIt->second.intellect;
|
||||||
def.spirit = infoIt->second.spirit;
|
def.spirit = infoIt->second.spirit;
|
||||||
|
def.sellPrice = infoIt->second.sellPrice;
|
||||||
|
def.itemLevel = infoIt->second.itemLevel;
|
||||||
|
def.requiredLevel = infoIt->second.requiredLevel;
|
||||||
|
def.bindType = infoIt->second.bindType;
|
||||||
|
def.description = infoIt->second.description;
|
||||||
|
def.startQuestId = infoIt->second.startQuestId;
|
||||||
|
def.extraStats.clear();
|
||||||
|
for (const auto& es : infoIt->second.extraStats)
|
||||||
|
def.extraStats.push_back({es.statType, es.statValue});
|
||||||
def.bagSlots = infoIt->second.containerSlots;
|
def.bagSlots = infoIt->second.containerSlots;
|
||||||
} else {
|
} else {
|
||||||
def.name = "Item " + std::to_string(def.itemId);
|
def.name = "Item " + std::to_string(def.itemId);
|
||||||
|
|
@ -10705,6 +10953,8 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
ItemDef def;
|
ItemDef def;
|
||||||
def.itemId = itemIt->second.entry;
|
def.itemId = itemIt->second.entry;
|
||||||
def.stackCount = itemIt->second.stackCount;
|
def.stackCount = itemIt->second.stackCount;
|
||||||
|
def.curDurability = itemIt->second.curDurability;
|
||||||
|
def.maxDurability = itemIt->second.maxDurability;
|
||||||
def.maxStack = 1;
|
def.maxStack = 1;
|
||||||
|
|
||||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||||
|
|
@ -10724,6 +10974,14 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
def.agility = infoIt->second.agility;
|
def.agility = infoIt->second.agility;
|
||||||
def.intellect = infoIt->second.intellect;
|
def.intellect = infoIt->second.intellect;
|
||||||
def.spirit = infoIt->second.spirit;
|
def.spirit = infoIt->second.spirit;
|
||||||
|
def.itemLevel = infoIt->second.itemLevel;
|
||||||
|
def.requiredLevel = infoIt->second.requiredLevel;
|
||||||
|
def.bindType = infoIt->second.bindType;
|
||||||
|
def.description = infoIt->second.description;
|
||||||
|
def.startQuestId = infoIt->second.startQuestId;
|
||||||
|
def.extraStats.clear();
|
||||||
|
for (const auto& es : infoIt->second.extraStats)
|
||||||
|
def.extraStats.push_back({es.statType, es.statValue});
|
||||||
def.sellPrice = infoIt->second.sellPrice;
|
def.sellPrice = infoIt->second.sellPrice;
|
||||||
def.bagSlots = infoIt->second.containerSlots;
|
def.bagSlots = infoIt->second.containerSlots;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -10786,6 +11044,8 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
ItemDef def;
|
ItemDef def;
|
||||||
def.itemId = itemIt->second.entry;
|
def.itemId = itemIt->second.entry;
|
||||||
def.stackCount = itemIt->second.stackCount;
|
def.stackCount = itemIt->second.stackCount;
|
||||||
|
def.curDurability = itemIt->second.curDurability;
|
||||||
|
def.maxDurability = itemIt->second.maxDurability;
|
||||||
def.maxStack = 1;
|
def.maxStack = 1;
|
||||||
|
|
||||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||||
|
|
@ -10805,7 +11065,15 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
def.agility = infoIt->second.agility;
|
def.agility = infoIt->second.agility;
|
||||||
def.intellect = infoIt->second.intellect;
|
def.intellect = infoIt->second.intellect;
|
||||||
def.spirit = infoIt->second.spirit;
|
def.spirit = infoIt->second.spirit;
|
||||||
|
def.itemLevel = infoIt->second.itemLevel;
|
||||||
|
def.requiredLevel = infoIt->second.requiredLevel;
|
||||||
def.sellPrice = infoIt->second.sellPrice;
|
def.sellPrice = infoIt->second.sellPrice;
|
||||||
|
def.bindType = infoIt->second.bindType;
|
||||||
|
def.description = infoIt->second.description;
|
||||||
|
def.startQuestId = infoIt->second.startQuestId;
|
||||||
|
def.extraStats.clear();
|
||||||
|
for (const auto& es : infoIt->second.extraStats)
|
||||||
|
def.extraStats.push_back({es.statType, es.statValue});
|
||||||
def.bagSlots = infoIt->second.containerSlots;
|
def.bagSlots = infoIt->second.containerSlots;
|
||||||
} else {
|
} else {
|
||||||
def.name = "Item " + std::to_string(def.itemId);
|
def.name = "Item " + std::to_string(def.itemId);
|
||||||
|
|
@ -11398,6 +11666,47 @@ void GameHandler::handleForceMoveFlagChange(network::Packet& packet, const char*
|
||||||
socket->send(ack);
|
socket->send(ack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::handleMoveSetCollisionHeight(network::Packet& packet) {
|
||||||
|
// SMSG_MOVE_SET_COLLISION_HGT: packed guid + counter + float (height)
|
||||||
|
// ACK: CMSG_MOVE_SET_COLLISION_HGT_ACK = packed guid + counter + movement block + float (height)
|
||||||
|
const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||||
|
if (packet.getSize() - packet.getReadPos() < (legacyGuid ? 8u : 2u)) return;
|
||||||
|
uint64_t guid = legacyGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 8) return; // counter(4) + height(4)
|
||||||
|
uint32_t counter = packet.readUInt32();
|
||||||
|
float height = packet.readFloat();
|
||||||
|
|
||||||
|
LOG_INFO("SMSG_MOVE_SET_COLLISION_HGT: guid=0x", std::hex, guid, std::dec,
|
||||||
|
" counter=", counter, " height=", height);
|
||||||
|
|
||||||
|
if (guid != playerGuid) return;
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK);
|
||||||
|
if (ackWire == 0xFFFF) return;
|
||||||
|
|
||||||
|
network::Packet ack(ackWire);
|
||||||
|
const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
|
||||||
|
if (legacyGuidAck) {
|
||||||
|
ack.writeUInt64(playerGuid);
|
||||||
|
} else {
|
||||||
|
MovementPacket::writePackedGuid(ack, playerGuid);
|
||||||
|
}
|
||||||
|
ack.writeUInt32(counter);
|
||||||
|
|
||||||
|
MovementInfo wire = movementInfo;
|
||||||
|
wire.time = nextMovementTimestampMs();
|
||||||
|
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z));
|
||||||
|
wire.x = serverPos.x;
|
||||||
|
wire.y = serverPos.y;
|
||||||
|
wire.z = serverPos.z;
|
||||||
|
if (packetParsers_) packetParsers_->writeMovementPayload(ack, wire);
|
||||||
|
else MovementPacket::writeMovementPayload(ack, wire);
|
||||||
|
ack.writeFloat(height);
|
||||||
|
|
||||||
|
socket->send(ack);
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::handleMoveKnockBack(network::Packet& packet) {
|
void GameHandler::handleMoveKnockBack(network::Packet& packet) {
|
||||||
// WotLK: packed GUID; TBC/Classic: full uint64
|
// WotLK: packed GUID; TBC/Classic: full uint64
|
||||||
const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||||
|
|
@ -11406,16 +11715,23 @@ void GameHandler::handleMoveKnockBack(network::Packet& packet) {
|
||||||
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
|
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
|
||||||
if (packet.getSize() - packet.getReadPos() < 20) return; // counter(4) + vcos(4) + vsin(4) + hspeed(4) + vspeed(4)
|
if (packet.getSize() - packet.getReadPos() < 20) return; // counter(4) + vcos(4) + vsin(4) + hspeed(4) + vspeed(4)
|
||||||
uint32_t counter = packet.readUInt32();
|
uint32_t counter = packet.readUInt32();
|
||||||
[[maybe_unused]] float vcos = packet.readFloat();
|
float vcos = packet.readFloat();
|
||||||
[[maybe_unused]] float vsin = packet.readFloat();
|
float vsin = packet.readFloat();
|
||||||
[[maybe_unused]] float hspeed = packet.readFloat();
|
float hspeed = packet.readFloat();
|
||||||
[[maybe_unused]] float vspeed = packet.readFloat();
|
float vspeed = packet.readFloat();
|
||||||
|
|
||||||
LOG_INFO("SMSG_MOVE_KNOCK_BACK: guid=0x", std::hex, guid, std::dec,
|
LOG_INFO("SMSG_MOVE_KNOCK_BACK: guid=0x", std::hex, guid, std::dec,
|
||||||
" counter=", counter, " hspeed=", hspeed, " vspeed=", vspeed);
|
" counter=", counter, " vcos=", vcos, " vsin=", vsin,
|
||||||
|
" hspeed=", hspeed, " vspeed=", vspeed);
|
||||||
|
|
||||||
if (guid != playerGuid) return;
|
if (guid != playerGuid) return;
|
||||||
|
|
||||||
|
// Apply knockback physics locally so the player visually flies through the air.
|
||||||
|
// The callback forwards to CameraController::applyKnockBack().
|
||||||
|
if (knockBackCallback_) {
|
||||||
|
knockBackCallback_(vcos, vsin, hspeed, vspeed);
|
||||||
|
}
|
||||||
|
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK);
|
uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK);
|
||||||
if (ackWire == 0xFFFF) return;
|
if (ackWire == 0xFFFF) return;
|
||||||
|
|
@ -11867,13 +12183,18 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) {
|
||||||
(void)myVote; (void)totalVotes; (void)bootVotes; (void)timeLeft; (void)votesNeeded;
|
(void)myVote; (void)totalVotes; (void)bootVotes; (void)timeLeft; (void)votesNeeded;
|
||||||
|
|
||||||
if (inProgress) {
|
if (inProgress) {
|
||||||
|
lfgState_ = LfgState::Boot;
|
||||||
addSystemChatMessage(
|
addSystemChatMessage(
|
||||||
std::string("Dungeon Finder: Vote to kick in progress (") +
|
std::string("Dungeon Finder: Vote to kick in progress (") +
|
||||||
std::to_string(timeLeft) + "s remaining).");
|
std::to_string(timeLeft) + "s remaining).");
|
||||||
} else if (myAnswer) {
|
|
||||||
addSystemChatMessage("Dungeon Finder: Vote kick passed — member removed.");
|
|
||||||
} else {
|
} else {
|
||||||
addSystemChatMessage("Dungeon Finder: Vote kick failed.");
|
// Boot vote ended — return to InDungeon state regardless of outcome
|
||||||
|
lfgState_ = LfgState::InDungeon;
|
||||||
|
if (myAnswer) {
|
||||||
|
addSystemChatMessage("Dungeon Finder: Vote kick passed — member removed.");
|
||||||
|
} else {
|
||||||
|
addSystemChatMessage("Dungeon Finder: Vote kick failed.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("SMSG_LFG_BOOT_PROPOSAL_UPDATE: inProgress=", inProgress,
|
LOG_INFO("SMSG_LFG_BOOT_PROPOSAL_UPDATE: inProgress=", inProgress,
|
||||||
|
|
@ -11942,6 +12263,18 @@ void GameHandler::lfgTeleport(bool toLfgDungeon) {
|
||||||
LOG_INFO("Sent CMSG_LFG_TELEPORT: toLfgDungeon=", toLfgDungeon);
|
LOG_INFO("Sent CMSG_LFG_TELEPORT: toLfgDungeon=", toLfgDungeon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::lfgSetBootVote(bool vote) {
|
||||||
|
if (!socket) return;
|
||||||
|
uint16_t wireOp = wireOpcode(Opcode::CMSG_LFG_SET_BOOT_VOTE);
|
||||||
|
if (wireOp == 0xFFFF) return;
|
||||||
|
|
||||||
|
network::Packet pkt(wireOp);
|
||||||
|
pkt.writeUInt8(vote ? 1 : 0);
|
||||||
|
|
||||||
|
socket->send(pkt);
|
||||||
|
LOG_INFO("Sent CMSG_LFG_SET_BOOT_VOTE: vote=", vote);
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::loadAreaTriggerDbc() {
|
void GameHandler::loadAreaTriggerDbc() {
|
||||||
if (areaTriggerDbcLoaded_) return;
|
if (areaTriggerDbcLoaded_) return;
|
||||||
areaTriggerDbcLoaded_ = true;
|
areaTriggerDbcLoaded_ = true;
|
||||||
|
|
@ -12245,7 +12578,7 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) {
|
||||||
|
|
||||||
// Player movement sub-opcodes (SMSG_MULTIPLE_MOVES carries MSG_MOVE_*)
|
// Player movement sub-opcodes (SMSG_MULTIPLE_MOVES carries MSG_MOVE_*)
|
||||||
// Not static — wireOpcode() depends on runtime active opcode table.
|
// Not static — wireOpcode() depends on runtime active opcode table.
|
||||||
const std::array<uint16_t, 22> kMoveOpcodes = {
|
const std::array<uint16_t, 29> kMoveOpcodes = {
|
||||||
wireOpcode(Opcode::MSG_MOVE_START_FORWARD),
|
wireOpcode(Opcode::MSG_MOVE_START_FORWARD),
|
||||||
wireOpcode(Opcode::MSG_MOVE_START_BACKWARD),
|
wireOpcode(Opcode::MSG_MOVE_START_BACKWARD),
|
||||||
wireOpcode(Opcode::MSG_MOVE_STOP),
|
wireOpcode(Opcode::MSG_MOVE_STOP),
|
||||||
|
|
@ -12268,6 +12601,13 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) {
|
||||||
wireOpcode(Opcode::MSG_MOVE_STOP_PITCH),
|
wireOpcode(Opcode::MSG_MOVE_STOP_PITCH),
|
||||||
wireOpcode(Opcode::MSG_MOVE_START_ASCEND),
|
wireOpcode(Opcode::MSG_MOVE_START_ASCEND),
|
||||||
wireOpcode(Opcode::MSG_MOVE_STOP_ASCEND),
|
wireOpcode(Opcode::MSG_MOVE_STOP_ASCEND),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_START_DESCEND),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_SET_PITCH),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_GRAVITY_CHNG),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_UPDATE_CAN_FLY),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_ROOT),
|
||||||
|
wireOpcode(Opcode::MSG_MOVE_UNROOT),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track unhandled sub-opcodes once per compressed packet (avoid log spam)
|
// Track unhandled sub-opcodes once per compressed packet (avoid log spam)
|
||||||
|
|
@ -13160,20 +13500,35 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
||||||
SpellCooldownData data;
|
// Classic 1.12: guid(8) + N×[spellId(4) + itemId(4) + cooldown(4)] — no flags byte, 12 bytes/entry
|
||||||
if (!SpellCooldownParser::parse(packet, data)) return;
|
// TBC 2.4.3 / WotLK 3.3.5a: guid(8) + flags(1) + N×[spellId(4) + cooldown(4)] — 8 bytes/entry
|
||||||
|
const bool isClassicFormat = isClassicLikeExpansion();
|
||||||
|
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 8) return;
|
||||||
|
/*data.guid =*/ packet.readUInt64(); // guid (not used further)
|
||||||
|
|
||||||
|
if (!isClassicFormat) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 1) return;
|
||||||
|
/*data.flags =*/ packet.readUInt8(); // flags (consumed but not stored)
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t entrySize = isClassicFormat ? 12u : 8u;
|
||||||
|
while (packet.getSize() - packet.getReadPos() >= entrySize) {
|
||||||
|
uint32_t spellId = packet.readUInt32();
|
||||||
|
if (isClassicFormat) packet.readUInt32(); // itemId — consumed, not used
|
||||||
|
uint32_t cooldownMs = packet.readUInt32();
|
||||||
|
|
||||||
for (const auto& [spellId, cooldownMs] : data.cooldowns) {
|
|
||||||
float seconds = cooldownMs / 1000.0f;
|
float seconds = cooldownMs / 1000.0f;
|
||||||
spellCooldowns[spellId] = seconds;
|
spellCooldowns[spellId] = seconds;
|
||||||
// Update action bar cooldowns
|
|
||||||
for (auto& slot : actionBar) {
|
for (auto& slot : actionBar) {
|
||||||
if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) {
|
if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) {
|
||||||
slot.cooldownTotal = seconds;
|
slot.cooldownTotal = seconds;
|
||||||
slot.cooldownRemaining = seconds;
|
slot.cooldownRemaining = seconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LOG_DEBUG("handleSpellCooldown: parsed for ",
|
||||||
|
isClassicFormat ? "Classic" : "TBC/WotLK", " format");
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleCooldownEvent(network::Packet& packet) {
|
void GameHandler::handleCooldownEvent(network::Packet& packet) {
|
||||||
|
|
@ -13395,6 +13750,24 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) {
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::confirmTalentWipe() {
|
||||||
|
if (!talentWipePending_) return;
|
||||||
|
talentWipePending_ = false;
|
||||||
|
|
||||||
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
|
|
||||||
|
// Respond to MSG_TALENT_WIPE_CONFIRM with the trainer GUID to trigger the reset.
|
||||||
|
// Packet: opcode(2) + uint64 npcGuid = 10 bytes.
|
||||||
|
network::Packet pkt(wireOpcode(Opcode::MSG_TALENT_WIPE_CONFIRM));
|
||||||
|
pkt.writeUInt64(talentWipeNpcGuid_);
|
||||||
|
socket->send(pkt);
|
||||||
|
|
||||||
|
LOG_INFO("confirmTalentWipe: sent confirm for npc=0x", std::hex, talentWipeNpcGuid_, std::dec);
|
||||||
|
addSystemChatMessage("Talent reset confirmed. The server will update your talents.");
|
||||||
|
talentWipeNpcGuid_ = 0;
|
||||||
|
talentWipeCost_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Phase 4: Group/Party
|
// Phase 4: Group/Party
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -14042,8 +14415,9 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
// animation/sound and expects the client to request the mail list.
|
// animation/sound and expects the client to request the mail list.
|
||||||
bool isMailbox = false;
|
bool isMailbox = false;
|
||||||
bool chestLike = false;
|
bool chestLike = false;
|
||||||
// Stock-like behavior: GO use opens GO loot context. Keep eager CMSG_LOOT only
|
// Chest-type game objects (type=3): on all expansions, also send CMSG_LOOT so
|
||||||
// as Classic/Turtle fallback behavior.
|
// the server opens the loot response. Other harvestable/interactive types rely
|
||||||
|
// on the server auto-sending SMSG_LOOT_RESPONSE after CMSG_GAMEOBJ_USE.
|
||||||
bool shouldSendLoot = isActiveExpansion("classic") || isActiveExpansion("turtle");
|
bool shouldSendLoot = isActiveExpansion("classic") || isActiveExpansion("turtle");
|
||||||
if (entity && entity->getType() == ObjectType::GAMEOBJECT) {
|
if (entity && entity->getType() == ObjectType::GAMEOBJECT) {
|
||||||
auto go = std::static_pointer_cast<GameObject>(entity);
|
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||||
|
|
@ -14060,6 +14434,8 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
refreshMailList();
|
refreshMailList();
|
||||||
} else if (info && info->type == 3) {
|
} else if (info && info->type == 3) {
|
||||||
chestLike = true;
|
chestLike = true;
|
||||||
|
// Type-3 chests require CMSG_LOOT on all expansions (AzerothCore WotLK included)
|
||||||
|
shouldSendLoot = true;
|
||||||
} else if (turtleMode) {
|
} else if (turtleMode) {
|
||||||
// Turtle compatibility: keep eager loot open behavior.
|
// Turtle compatibility: keep eager loot open behavior.
|
||||||
shouldSendLoot = true;
|
shouldSendLoot = true;
|
||||||
|
|
@ -14070,21 +14446,19 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
chestLike = (lower.find("chest") != std::string::npos);
|
chestLike = (lower.find("chest") != std::string::npos);
|
||||||
|
if (chestLike) shouldSendLoot = true;
|
||||||
}
|
}
|
||||||
// For WotLK chest-like gameobjects, report use but let server open loot.
|
// For WotLK chest-like gameobjects, also send CMSG_GAMEOBJ_REPORT_USE.
|
||||||
if (!isMailbox && chestLike) {
|
if (!isMailbox && chestLike && isActiveExpansion("wotlk")) {
|
||||||
if (isActiveExpansion("wotlk")) {
|
network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE));
|
||||||
network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE));
|
reportUse.writeUInt64(guid);
|
||||||
reportUse.writeUInt64(guid);
|
socket->send(reportUse);
|
||||||
socket->send(reportUse);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (shouldSendLoot) {
|
if (shouldSendLoot) {
|
||||||
lootTarget(guid);
|
lootTarget(guid);
|
||||||
}
|
}
|
||||||
// Retry use briefly to survive packet loss/order races. Keep loot retries only
|
// Retry use briefly to survive packet loss/order races.
|
||||||
// when we intentionally use eager loot-open mode.
|
const bool retryLoot = shouldSendLoot;
|
||||||
const bool retryLoot = shouldSendLoot && (turtleMode || isActiveExpansion("classic"));
|
|
||||||
const bool retryUse = turtleMode || isActiveExpansion("classic");
|
const bool retryUse = turtleMode || isActiveExpansion("classic");
|
||||||
if (retryUse || retryLoot) {
|
if (retryUse || retryLoot) {
|
||||||
pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot});
|
pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot});
|
||||||
|
|
@ -14610,6 +14984,26 @@ void GameHandler::buyBackItem(uint32_t buybackSlot) {
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) {
|
||||||
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
|
// CMSG_REPAIR_ITEM: npcGuid(8) + itemGuid(8) + useGuildBank(uint8)
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM));
|
||||||
|
packet.writeUInt64(vendorGuid);
|
||||||
|
packet.writeUInt64(itemGuid);
|
||||||
|
packet.writeUInt8(0); // do not use guild bank
|
||||||
|
socket->send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) {
|
||||||
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
|
// itemGuid = 0 signals "repair all equipped" to the server
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM));
|
||||||
|
packet.writeUInt64(vendorGuid);
|
||||||
|
packet.writeUInt64(0);
|
||||||
|
packet.writeUInt8(useGuildBank ? 1 : 0);
|
||||||
|
socket->send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
|
void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
|
||||||
if (state != WorldState::IN_WORLD || !socket) return;
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
LOG_INFO("Sell request: vendorGuid=0x", std::hex, vendorGuid,
|
LOG_INFO("Sell request: vendorGuid=0x", std::hex, vendorGuid,
|
||||||
|
|
@ -15173,7 +15567,9 @@ void GameHandler::handleGossipComplete(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleListInventory(network::Packet& packet) {
|
void GameHandler::handleListInventory(network::Packet& packet) {
|
||||||
|
bool savedCanRepair = currentVendorItems.canRepair; // preserve armorer flag set before openVendor()
|
||||||
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
||||||
|
currentVendorItems.canRepair = savedCanRepair;
|
||||||
vendorWindowOpen = true;
|
vendorWindowOpen = true;
|
||||||
gossipWindowOpen = false; // Close gossip if vendor opens
|
gossipWindowOpen = false; // Close gossip if vendor opens
|
||||||
|
|
||||||
|
|
@ -15275,8 +15671,10 @@ void GameHandler::loadSpellNameCache() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbc->getFieldCount() < 154) {
|
// Classic 1.12 Spell.dbc has 148 fields; TBC/WotLK have more.
|
||||||
LOG_WARNING("Trainer: Spell.dbc has too few fields");
|
// Require at least 148 so Classic trainers can resolve spell names.
|
||||||
|
if (dbc->getFieldCount() < 148) {
|
||||||
|
LOG_WARNING("Trainer: Spell.dbc has too few fields (", dbc->getFieldCount(), ")");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1242,8 +1242,8 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
||||||
|
|
||||||
packet.readUInt32(); // AllowableClass
|
packet.readUInt32(); // AllowableClass
|
||||||
packet.readUInt32(); // AllowableRace
|
packet.readUInt32(); // AllowableRace
|
||||||
packet.readUInt32(); // ItemLevel
|
data.itemLevel = packet.readUInt32();
|
||||||
packet.readUInt32(); // RequiredLevel
|
data.requiredLevel = packet.readUInt32();
|
||||||
packet.readUInt32(); // RequiredSkill
|
packet.readUInt32(); // RequiredSkill
|
||||||
packet.readUInt32(); // RequiredSkillRank
|
packet.readUInt32(); // RequiredSkillRank
|
||||||
packet.readUInt32(); // RequiredSpell
|
packet.readUInt32(); // RequiredSpell
|
||||||
|
|
@ -1266,7 +1266,10 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
||||||
case 5: data.intellect = statValue; break;
|
case 5: data.intellect = statValue; break;
|
||||||
case 6: data.spirit = statValue; break;
|
case 6: data.spirit = statValue; break;
|
||||||
case 7: data.stamina = statValue; break;
|
case 7: data.stamina = statValue; break;
|
||||||
default: break;
|
default:
|
||||||
|
if (statValue != 0)
|
||||||
|
data.extraStats.push_back({statType, statValue});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1302,6 +1305,40 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
||||||
data.delayMs = packet.readUInt32();
|
data.delayMs = packet.readUInt32();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AmmoType + RangedModRange (2 fields, 8 bytes)
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||||
|
packet.readUInt32(); // AmmoType
|
||||||
|
packet.readFloat(); // RangedModRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 item spells in Vanilla (3 fields each: SpellId, Trigger, Charges)
|
||||||
|
// Actually vanilla has 5 spells: SpellId, Trigger, Charges, Cooldown, Category, CatCooldown = 24 bytes each
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (packet.getReadPos() + 24 > packet.getSize()) break;
|
||||||
|
data.spells[i].spellId = packet.readUInt32();
|
||||||
|
data.spells[i].spellTrigger = packet.readUInt32();
|
||||||
|
packet.readUInt32(); // SpellCharges
|
||||||
|
packet.readUInt32(); // SpellCooldown
|
||||||
|
packet.readUInt32(); // SpellCategory
|
||||||
|
packet.readUInt32(); // SpellCategoryCooldown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonding type
|
||||||
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
||||||
|
data.bindType = packet.readUInt32();
|
||||||
|
|
||||||
|
// Description (flavor/lore text)
|
||||||
|
if (packet.getReadPos() < packet.getSize())
|
||||||
|
data.description = packet.readString();
|
||||||
|
|
||||||
|
// Post-description: PageText, LanguageID, PageMaterial, StartQuest
|
||||||
|
if (packet.getReadPos() + 16 <= packet.getSize()) {
|
||||||
|
packet.readUInt32(); // PageText
|
||||||
|
packet.readUInt32(); // LanguageID
|
||||||
|
packet.readUInt32(); // PageMaterial
|
||||||
|
data.startQuestId = packet.readUInt32(); // StartQuest
|
||||||
|
}
|
||||||
|
|
||||||
data.valid = !data.name.empty();
|
data.valid = !data.name.empty();
|
||||||
LOG_DEBUG("[Classic] Item query response: ", data.name, " (quality=", data.quality,
|
LOG_DEBUG("[Classic] Item query response: ", data.name, " (quality=", data.quality,
|
||||||
" invType=", data.inventoryType, " stack=", data.maxStack, ")");
|
" invType=", data.inventoryType, " stack=", data.maxStack, ")");
|
||||||
|
|
|
||||||
|
|
@ -739,9 +739,15 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa
|
||||||
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
||||||
uint32_t choiceCount = packet.readUInt32();
|
uint32_t choiceCount = packet.readUInt32();
|
||||||
for (uint32_t i = 0; i < choiceCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
|
for (uint32_t i = 0; i < choiceCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
|
||||||
packet.readUInt32(); // itemId
|
uint32_t itemId = packet.readUInt32();
|
||||||
packet.readUInt32(); // count
|
uint32_t count = packet.readUInt32();
|
||||||
packet.readUInt32(); // displayInfo
|
uint32_t dispId = packet.readUInt32();
|
||||||
|
if (itemId != 0) {
|
||||||
|
QuestRewardItem ri;
|
||||||
|
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
|
||||||
|
ri.choiceSlot = i;
|
||||||
|
data.rewardChoiceItems.push_back(ri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -749,9 +755,14 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa
|
||||||
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
||||||
uint32_t rewardCount = packet.readUInt32();
|
uint32_t rewardCount = packet.readUInt32();
|
||||||
for (uint32_t i = 0; i < rewardCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
|
for (uint32_t i = 0; i < rewardCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
|
||||||
packet.readUInt32(); // itemId
|
uint32_t itemId = packet.readUInt32();
|
||||||
packet.readUInt32(); // count
|
uint32_t count = packet.readUInt32();
|
||||||
packet.readUInt32(); // displayInfo
|
uint32_t dispId = packet.readUInt32();
|
||||||
|
if (itemId != 0) {
|
||||||
|
QuestRewardItem ri;
|
||||||
|
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
|
||||||
|
data.rewardItems.push_back(ri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -906,8 +917,8 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
||||||
|
|
||||||
packet.readUInt32(); // AllowableClass
|
packet.readUInt32(); // AllowableClass
|
||||||
packet.readUInt32(); // AllowableRace
|
packet.readUInt32(); // AllowableRace
|
||||||
packet.readUInt32(); // ItemLevel
|
data.itemLevel = packet.readUInt32();
|
||||||
packet.readUInt32(); // RequiredLevel
|
data.requiredLevel = packet.readUInt32();
|
||||||
packet.readUInt32(); // RequiredSkill
|
packet.readUInt32(); // RequiredSkill
|
||||||
packet.readUInt32(); // RequiredSkillRank
|
packet.readUInt32(); // RequiredSkillRank
|
||||||
packet.readUInt32(); // RequiredSpell
|
packet.readUInt32(); // RequiredSpell
|
||||||
|
|
@ -931,7 +942,10 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
||||||
case 5: data.intellect = statValue; break;
|
case 5: data.intellect = statValue; break;
|
||||||
case 6: data.spirit = statValue; break;
|
case 6: data.spirit = statValue; break;
|
||||||
case 7: data.stamina = statValue; break;
|
case 7: data.stamina = statValue; break;
|
||||||
default: break;
|
default:
|
||||||
|
if (statValue != 0)
|
||||||
|
data.extraStats.push_back({statType, statValue});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TBC: NO ScalingStatDistribution, NO ScalingStatValue (WotLK-only)
|
// TBC: NO ScalingStatDistribution, NO ScalingStatValue (WotLK-only)
|
||||||
|
|
@ -963,6 +977,39 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
||||||
data.delayMs = packet.readUInt32();
|
data.delayMs = packet.readUInt32();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AmmoType + RangedModRange
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||||
|
packet.readUInt32(); // AmmoType
|
||||||
|
packet.readFloat(); // RangedModRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 item spells
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (packet.getReadPos() + 24 > packet.getSize()) break;
|
||||||
|
data.spells[i].spellId = packet.readUInt32();
|
||||||
|
data.spells[i].spellTrigger = packet.readUInt32();
|
||||||
|
packet.readUInt32(); // SpellCharges
|
||||||
|
packet.readUInt32(); // SpellCooldown
|
||||||
|
packet.readUInt32(); // SpellCategory
|
||||||
|
packet.readUInt32(); // SpellCategoryCooldown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonding type
|
||||||
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
||||||
|
data.bindType = packet.readUInt32();
|
||||||
|
|
||||||
|
// Flavor/lore text
|
||||||
|
if (packet.getReadPos() < packet.getSize())
|
||||||
|
data.description = packet.readString();
|
||||||
|
|
||||||
|
// Post-description: PageText, LanguageID, PageMaterial, StartQuest
|
||||||
|
if (packet.getReadPos() + 16 <= packet.getSize()) {
|
||||||
|
packet.readUInt32(); // PageText
|
||||||
|
packet.readUInt32(); // LanguageID
|
||||||
|
packet.readUInt32(); // PageMaterial
|
||||||
|
data.startQuestId = packet.readUInt32(); // StartQuest
|
||||||
|
}
|
||||||
|
|
||||||
data.valid = !data.name.empty();
|
data.valid = !data.name.empty();
|
||||||
LOG_DEBUG("[TBC] Item query: ", data.name, " quality=", data.quality,
|
LOG_DEBUG("[TBC] Item query: ", data.name, " quality=", data.quality,
|
||||||
" invType=", data.inventoryType, " armor=", data.armor);
|
" invType=", data.inventoryType, " armor=", data.armor);
|
||||||
|
|
|
||||||
|
|
@ -1422,6 +1422,14 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ChatType::BG_SYSTEM_NEUTRAL:
|
||||||
|
case ChatType::BG_SYSTEM_ALLIANCE:
|
||||||
|
case ChatType::BG_SYSTEM_HORDE:
|
||||||
|
// BG/Arena system messages — no sender GUID or name field, just message.
|
||||||
|
// Reclassify as SYSTEM for consistent display.
|
||||||
|
data.type = ChatType::SYSTEM;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// SAY, GUILD, PARTY, YELL, WHISPER, WHISPER_INFORM, RAID, etc.
|
// SAY, GUILD, PARTY, YELL, WHISPER, WHISPER_INFORM, RAID, etc.
|
||||||
// All have receiverGuid (typically senderGuid repeated)
|
// All have receiverGuid (typically senderGuid repeated)
|
||||||
|
|
@ -2441,8 +2449,8 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
||||||
|
|
||||||
packet.readUInt32(); // AllowableClass
|
packet.readUInt32(); // AllowableClass
|
||||||
packet.readUInt32(); // AllowableRace
|
packet.readUInt32(); // AllowableRace
|
||||||
packet.readUInt32(); // ItemLevel
|
data.itemLevel = packet.readUInt32();
|
||||||
packet.readUInt32(); // RequiredLevel
|
data.requiredLevel = packet.readUInt32();
|
||||||
packet.readUInt32(); // RequiredSkill
|
packet.readUInt32(); // RequiredSkill
|
||||||
packet.readUInt32(); // RequiredSkillRank
|
packet.readUInt32(); // RequiredSkillRank
|
||||||
packet.readUInt32(); // RequiredSpell
|
packet.readUInt32(); // RequiredSpell
|
||||||
|
|
@ -2466,7 +2474,10 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
||||||
case 5: data.intellect = statValue; break;
|
case 5: data.intellect = statValue; break;
|
||||||
case 6: data.spirit = statValue; break;
|
case 6: data.spirit = statValue; break;
|
||||||
case 7: data.stamina = statValue; break;
|
case 7: data.stamina = statValue; break;
|
||||||
default: break;
|
default:
|
||||||
|
if (statValue != 0)
|
||||||
|
data.extraStats.push_back({statType, statValue});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2510,6 +2521,22 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
||||||
packet.readUInt32(); // SpellCategoryCooldown
|
packet.readUInt32(); // SpellCategoryCooldown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bonding type (0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ)
|
||||||
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
||||||
|
data.bindType = packet.readUInt32();
|
||||||
|
|
||||||
|
// Flavor/lore text (Description cstring)
|
||||||
|
if (packet.getReadPos() < packet.getSize())
|
||||||
|
data.description = packet.readString();
|
||||||
|
|
||||||
|
// Post-description fields: PageText, LanguageID, PageMaterial, StartQuest
|
||||||
|
if (packet.getReadPos() + 16 <= packet.getSize()) {
|
||||||
|
packet.readUInt32(); // PageText
|
||||||
|
packet.readUInt32(); // LanguageID
|
||||||
|
packet.readUInt32(); // PageMaterial
|
||||||
|
data.startQuestId = packet.readUInt32(); // StartQuest
|
||||||
|
}
|
||||||
|
|
||||||
data.valid = !data.name.empty();
|
data.valid = !data.name.empty();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -3419,9 +3446,15 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data)
|
||||||
/*choiceCount*/ packet.readUInt32();
|
/*choiceCount*/ packet.readUInt32();
|
||||||
for (int i = 0; i < 6; i++) {
|
for (int i = 0; i < 6; i++) {
|
||||||
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
||||||
packet.readUInt32(); // itemId
|
uint32_t itemId = packet.readUInt32();
|
||||||
packet.readUInt32(); // count
|
uint32_t count = packet.readUInt32();
|
||||||
packet.readUInt32(); // displayInfo
|
uint32_t dispId = packet.readUInt32();
|
||||||
|
if (itemId != 0) {
|
||||||
|
QuestRewardItem ri;
|
||||||
|
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
|
||||||
|
ri.choiceSlot = static_cast<uint32_t>(i);
|
||||||
|
data.rewardChoiceItems.push_back(ri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3430,9 +3463,14 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data)
|
||||||
/*rewardCount*/ packet.readUInt32();
|
/*rewardCount*/ packet.readUInt32();
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
||||||
packet.readUInt32(); // itemId
|
uint32_t itemId = packet.readUInt32();
|
||||||
packet.readUInt32(); // count
|
uint32_t count = packet.readUInt32();
|
||||||
packet.readUInt32(); // displayInfo
|
uint32_t dispId = packet.readUInt32();
|
||||||
|
if (itemId != 0) {
|
||||||
|
QuestRewardItem ri;
|
||||||
|
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
|
||||||
|
data.rewardItems.push_back(ri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,7 @@ void CameraController::update(float deltaTime) {
|
||||||
bool shiftDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
|
bool shiftDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
|
||||||
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
||||||
bool nowJump = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyJustPressed(SDL_SCANCODE_SPACE);
|
bool nowJump = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyJustPressed(SDL_SCANCODE_SPACE);
|
||||||
|
bool spaceDown = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
||||||
|
|
||||||
// Idle camera: any input resets the timer; timeout triggers a slow orbit pan
|
// Idle camera: any input resets the timer; timeout triggers a slow orbit pan
|
||||||
bool anyInput = leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump;
|
bool anyInput = leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump;
|
||||||
|
|
@ -275,8 +276,10 @@ void CameraController::update(float deltaTime) {
|
||||||
if (mouseAutorun) {
|
if (mouseAutorun) {
|
||||||
autoRunning = false;
|
autoRunning = false;
|
||||||
}
|
}
|
||||||
bool nowForward = keyW || mouseAutorun || autoRunning;
|
// When the server has rooted the player, suppress all horizontal movement input.
|
||||||
bool nowBackward = keyS;
|
const bool movBlocked = movementRooted_;
|
||||||
|
bool nowForward = !movBlocked && (keyW || mouseAutorun || autoRunning);
|
||||||
|
bool nowBackward = !movBlocked && keyS;
|
||||||
bool nowStrafeLeft = false;
|
bool nowStrafeLeft = false;
|
||||||
bool nowStrafeRight = false;
|
bool nowStrafeRight = false;
|
||||||
bool nowTurnLeft = false;
|
bool nowTurnLeft = false;
|
||||||
|
|
@ -285,21 +288,27 @@ void CameraController::update(float deltaTime) {
|
||||||
// WoW-like third-person keyboard behavior:
|
// WoW-like third-person keyboard behavior:
|
||||||
// - RMB held: A/D strafe
|
// - RMB held: A/D strafe
|
||||||
// - RMB released: A/D turn character+camera, Q/E strafe
|
// - RMB released: A/D turn character+camera, Q/E strafe
|
||||||
|
// Turning is allowed even while rooted; only positional movement is blocked.
|
||||||
if (thirdPerson && !rightMouseDown) {
|
if (thirdPerson && !rightMouseDown) {
|
||||||
nowTurnLeft = keyA;
|
nowTurnLeft = keyA;
|
||||||
nowTurnRight = keyD;
|
nowTurnRight = keyD;
|
||||||
nowStrafeLeft = keyQ;
|
nowStrafeLeft = !movBlocked && keyQ;
|
||||||
nowStrafeRight = keyE;
|
nowStrafeRight = !movBlocked && keyE;
|
||||||
} else {
|
} else {
|
||||||
nowStrafeLeft = keyA || keyQ;
|
nowStrafeLeft = !movBlocked && (keyA || keyQ);
|
||||||
nowStrafeRight = keyD || keyE;
|
nowStrafeRight = !movBlocked && (keyD || keyE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard turning updates camera yaw (character follows yaw in renderer)
|
// Keyboard turning updates camera yaw (character follows yaw in renderer).
|
||||||
|
// Use server turn rate (rad/s) when set; otherwise fall back to WOW_TURN_SPEED (deg/s).
|
||||||
|
const float activeTurnSpeedDeg = (turnRateOverride_ > 0.0f && turnRateOverride_ < 20.0f
|
||||||
|
&& !std::isnan(turnRateOverride_))
|
||||||
|
? glm::degrees(turnRateOverride_)
|
||||||
|
: WOW_TURN_SPEED;
|
||||||
if (nowTurnLeft && !nowTurnRight) {
|
if (nowTurnLeft && !nowTurnRight) {
|
||||||
yaw += WOW_TURN_SPEED * deltaTime;
|
yaw += activeTurnSpeedDeg * deltaTime;
|
||||||
} else if (nowTurnRight && !nowTurnLeft) {
|
} else if (nowTurnRight && !nowTurnLeft) {
|
||||||
yaw -= WOW_TURN_SPEED * deltaTime;
|
yaw -= activeTurnSpeedDeg * deltaTime;
|
||||||
}
|
}
|
||||||
if (nowTurnLeft || nowTurnRight) {
|
if (nowTurnLeft || nowTurnRight) {
|
||||||
camera->setRotation(yaw, pitch);
|
camera->setRotation(yaw, pitch);
|
||||||
|
|
@ -315,9 +324,12 @@ void CameraController::update(float deltaTime) {
|
||||||
if (useWoWSpeed) {
|
if (useWoWSpeed) {
|
||||||
// Movement speeds (WoW-like: Ctrl walk, default run, backpedal slower)
|
// Movement speeds (WoW-like: Ctrl walk, default run, backpedal slower)
|
||||||
if (nowBackward && !nowForward) {
|
if (nowBackward && !nowForward) {
|
||||||
speed = WOW_BACK_SPEED;
|
speed = (runBackSpeedOverride_ > 0.0f && runBackSpeedOverride_ < 100.0f
|
||||||
|
&& !std::isnan(runBackSpeedOverride_))
|
||||||
|
? runBackSpeedOverride_ : WOW_BACK_SPEED;
|
||||||
} else if (ctrlDown) {
|
} else if (ctrlDown) {
|
||||||
speed = WOW_WALK_SPEED;
|
speed = (walkSpeedOverride_ > 0.0f && walkSpeedOverride_ < 100.0f && !std::isnan(walkSpeedOverride_))
|
||||||
|
? walkSpeedOverride_ : WOW_WALK_SPEED;
|
||||||
} else if (runSpeedOverride_ > 0.0f && runSpeedOverride_ < 100.0f && !std::isnan(runSpeedOverride_)) {
|
} else if (runSpeedOverride_ > 0.0f && runSpeedOverride_ < 100.0f && !std::isnan(runSpeedOverride_)) {
|
||||||
speed = runSpeedOverride_;
|
speed = runSpeedOverride_;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -406,7 +418,14 @@ void CameraController::update(float deltaTime) {
|
||||||
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
|
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
|
||||||
constexpr float MIN_SWIM_WATER_DEPTH = 1.0f;
|
constexpr float MIN_SWIM_WATER_DEPTH = 1.0f;
|
||||||
bool inWater = false;
|
bool inWater = false;
|
||||||
if (waterH && targetPos.z < *waterH) {
|
// Water Walk: treat water surface as ground — player walks on top, not through.
|
||||||
|
if (waterWalkActive_ && waterH && targetPos.z >= *waterH - 0.5f) {
|
||||||
|
// Clamp to water surface so the player stands on it
|
||||||
|
targetPos.z = *waterH;
|
||||||
|
verticalVelocity = 0.0f;
|
||||||
|
grounded = true;
|
||||||
|
inWater = false;
|
||||||
|
} else if (waterH && targetPos.z < *waterH) {
|
||||||
std::optional<uint16_t> waterType;
|
std::optional<uint16_t> waterType;
|
||||||
if (waterRenderer) {
|
if (waterRenderer) {
|
||||||
waterType = waterRenderer->getWaterTypeAt(targetPos.x, targetPos.y);
|
waterType = waterRenderer->getWaterTypeAt(targetPos.x, targetPos.y);
|
||||||
|
|
@ -504,7 +523,8 @@ void CameraController::update(float deltaTime) {
|
||||||
swimming = true;
|
swimming = true;
|
||||||
// Swim movement follows look pitch (forward/back), while strafe stays
|
// Swim movement follows look pitch (forward/back), while strafe stays
|
||||||
// lateral for stable control.
|
// lateral for stable control.
|
||||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
float swimSpeed = (swimSpeedOverride_ > 0.0f && swimSpeedOverride_ < 100.0f && !std::isnan(swimSpeedOverride_))
|
||||||
|
? swimSpeedOverride_ : speed * SWIM_SPEED_FACTOR;
|
||||||
float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z;
|
float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z;
|
||||||
|
|
||||||
// For auto-run/auto-swim: use character facing (immune to camera pan)
|
// For auto-run/auto-swim: use character facing (immune to camera pan)
|
||||||
|
|
@ -523,6 +543,10 @@ void CameraController::update(float deltaTime) {
|
||||||
// Use character's facing direction for strafe, not camera's right vector
|
// Use character's facing direction for strafe, not camera's right vector
|
||||||
glm::vec3 swimRight = right; // Character's right (horizontal facing), not camera's
|
glm::vec3 swimRight = right; // Character's right (horizontal facing), not camera's
|
||||||
|
|
||||||
|
float swimBackSpeed = (swimBackSpeedOverride_ > 0.0f && swimBackSpeedOverride_ < 100.0f
|
||||||
|
&& !std::isnan(swimBackSpeedOverride_))
|
||||||
|
? swimBackSpeedOverride_ : swimSpeed * 0.5f;
|
||||||
|
|
||||||
glm::vec3 swimMove(0.0f);
|
glm::vec3 swimMove(0.0f);
|
||||||
if (nowForward) swimMove += swimForward;
|
if (nowForward) swimMove += swimForward;
|
||||||
if (nowBackward) swimMove -= swimForward;
|
if (nowBackward) swimMove -= swimForward;
|
||||||
|
|
@ -531,7 +555,9 @@ void CameraController::update(float deltaTime) {
|
||||||
|
|
||||||
if (glm::length(swimMove) > 0.001f) {
|
if (glm::length(swimMove) > 0.001f) {
|
||||||
swimMove = glm::normalize(swimMove);
|
swimMove = glm::normalize(swimMove);
|
||||||
targetPos += swimMove * swimSpeed * physicsDeltaTime;
|
// Use backward swim speed when moving backwards only (not when combining with strafe)
|
||||||
|
float applySpeed = (nowBackward && !nowForward) ? swimBackSpeed : swimSpeed;
|
||||||
|
targetPos += swimMove * applySpeed * physicsDeltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spacebar = swim up (continuous, not a jump)
|
// Spacebar = swim up (continuous, not a jump)
|
||||||
|
|
@ -680,11 +706,60 @@ void CameraController::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
swimming = false;
|
swimming = false;
|
||||||
|
|
||||||
|
// Player-controlled flight (flying mount / druid Flight Form):
|
||||||
|
// Use 3D pitch-following movement with no gravity or grounding.
|
||||||
|
if (flyingActive_) {
|
||||||
|
grounded = true; // suppress fall-damage checks
|
||||||
|
verticalVelocity = 0.0f;
|
||||||
|
jumpBufferTimer = 0.0f;
|
||||||
|
coyoteTimer = 0.0f;
|
||||||
|
|
||||||
|
// Forward/back follows camera 3D direction (same as swim)
|
||||||
|
glm::vec3 flyFwd = glm::normalize(forward3D);
|
||||||
|
if (glm::length(flyFwd) < 1e-4f) flyFwd = forward;
|
||||||
|
glm::vec3 flyMove(0.0f);
|
||||||
|
if (nowForward) flyMove += flyFwd;
|
||||||
|
if (nowBackward) flyMove -= flyFwd;
|
||||||
|
if (nowStrafeLeft) flyMove += right;
|
||||||
|
if (nowStrafeRight) flyMove -= right;
|
||||||
|
// Space = ascend, X = descend while airborne
|
||||||
|
bool flyDescend = !uiWantsKeyboard && xDown && mounted_;
|
||||||
|
if (nowJump) flyMove.z += 1.0f;
|
||||||
|
if (flyDescend) flyMove.z -= 1.0f;
|
||||||
|
if (glm::length(flyMove) > 0.001f) {
|
||||||
|
flyMove = glm::normalize(flyMove);
|
||||||
|
float flyFwdSpeed = (flightSpeedOverride_ > 0.0f && flightSpeedOverride_ < 200.0f
|
||||||
|
&& !std::isnan(flightSpeedOverride_))
|
||||||
|
? flightSpeedOverride_ : speed;
|
||||||
|
float flyBackSpeed = (flightBackSpeedOverride_ > 0.0f && flightBackSpeedOverride_ < 200.0f
|
||||||
|
&& !std::isnan(flightBackSpeedOverride_))
|
||||||
|
? flightBackSpeedOverride_ : flyFwdSpeed * 0.5f;
|
||||||
|
float flySpeed = (nowBackward && !nowForward) ? flyBackSpeed : flyFwdSpeed;
|
||||||
|
targetPos += flyMove * flySpeed * physicsDeltaTime;
|
||||||
|
}
|
||||||
|
targetPos.z += verticalVelocity * physicsDeltaTime;
|
||||||
|
// Skip all ground physics — go straight to collision/WMO sections
|
||||||
|
} else {
|
||||||
|
|
||||||
if (glm::length(movement) > 0.001f) {
|
if (glm::length(movement) > 0.001f) {
|
||||||
movement = glm::normalize(movement);
|
movement = glm::normalize(movement);
|
||||||
targetPos += movement * speed * physicsDeltaTime;
|
targetPos += movement * speed * physicsDeltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply server-driven knockback horizontal velocity (decays over time).
|
||||||
|
if (knockbackActive_) {
|
||||||
|
targetPos.x += knockbackHorizVel_.x * physicsDeltaTime;
|
||||||
|
targetPos.y += knockbackHorizVel_.y * physicsDeltaTime;
|
||||||
|
// Exponential drag: reduce each frame so the player decelerates naturally.
|
||||||
|
float drag = std::exp(-KNOCKBACK_HORIZ_DRAG * physicsDeltaTime);
|
||||||
|
knockbackHorizVel_ *= drag;
|
||||||
|
// Once negligible, clear the flag so collision/grounding work normally.
|
||||||
|
if (glm::length(knockbackHorizVel_) < 0.05f) {
|
||||||
|
knockbackActive_ = false;
|
||||||
|
knockbackHorizVel_ = glm::vec2(0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Jump with input buffering and coyote time
|
// Jump with input buffering and coyote time
|
||||||
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
|
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
|
||||||
if (grounded) coyoteTimer = COYOTE_TIME;
|
if (grounded) coyoteTimer = COYOTE_TIME;
|
||||||
|
|
@ -700,10 +775,20 @@ void CameraController::update(float deltaTime) {
|
||||||
jumpBufferTimer -= physicsDeltaTime;
|
jumpBufferTimer -= physicsDeltaTime;
|
||||||
coyoteTimer -= physicsDeltaTime;
|
coyoteTimer -= physicsDeltaTime;
|
||||||
|
|
||||||
// Apply gravity
|
// Apply gravity (skip when server has disabled gravity, e.g. Levitate spell)
|
||||||
verticalVelocity += gravity * physicsDeltaTime;
|
if (gravityDisabled_) {
|
||||||
targetPos.z += verticalVelocity * physicsDeltaTime;
|
// Float in place: bleed off any downward velocity, allow upward to decay slowly
|
||||||
|
if (verticalVelocity < 0.0f) verticalVelocity = 0.0f;
|
||||||
|
else verticalVelocity *= std::max(0.0f, 1.0f - 3.0f * physicsDeltaTime);
|
||||||
|
} else {
|
||||||
|
verticalVelocity += gravity * physicsDeltaTime;
|
||||||
|
// Feather Fall / Slow Fall: cap downward terminal velocity to ~2 m/s
|
||||||
|
if (featherFallActive_ && verticalVelocity < -2.0f)
|
||||||
|
verticalVelocity = -2.0f;
|
||||||
}
|
}
|
||||||
|
targetPos.z += verticalVelocity * physicsDeltaTime;
|
||||||
|
} // end !flyingActive_ ground physics
|
||||||
|
} // end !inWater
|
||||||
} else {
|
} else {
|
||||||
// External follow (e.g., taxi): trust server position without grounding.
|
// External follow (e.g., taxi): trust server position without grounding.
|
||||||
swimming = false;
|
swimming = false;
|
||||||
|
|
@ -1180,7 +1265,10 @@ void CameraController::update(float deltaTime) {
|
||||||
dz >= -0.25f && dz <= stepUp * 1.5f);
|
dz >= -0.25f && dz <= stepUp * 1.5f);
|
||||||
|
|
||||||
if (dz >= -fallCatch && (nearGround || airFalling || slopeGrace)) {
|
if (dz >= -fallCatch && (nearGround || airFalling || slopeGrace)) {
|
||||||
targetPos.z = *groundH;
|
// HOVER: float at fixed height above ground instead of standing on it
|
||||||
|
static constexpr float HOVER_HEIGHT = 4.0f; // ~4 yards above ground
|
||||||
|
const float snapH = hoverActive_ ? (*groundH + HOVER_HEIGHT) : *groundH;
|
||||||
|
targetPos.z = snapH;
|
||||||
verticalVelocity = 0.0f;
|
verticalVelocity = 0.0f;
|
||||||
grounded = true;
|
grounded = true;
|
||||||
lastGroundZ = *groundH;
|
lastGroundZ = *groundH;
|
||||||
|
|
@ -1495,7 +1583,8 @@ void CameraController::update(float deltaTime) {
|
||||||
|
|
||||||
if (inWater) {
|
if (inWater) {
|
||||||
swimming = true;
|
swimming = true;
|
||||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
float swimSpeed = (swimSpeedOverride_ > 0.0f && swimSpeedOverride_ < 100.0f && !std::isnan(swimSpeedOverride_))
|
||||||
|
? swimSpeedOverride_ : speed * SWIM_SPEED_FACTOR;
|
||||||
float waterSurfaceCamZ = waterH ? (*waterH - WATER_SURFACE_OFFSET + eyeHeight) : newPos.z;
|
float waterSurfaceCamZ = waterH ? (*waterH - WATER_SURFACE_OFFSET + eyeHeight) : newPos.z;
|
||||||
bool diveIntent = nowForward && (forward3D.z < -0.28f);
|
bool diveIntent = nowForward && (forward3D.z < -0.28f);
|
||||||
|
|
||||||
|
|
@ -1707,6 +1796,35 @@ void CameraController::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flight ascend/descend transitions (Space = ascend, X = descend while mounted+flying)
|
||||||
|
if (movementCallback && !externalFollow_) {
|
||||||
|
const bool nowAscending = flyingActive_ && spaceDown;
|
||||||
|
const bool nowDescending = flyingActive_ && xDown && mounted_;
|
||||||
|
|
||||||
|
if (flyingActive_) {
|
||||||
|
if (nowAscending && !wasAscending_) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_START_ASCEND));
|
||||||
|
} else if (!nowAscending && wasAscending_) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_STOP_ASCEND));
|
||||||
|
}
|
||||||
|
if (nowDescending && !wasDescending_) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_START_DESCEND));
|
||||||
|
} else if (!nowDescending && wasDescending_) {
|
||||||
|
// No separate STOP_DESCEND opcode; STOP_ASCEND ends all vertical movement
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_STOP_ASCEND));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Left flight mode: clear any lingering vertical movement states
|
||||||
|
if (wasAscending_) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_STOP_ASCEND));
|
||||||
|
} else if (wasDescending_) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_STOP_ASCEND));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasAscending_ = nowAscending;
|
||||||
|
wasDescending_ = nowDescending;
|
||||||
|
}
|
||||||
|
|
||||||
// Update previous-frame state
|
// Update previous-frame state
|
||||||
wasSwimming = swimming;
|
wasSwimming = swimming;
|
||||||
wasMovingForward = nowForward;
|
wasMovingForward = nowForward;
|
||||||
|
|
@ -2096,5 +2214,28 @@ void CameraController::triggerMountJump() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CameraController::applyKnockBack(float vcos, float vsin, float hspeed, float vspeed) {
|
||||||
|
// The server sends (vcos, vsin) as the 2D direction vector in server/wire
|
||||||
|
// coordinate space. After the server→canonical→render swaps, the direction
|
||||||
|
// in render space is simply (vcos, vsin) — the two swaps cancel each other.
|
||||||
|
knockbackHorizVel_ = glm::vec2(vcos, vsin) * hspeed;
|
||||||
|
knockbackActive_ = true;
|
||||||
|
|
||||||
|
// vspeed in the wire packet is negative when the server wants to launch the
|
||||||
|
// player upward (matches TrinityCore: data << float(-speedZ)). Negate it
|
||||||
|
// here to obtain the correct upward initial velocity.
|
||||||
|
verticalVelocity = -vspeed;
|
||||||
|
grounded = false;
|
||||||
|
coyoteTimer = 0.0f;
|
||||||
|
jumpBufferTimer = 0.0f;
|
||||||
|
|
||||||
|
// Notify the server that the player left the ground so the FALLING flag is
|
||||||
|
// set in subsequent movement heartbeats. The normal jump detection
|
||||||
|
// (nowJump && grounded) does not fire during a server-driven knockback.
|
||||||
|
if (movementCallback) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::MSG_MOVE_JUMP));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -2096,8 +2096,8 @@ void Renderer::updateCharacterAnimation() {
|
||||||
// Rider uses character facing yaw, not mount bone rotation
|
// Rider uses character facing yaw, not mount bone rotation
|
||||||
// (rider faces character direction, seat bone only provides position)
|
// (rider faces character direction, seat bone only provides position)
|
||||||
float yawRad = glm::radians(characterYaw);
|
float yawRad = glm::radians(characterYaw);
|
||||||
float riderPitch = taxiFlight_ ? mountPitch_ * 0.35f : 0.0f;
|
float riderPitch = mountPitch_ * 0.35f;
|
||||||
float riderRoll = taxiFlight_ ? mountRoll_ * 0.35f : 0.0f;
|
float riderRoll = mountRoll_ * 0.35f;
|
||||||
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad));
|
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad));
|
||||||
} else {
|
} else {
|
||||||
// Fallback to old manual positioning if attachment not found
|
// Fallback to old manual positioning if attachment not found
|
||||||
|
|
@ -4737,7 +4737,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
auto t0 = std::chrono::steady_clock::now();
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
VkCommandBuffer cmd = beginSecondary(SEC_WMO);
|
VkCommandBuffer cmd = beginSecondary(SEC_WMO);
|
||||||
setSecondaryViewportScissor(cmd);
|
setSecondaryViewportScissor(cmd);
|
||||||
wmoRenderer->render(cmd, perFrameSet, *camera);
|
wmoRenderer->render(cmd, perFrameSet, *camera, &characterPosition);
|
||||||
vkEndCommandBuffer(cmd);
|
vkEndCommandBuffer(cmd);
|
||||||
return std::chrono::duration<double, std::milli>(
|
return std::chrono::duration<double, std::milli>(
|
||||||
std::chrono::steady_clock::now() - t0).count();
|
std::chrono::steady_clock::now() - t0).count();
|
||||||
|
|
@ -4905,7 +4905,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
if (wmoRenderer && camera && !skipWMO) {
|
if (wmoRenderer && camera && !skipWMO) {
|
||||||
wmoRenderer->prepareRender();
|
wmoRenderer->prepareRender();
|
||||||
auto wmoStart = std::chrono::steady_clock::now();
|
auto wmoStart = std::chrono::steady_clock::now();
|
||||||
wmoRenderer->render(currentCmd, perFrameSet, *camera);
|
wmoRenderer->render(currentCmd, perFrameSet, *camera, &characterPosition);
|
||||||
lastWMORenderMs = std::chrono::duration<double, std::milli>(
|
lastWMORenderMs = std::chrono::duration<double, std::milli>(
|
||||||
std::chrono::steady_clock::now() - wmoStart).count();
|
std::chrono::steady_clock::now() - wmoStart).count();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1356,7 +1356,8 @@ void WMORenderer::prepareRender() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera,
|
||||||
|
const glm::vec3* viewerPos) {
|
||||||
if (!opaquePipeline_ || instances.empty()) {
|
if (!opaquePipeline_ || instances.empty()) {
|
||||||
lastDrawCalls = 0;
|
lastDrawCalls = 0;
|
||||||
return;
|
return;
|
||||||
|
|
@ -1380,6 +1381,11 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec3 camPos = camera.getPosition();
|
glm::vec3 camPos = camera.getPosition();
|
||||||
|
// For portal culling, use the character/player position when available.
|
||||||
|
// The 3rd-person camera can orbit outside a WMO while the character is inside,
|
||||||
|
// causing the portal traversal to start from outside and cull interior groups.
|
||||||
|
// Passing the actual character position as the viewer fixes this.
|
||||||
|
glm::vec3 portalViewerPos = viewerPos ? *viewerPos : camPos;
|
||||||
bool doPortalCull = portalCulling;
|
bool doPortalCull = portalCulling;
|
||||||
bool doDistanceCull = distanceCulling;
|
bool doDistanceCull = distanceCulling;
|
||||||
|
|
||||||
|
|
@ -1400,7 +1406,7 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
bool usePortalCulling = doPortalCull && !model.portals.empty() && !model.portalRefs.empty();
|
bool usePortalCulling = doPortalCull && !model.portals.empty() && !model.portalRefs.empty();
|
||||||
if (usePortalCulling) {
|
if (usePortalCulling) {
|
||||||
std::unordered_set<uint32_t> pvgSet;
|
std::unordered_set<uint32_t> pvgSet;
|
||||||
glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(camPos, 1.0f);
|
glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(portalViewerPos, 1.0f);
|
||||||
getVisibleGroupsViaPortals(model, glm::vec3(localCamPos), frustum,
|
getVisibleGroupsViaPortals(model, glm::vec3(localCamPos), frustum,
|
||||||
instance.modelMatrix, pvgSet);
|
instance.modelMatrix, pvgSet);
|
||||||
portalVisibleGroups.assign(pvgSet.begin(), pvgSet.end());
|
portalVisibleGroups.assign(pvgSet.begin(), pvgSet.end());
|
||||||
|
|
@ -2049,12 +2055,25 @@ bool WMORenderer::isPortalVisible(const ModelData& model, uint16_t portalIndex,
|
||||||
}
|
}
|
||||||
center /= static_cast<float>(portal.vertexCount);
|
center /= static_cast<float>(portal.vertexCount);
|
||||||
|
|
||||||
// Transform bounds to world space for frustum test
|
// Transform all 8 corners to world space to build the correct world AABB.
|
||||||
glm::vec4 worldMin = modelMatrix * glm::vec4(pMin, 1.0f);
|
// Direct transform of pMin/pMax is wrong for rotated WMOs — the matrix can
|
||||||
glm::vec4 worldMax = modelMatrix * glm::vec4(pMax, 1.0f);
|
// swap or negate components, inverting min/max and causing frustum test failures.
|
||||||
|
const glm::vec3 corners[8] = {
|
||||||
|
{pMin.x, pMin.y, pMin.z}, {pMax.x, pMin.y, pMin.z},
|
||||||
|
{pMin.x, pMax.y, pMin.z}, {pMax.x, pMax.y, pMin.z},
|
||||||
|
{pMin.x, pMin.y, pMax.z}, {pMax.x, pMin.y, pMax.z},
|
||||||
|
{pMin.x, pMax.y, pMax.z}, {pMax.x, pMax.y, pMax.z},
|
||||||
|
};
|
||||||
|
glm::vec3 worldMin( std::numeric_limits<float>::max());
|
||||||
|
glm::vec3 worldMax(-std::numeric_limits<float>::max());
|
||||||
|
for (const auto& c : corners) {
|
||||||
|
glm::vec3 wc = glm::vec3(modelMatrix * glm::vec4(c, 1.0f));
|
||||||
|
worldMin = glm::min(worldMin, wc);
|
||||||
|
worldMax = glm::max(worldMax, wc);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if portal AABB intersects frustum (more robust than point test)
|
// Check if portal AABB intersects frustum (more robust than point test)
|
||||||
return frustum.intersectsAABB(glm::vec3(worldMin), glm::vec3(worldMax));
|
return frustum.intersectsAABB(worldMin, worldMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
|
void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1594,6 +1594,20 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
IM_COL32(255, 255, 255, 220), countStr);
|
IM_COL32(255, 255, 255, 220), countStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Durability bar on equipment slots (3px strip at bottom of slot icon)
|
||||||
|
if (kind == SlotKind::EQUIPMENT && item.maxDurability > 0) {
|
||||||
|
float durPct = static_cast<float>(item.curDurability) /
|
||||||
|
static_cast<float>(item.maxDurability);
|
||||||
|
ImU32 durCol;
|
||||||
|
if (durPct > 0.5f) durCol = IM_COL32(0, 200, 0, 220);
|
||||||
|
else if (durPct > 0.25f) durCol = IM_COL32(220, 220, 0, 220);
|
||||||
|
else durCol = IM_COL32(220, 40, 40, 220);
|
||||||
|
float barW = size * durPct;
|
||||||
|
drawList->AddRectFilled(ImVec2(pos.x, pos.y + size - 3.0f),
|
||||||
|
ImVec2(pos.x + barW, pos.y + size),
|
||||||
|
durCol);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||||
|
|
||||||
// Left mouse: hold to pick up, release to drop/swap
|
// Left mouse: hold to pick up, release to drop/swap
|
||||||
|
|
@ -1700,6 +1714,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
||||||
|
|
||||||
ImVec4 qColor = getQualityColor(item.quality);
|
ImVec4 qColor = getQualityColor(item.quality);
|
||||||
ImGui::TextColored(qColor, "%s", item.name.c_str());
|
ImGui::TextColored(qColor, "%s", item.name.c_str());
|
||||||
|
if (item.itemLevel > 0) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", item.itemLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binding type
|
||||||
|
switch (item.bindType) {
|
||||||
|
case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break;
|
||||||
|
case 2: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when equipped"); break;
|
||||||
|
case 3: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when used"); break;
|
||||||
|
case 4: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Quest Item"); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.itemId == 6948 && gameHandler_) {
|
if (item.itemId == 6948 && gameHandler_) {
|
||||||
uint32_t mapId = 0;
|
uint32_t mapId = 0;
|
||||||
|
|
@ -1776,13 +1802,15 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
||||||
};
|
};
|
||||||
const bool isWeapon = isWeaponInventoryType(item.inventoryType);
|
const bool isWeapon = isWeaponInventoryType(item.inventoryType);
|
||||||
|
|
||||||
// Compact stats view for weapons: DPS + condensed stat bonuses.
|
// Compact stats view for weapons: damage range + speed + DPS
|
||||||
// Non-weapons keep armor/sell info visible.
|
|
||||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
if (isWeapon && item.damageMax > 0.0f && item.delayMs > 0) {
|
if (isWeapon && item.damageMax > 0.0f && item.delayMs > 0) {
|
||||||
float speed = static_cast<float>(item.delayMs) / 1000.0f;
|
float speed = static_cast<float>(item.delayMs) / 1000.0f;
|
||||||
float dps = ((item.damageMin + item.damageMax) * 0.5f) / speed;
|
float dps = ((item.damageMin + item.damageMax) * 0.5f) / speed;
|
||||||
ImGui::Text("%.1f DPS", dps);
|
ImGui::Text("%.0f - %.0f Damage", item.damageMin, item.damageMax);
|
||||||
|
ImGui::SameLine(160.0f);
|
||||||
|
ImGui::TextDisabled("Speed %.2f", speed);
|
||||||
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(%.1f damage per second)", dps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Armor appears before stat bonuses — matches WoW tooltip order
|
// Armor appears before stat bonuses — matches WoW tooltip order
|
||||||
|
|
@ -1805,6 +1833,101 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
||||||
if (!bonusLine.empty()) {
|
if (!bonusLine.empty()) {
|
||||||
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extra stats (hit, crit, haste, AP, SP, etc.) — one line each
|
||||||
|
for (const auto& es : item.extraStats) {
|
||||||
|
const char* statName = nullptr;
|
||||||
|
switch (es.statType) {
|
||||||
|
case 0: statName = "Mana"; break;
|
||||||
|
case 1: statName = "Health"; break;
|
||||||
|
case 12: statName = "Defense Rating"; break;
|
||||||
|
case 13: statName = "Dodge Rating"; break;
|
||||||
|
case 14: statName = "Parry Rating"; break;
|
||||||
|
case 15: statName = "Block Rating"; break;
|
||||||
|
case 16: statName = "Hit Rating"; break;
|
||||||
|
case 17: statName = "Hit Rating"; break;
|
||||||
|
case 18: statName = "Hit Rating"; break;
|
||||||
|
case 19: statName = "Crit Rating"; break;
|
||||||
|
case 20: statName = "Crit Rating"; break;
|
||||||
|
case 21: statName = "Crit Rating"; break;
|
||||||
|
case 28: statName = "Haste Rating"; break;
|
||||||
|
case 29: statName = "Haste Rating"; break;
|
||||||
|
case 30: statName = "Haste Rating"; break;
|
||||||
|
case 31: statName = "Hit Rating"; break;
|
||||||
|
case 32: statName = "Crit Rating"; break;
|
||||||
|
case 35: statName = "Resilience"; break;
|
||||||
|
case 36: statName = "Haste Rating"; break;
|
||||||
|
case 37: statName = "Expertise Rating"; break;
|
||||||
|
case 38: statName = "Attack Power"; break;
|
||||||
|
case 39: statName = "Ranged Attack Power"; break;
|
||||||
|
case 41: statName = "Healing Power"; break;
|
||||||
|
case 42: statName = "Spell Damage"; break;
|
||||||
|
case 43: statName = "Mana per 5 sec"; break;
|
||||||
|
case 44: statName = "Armor Penetration"; break;
|
||||||
|
case 45: statName = "Spell Power"; break;
|
||||||
|
case 46: statName = "Health per 5 sec"; break;
|
||||||
|
case 47: statName = "Spell Penetration"; break;
|
||||||
|
case 48: statName = "Block Value"; break;
|
||||||
|
default: statName = nullptr; break;
|
||||||
|
}
|
||||||
|
char buf[64];
|
||||||
|
if (statName) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%+d %s", es.statValue, statName);
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%+d (stat %u)", es.statValue, es.statType);
|
||||||
|
}
|
||||||
|
ImGui::TextColored(green, "%s", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.requiredLevel > 1) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Requires Level %u", item.requiredLevel);
|
||||||
|
}
|
||||||
|
if (item.maxDurability > 0) {
|
||||||
|
float durPct = static_cast<float>(item.curDurability) / static_cast<float>(item.maxDurability);
|
||||||
|
ImVec4 durColor;
|
||||||
|
if (durPct > 0.5f) durColor = ImVec4(0.1f, 1.0f, 0.1f, 1.0f); // green
|
||||||
|
else if (durPct > 0.25f) durColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // yellow
|
||||||
|
else durColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); // red
|
||||||
|
ImGui::TextColored(durColor, "Durability %u / %u",
|
||||||
|
item.curDurability, item.maxDurability);
|
||||||
|
}
|
||||||
|
// Item spell effects (Use/Equip/Chance on Hit)
|
||||||
|
if (gameHandler_) {
|
||||||
|
auto* info = gameHandler_->getItemInfo(item.itemId);
|
||||||
|
if (info) {
|
||||||
|
for (const auto& sp : info->spells) {
|
||||||
|
if (sp.spellId == 0) continue;
|
||||||
|
const char* trigger = nullptr;
|
||||||
|
switch (sp.spellTrigger) {
|
||||||
|
case 0: trigger = "Use"; break;
|
||||||
|
case 1: trigger = "Equip"; break;
|
||||||
|
case 2: trigger = "Chance on Hit"; break;
|
||||||
|
case 6: trigger = "Soulstone"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (!trigger) continue;
|
||||||
|
const std::string& spName = gameHandler_->getSpellName(sp.spellId);
|
||||||
|
if (!spName.empty()) {
|
||||||
|
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f),
|
||||||
|
"%s: %s", trigger, spName.c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f),
|
||||||
|
"%s: Spell #%u", trigger, sp.spellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Begins a Quest" line (shown in yellow-green like the game)
|
||||||
|
if (item.startQuestId != 0) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Begins a Quest");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flavor / lore text (italic yellow in WoW, just yellow here)
|
||||||
|
if (!item.description.empty()) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 0.9f), "\"%s\"", item.description.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
if (item.sellPrice > 0) {
|
if (item.sellPrice > 0) {
|
||||||
uint32_t g = item.sellPrice / 10000;
|
uint32_t g = item.sellPrice / 10000;
|
||||||
uint32_t s = (item.sellPrice / 100) % 100;
|
uint32_t s = (item.sellPrice / 100) % 100;
|
||||||
|
|
@ -1824,23 +1947,79 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
||||||
}
|
}
|
||||||
ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
||||||
|
|
||||||
if (isWeaponInventoryType(eq->item.inventoryType) &&
|
// Helper: render a numeric stat diff line
|
||||||
eq->item.damageMax > 0.0f && eq->item.delayMs > 0) {
|
auto showDiff = [](const char* label, float newVal, float eqVal) {
|
||||||
float speed = static_cast<float>(eq->item.delayMs) / 1000.0f;
|
if (newVal == 0.0f && eqVal == 0.0f) return;
|
||||||
float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed;
|
float diff = newVal - eqVal;
|
||||||
ImGui::Text("%.1f DPS", dps);
|
char buf[128];
|
||||||
|
if (diff > 0.0f) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%s: %.0f (▲%.0f)", label, newVal, diff);
|
||||||
|
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "%s", buf);
|
||||||
|
} else if (diff < 0.0f) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%s: %.0f (▼%.0f)", label, newVal, -diff);
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", buf);
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%s: %.0f", label, newVal);
|
||||||
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", buf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DPS comparison for weapons
|
||||||
|
if (isWeaponInventoryType(item.inventoryType) && isWeaponInventoryType(eq->item.inventoryType)) {
|
||||||
|
float newDps = 0.0f, eqDps = 0.0f;
|
||||||
|
if (item.damageMax > 0.0f && item.delayMs > 0)
|
||||||
|
newDps = ((item.damageMin + item.damageMax) * 0.5f) / (item.delayMs / 1000.0f);
|
||||||
|
if (eq->item.damageMax > 0.0f && eq->item.delayMs > 0)
|
||||||
|
eqDps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / (eq->item.delayMs / 1000.0f);
|
||||||
|
showDiff("DPS", newDps, eqDps);
|
||||||
}
|
}
|
||||||
if (eq->item.armor > 0) {
|
|
||||||
ImGui::Text("%d Armor", eq->item.armor);
|
// Armor
|
||||||
|
showDiff("Armor", static_cast<float>(item.armor), static_cast<float>(eq->item.armor));
|
||||||
|
|
||||||
|
// Primary stats
|
||||||
|
showDiff("Str", static_cast<float>(item.strength), static_cast<float>(eq->item.strength));
|
||||||
|
showDiff("Agi", static_cast<float>(item.agility), static_cast<float>(eq->item.agility));
|
||||||
|
showDiff("Sta", static_cast<float>(item.stamina), static_cast<float>(eq->item.stamina));
|
||||||
|
showDiff("Int", static_cast<float>(item.intellect), static_cast<float>(eq->item.intellect));
|
||||||
|
showDiff("Spi", static_cast<float>(item.spirit), static_cast<float>(eq->item.spirit));
|
||||||
|
|
||||||
|
// Extra stats diff — union of stat types from both items
|
||||||
|
auto findExtraStat = [](const game::ItemDef& it, uint32_t type) -> int32_t {
|
||||||
|
for (const auto& es : it.extraStats)
|
||||||
|
if (es.statType == type) return es.statValue;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
// Collect all extra stat types
|
||||||
|
std::vector<uint32_t> allTypes;
|
||||||
|
for (const auto& es : item.extraStats) allTypes.push_back(es.statType);
|
||||||
|
for (const auto& es : eq->item.extraStats) {
|
||||||
|
bool found = false;
|
||||||
|
for (uint32_t t : allTypes) if (t == es.statType) { found = true; break; }
|
||||||
|
if (!found) allTypes.push_back(es.statType);
|
||||||
}
|
}
|
||||||
std::string eqBonusLine;
|
for (uint32_t t : allTypes) {
|
||||||
appendBonus(eqBonusLine, eq->item.strength, "Str");
|
int32_t nv = findExtraStat(item, t);
|
||||||
appendBonus(eqBonusLine, eq->item.agility, "Agi");
|
int32_t ev = findExtraStat(eq->item, t);
|
||||||
appendBonus(eqBonusLine, eq->item.stamina, "Sta");
|
// Find a label for this stat type
|
||||||
appendBonus(eqBonusLine, eq->item.intellect, "Int");
|
const char* lbl = nullptr;
|
||||||
appendBonus(eqBonusLine, eq->item.spirit, "Spi");
|
switch (t) {
|
||||||
if (!eqBonusLine.empty()) {
|
case 31: lbl = "Hit"; break;
|
||||||
ImGui::TextColored(green, "%s", eqBonusLine.c_str());
|
case 32: lbl = "Crit"; break;
|
||||||
|
case 35: lbl = "Resilience"; break;
|
||||||
|
case 36: lbl = "Haste"; break;
|
||||||
|
case 37: lbl = "Expertise"; break;
|
||||||
|
case 38: lbl = "Attack Power"; break;
|
||||||
|
case 39: lbl = "Ranged AP"; break;
|
||||||
|
case 43: lbl = "MP5"; break;
|
||||||
|
case 44: lbl = "Armor Pen"; break;
|
||||||
|
case 45: lbl = "Spell Power"; break;
|
||||||
|
case 46: lbl = "HP5"; break;
|
||||||
|
case 48: lbl = "Block Value"; break;
|
||||||
|
default: lbl = nullptr; break;
|
||||||
|
}
|
||||||
|
if (!lbl) continue;
|
||||||
|
showDiff(lbl, static_cast<float>(nv), static_cast<float>(ev));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,18 +45,66 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t fieldCount = dbc->getFieldCount();
|
uint32_t fieldCount = dbc->getFieldCount();
|
||||||
if (fieldCount < 154) {
|
// Classic 1.12 Spell.dbc has 148 fields (Tooltip at index 147), TBC has ~220+ (SchoolMask at 215), WotLK has 234.
|
||||||
LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+");
|
// Require at least 148 fields so all expansions can load spell names/icons via the DBC layout.
|
||||||
|
if (fieldCount < 148) {
|
||||||
|
LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, too few to load");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||||
|
|
||||||
|
// Load SpellCastTimes.dbc: field 0=ID, field 1=Base(ms), field 2=PerLevel, field 3=Minimum
|
||||||
|
std::unordered_map<uint32_t, uint32_t> castTimeMap; // index → base ms
|
||||||
|
auto castTimeDbc = assetManager->loadDBC("SpellCastTimes.dbc");
|
||||||
|
if (castTimeDbc && castTimeDbc->isLoaded()) {
|
||||||
|
for (uint32_t i = 0; i < castTimeDbc->getRecordCount(); ++i) {
|
||||||
|
uint32_t id = castTimeDbc->getUInt32(i, 0);
|
||||||
|
int32_t base = static_cast<int32_t>(castTimeDbc->getUInt32(i, 1));
|
||||||
|
if (id > 0 && base > 0)
|
||||||
|
castTimeMap[id] = static_cast<uint32_t>(base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load SpellRange.dbc. Field layout differs by expansion:
|
||||||
|
// Classic 1.12: 0=ID, 1=MinRange, 2=MaxRange, 3=Flags, 4+=strings
|
||||||
|
// TBC / WotLK: 0=ID, 1=MinRangeFriendly, 2=MinRangeHostile,
|
||||||
|
// 3=MaxRangeFriendly, 4=MaxRangeHostile, 5=Flags, 6+=strings
|
||||||
|
// The correct field is declared in each expansion's dbc_layouts.json.
|
||||||
|
uint32_t spellRangeMaxField = 4; // WotLK / TBC default: MaxRangeHostile
|
||||||
|
const auto* spellRangeL = pipeline::getActiveDBCLayout()
|
||||||
|
? pipeline::getActiveDBCLayout()->getLayout("SpellRange")
|
||||||
|
: nullptr;
|
||||||
|
if (spellRangeL) {
|
||||||
|
try { spellRangeMaxField = (*spellRangeL)["MaxRange"]; } catch (...) {}
|
||||||
|
}
|
||||||
|
std::unordered_map<uint32_t, float> rangeMap; // index → max yards
|
||||||
|
auto rangeDbc = assetManager->loadDBC("SpellRange.dbc");
|
||||||
|
if (rangeDbc && rangeDbc->isLoaded()) {
|
||||||
|
uint32_t rangeFieldCount = rangeDbc->getFieldCount();
|
||||||
|
if (rangeFieldCount > spellRangeMaxField) {
|
||||||
|
for (uint32_t i = 0; i < rangeDbc->getRecordCount(); ++i) {
|
||||||
|
uint32_t id = rangeDbc->getUInt32(i, 0);
|
||||||
|
float maxRange = rangeDbc->getFloat(i, spellRangeMaxField);
|
||||||
|
if (id > 0 && maxRange > 0.0f)
|
||||||
|
rangeMap[id] = maxRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// schoolField / isSchoolEnum are declared before the lambda so the WotLK fallback path
|
||||||
|
// can override them before the second tryLoad call.
|
||||||
|
uint32_t schoolField_ = UINT32_MAX;
|
||||||
|
bool isSchoolEnum_ = false;
|
||||||
|
|
||||||
auto tryLoad = [&](uint32_t idField, uint32_t attrField, uint32_t iconField,
|
auto tryLoad = [&](uint32_t idField, uint32_t attrField, uint32_t iconField,
|
||||||
uint32_t nameField, uint32_t rankField, uint32_t tooltipField,
|
uint32_t nameField, uint32_t rankField, uint32_t tooltipField,
|
||||||
|
uint32_t powerTypeField, uint32_t manaCostField,
|
||||||
|
uint32_t castTimeIndexField, uint32_t rangeIndexField,
|
||||||
const char* label) {
|
const char* label) {
|
||||||
spellData.clear();
|
spellData.clear();
|
||||||
uint32_t count = dbc->getRecordCount();
|
uint32_t count = dbc->getRecordCount();
|
||||||
|
const uint32_t fc = dbc->getFieldCount();
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
uint32_t spellId = dbc->getUInt32(i, idField);
|
uint32_t spellId = dbc->getUInt32(i, idField);
|
||||||
if (spellId == 0) continue;
|
if (spellId == 0) continue;
|
||||||
|
|
@ -66,8 +114,31 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||||
info.attributes = dbc->getUInt32(i, attrField);
|
info.attributes = dbc->getUInt32(i, attrField);
|
||||||
info.iconId = dbc->getUInt32(i, iconField);
|
info.iconId = dbc->getUInt32(i, iconField);
|
||||||
info.name = dbc->getString(i, nameField);
|
info.name = dbc->getString(i, nameField);
|
||||||
info.rank = dbc->getString(i, rankField);
|
if (rankField < fc) info.rank = dbc->getString(i, rankField);
|
||||||
info.description = dbc->getString(i, tooltipField);
|
if (tooltipField < fc) info.description = dbc->getString(i, tooltipField);
|
||||||
|
// Optional fields: only read if field index is valid for this DBC version
|
||||||
|
if (powerTypeField < fc) info.powerType = dbc->getUInt32(i, powerTypeField);
|
||||||
|
if (manaCostField < fc) info.manaCost = dbc->getUInt32(i, manaCostField);
|
||||||
|
if (castTimeIndexField < fc) {
|
||||||
|
uint32_t ctIdx = dbc->getUInt32(i, castTimeIndexField);
|
||||||
|
if (ctIdx > 0) {
|
||||||
|
auto ctIt = castTimeMap.find(ctIdx);
|
||||||
|
if (ctIt != castTimeMap.end()) info.castTimeMs = ctIt->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rangeIndexField < fc) {
|
||||||
|
uint32_t rangeIdx = dbc->getUInt32(i, rangeIndexField);
|
||||||
|
if (rangeIdx > 0) {
|
||||||
|
auto rangeIt = rangeMap.find(rangeIdx);
|
||||||
|
if (rangeIt != rangeMap.end()) info.rangeIndex = static_cast<uint32_t>(rangeIt->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schoolField_ < fc) {
|
||||||
|
uint32_t raw = dbc->getUInt32(i, schoolField_);
|
||||||
|
// Classic/Turtle use a 0-6 school enum; TBC/WotLK use a bitmask.
|
||||||
|
// enum→mask: schoolEnum N maps to bit (1u << N), e.g. 0→1 (physical), 4→16 (frost).
|
||||||
|
info.schoolMask = isSchoolEnum_ ? (raw <= 6 ? (1u << raw) : 0u) : raw;
|
||||||
|
}
|
||||||
|
|
||||||
if (!info.name.empty()) {
|
if (!info.name.empty()) {
|
||||||
spellData[spellId] = std::move(info);
|
spellData[spellId] = std::move(info);
|
||||||
|
|
@ -77,21 +148,51 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (spellL) {
|
if (spellL) {
|
||||||
uint32_t tooltipField = 139;
|
// Default to UINT32_MAX for optional fields; tryLoad will skip them if >= fieldCount.
|
||||||
// Try to get Tooltip field from layout, fall back to 139
|
// Avoids reading wrong data from expansion DBCs that lack these fields (e.g. Classic/TBC).
|
||||||
try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {}
|
uint32_t tooltipField = UINT32_MAX;
|
||||||
|
uint32_t powerTypeField = UINT32_MAX;
|
||||||
|
uint32_t manaCostField = UINT32_MAX;
|
||||||
|
uint32_t castTimeIdxField = UINT32_MAX;
|
||||||
|
uint32_t rangeIdxField = UINT32_MAX;
|
||||||
|
try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {}
|
||||||
|
try { powerTypeField = (*spellL)["PowerType"]; } catch (...) {}
|
||||||
|
try { manaCostField = (*spellL)["ManaCost"]; } catch (...) {}
|
||||||
|
try { castTimeIdxField = (*spellL)["CastingTimeIndex"]; } catch (...) {}
|
||||||
|
try { rangeIdxField = (*spellL)["RangeIndex"]; } catch (...) {}
|
||||||
|
// Try SchoolMask (TBC/WotLK bitmask) then SchoolEnum (Classic/Turtle 0-6 value)
|
||||||
|
schoolField_ = UINT32_MAX;
|
||||||
|
isSchoolEnum_ = false;
|
||||||
|
try { schoolField_ = (*spellL)["SchoolMask"]; } catch (...) {}
|
||||||
|
if (schoolField_ == UINT32_MAX) {
|
||||||
|
try { schoolField_ = (*spellL)["SchoolEnum"]; isSchoolEnum_ = true; } catch (...) {}
|
||||||
|
}
|
||||||
tryLoad((*spellL)["ID"], (*spellL)["Attributes"], (*spellL)["IconID"],
|
tryLoad((*spellL)["ID"], (*spellL)["Attributes"], (*spellL)["IconID"],
|
||||||
(*spellL)["Name"], (*spellL)["Rank"], tooltipField, "expansion layout");
|
(*spellL)["Name"], (*spellL)["Rank"], tooltipField,
|
||||||
|
powerTypeField, manaCostField, castTimeIdxField, rangeIdxField,
|
||||||
|
"expansion layout");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spellData.empty() && fieldCount >= 200) {
|
if (spellData.empty() && fieldCount >= 200) {
|
||||||
LOG_INFO("Spellbook: Retrying with WotLK field indices (DBC has ", fieldCount, " fields)");
|
LOG_INFO("Spellbook: Retrying with WotLK field indices (DBC has ", fieldCount, " fields)");
|
||||||
tryLoad(0, 4, 133, 136, 153, 139, "WotLK fallback");
|
// WotLK Spell.dbc field indices (verified against 3.3.5a schema); SchoolMask at field 225
|
||||||
|
schoolField_ = 225;
|
||||||
|
isSchoolEnum_ = false;
|
||||||
|
tryLoad(0, 4, 133, 136, 153, 139, 14, 39, 47, 49, "WotLK fallback");
|
||||||
}
|
}
|
||||||
|
|
||||||
dbcLoaded = !spellData.empty();
|
dbcLoaded = !spellData.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SpellbookScreen::renderSpellInfoTooltip(uint32_t spellId, game::GameHandler& gameHandler,
|
||||||
|
pipeline::AssetManager* assetManager) {
|
||||||
|
if (!dbcLoadAttempted) loadSpellDBC(assetManager);
|
||||||
|
const SpellInfo* info = getSpellInfo(spellId);
|
||||||
|
if (!info) return false;
|
||||||
|
renderSpellTooltip(info, gameHandler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string SpellbookScreen::lookupSpellName(uint32_t spellId, pipeline::AssetManager* assetManager) {
|
std::string SpellbookScreen::lookupSpellName(uint32_t spellId, pipeline::AssetManager* assetManager) {
|
||||||
if (!dbcLoadAttempted) {
|
if (!dbcLoadAttempted) {
|
||||||
loadSpellDBC(assetManager);
|
loadSpellDBC(assetManager);
|
||||||
|
|
@ -363,6 +464,81 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle
|
||||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Passive");
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Passive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spell school — only show for non-physical schools (physical is the default/implicit)
|
||||||
|
if (info->schoolMask != 0 && info->schoolMask != 1 /*physical*/) {
|
||||||
|
struct SchoolEntry { uint32_t mask; const char* name; ImVec4 color; };
|
||||||
|
static constexpr SchoolEntry kSchools[] = {
|
||||||
|
{ 2, "Holy", { 1.0f, 1.0f, 0.6f, 1.0f } },
|
||||||
|
{ 4, "Fire", { 1.0f, 0.5f, 0.1f, 1.0f } },
|
||||||
|
{ 8, "Nature", { 0.4f, 0.9f, 0.3f, 1.0f } },
|
||||||
|
{ 16, "Frost", { 0.5f, 0.8f, 1.0f, 1.0f } },
|
||||||
|
{ 32, "Shadow", { 0.7f, 0.4f, 1.0f, 1.0f } },
|
||||||
|
{ 64, "Arcane", { 0.9f, 0.5f, 1.0f, 1.0f } },
|
||||||
|
};
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& s : kSchools) {
|
||||||
|
if (info->schoolMask & s.mask) {
|
||||||
|
if (!first) ImGui::SameLine(0, 0);
|
||||||
|
if (first) {
|
||||||
|
ImGui::TextColored(s.color, "%s", s.name);
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
ImGui::SameLine(0, 2);
|
||||||
|
ImGui::TextColored(s.color, "/%s", s.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource cost + cast time on same row (WoW style)
|
||||||
|
if (!info->isPassive()) {
|
||||||
|
// Left: resource cost
|
||||||
|
char costBuf[64] = "";
|
||||||
|
if (info->manaCost > 0) {
|
||||||
|
const char* powerName = "Mana";
|
||||||
|
switch (info->powerType) {
|
||||||
|
case 1: powerName = "Rage"; break;
|
||||||
|
case 3: powerName = "Energy"; break;
|
||||||
|
case 4: powerName = "Focus"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
std::snprintf(costBuf, sizeof(costBuf), "%u %s", info->manaCost, powerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right: cast time
|
||||||
|
char castBuf[32] = "";
|
||||||
|
if (info->castTimeMs == 0) {
|
||||||
|
std::snprintf(castBuf, sizeof(castBuf), "Instant cast");
|
||||||
|
} else {
|
||||||
|
float secs = info->castTimeMs / 1000.0f;
|
||||||
|
std::snprintf(castBuf, sizeof(castBuf), "%.1f sec cast", secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (costBuf[0] || castBuf[0]) {
|
||||||
|
float wrapW = 320.0f;
|
||||||
|
if (costBuf[0] && castBuf[0]) {
|
||||||
|
float castW = ImGui::CalcTextSize(castBuf).x;
|
||||||
|
ImGui::Text("%s", costBuf);
|
||||||
|
ImGui::SameLine(wrapW - castW);
|
||||||
|
ImGui::Text("%s", castBuf);
|
||||||
|
} else if (castBuf[0]) {
|
||||||
|
ImGui::Text("%s", castBuf);
|
||||||
|
} else {
|
||||||
|
ImGui::Text("%s", costBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range
|
||||||
|
if (info->rangeIndex > 0) {
|
||||||
|
char rangeBuf[32];
|
||||||
|
if (info->rangeIndex <= 5)
|
||||||
|
std::snprintf(rangeBuf, sizeof(rangeBuf), "Melee range");
|
||||||
|
else
|
||||||
|
std::snprintf(rangeBuf, sizeof(rangeBuf), "%u yd range", info->rangeIndex);
|
||||||
|
ImGui::Text("%s", rangeBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cooldown if active
|
// Cooldown if active
|
||||||
float cd = gameHandler.getSpellCooldown(info->spellId);
|
float cd = gameHandler.getSpellCooldown(info->spellId);
|
||||||
if (cd > 0.0f) {
|
if (cd > 0.0f) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue