Compare commits

..

No commits in common. "e0346c85df4f532cd7c1bdb9ad48c7824cbcb6e3" and "87cb293297e098e095fac95af5848757d4d2bd8e" have entirely different histories.

15 changed files with 236 additions and 2091 deletions

View file

@ -1,6 +1,6 @@
# Project Status
**Last updated**: 2026-03-18
**Last updated**: 2026-03-11
## What This Repo Is
@ -35,9 +35,9 @@ Implemented (working in normal use):
In progress / known gaps:
- Transports: M2 transports (trams) working with position-delta riding; WMO transports (ships, zeppelins) working with path following; some edge cases remain
- Visual edge cases: some M2/WMO rendering gaps (some particle effects)
- Visual edge cases: some M2/WMO rendering gaps (character shin mesh, some particle effects)
- Lava steam particles: sparse in some areas (tuning opportunity)
- Water refraction: enabled by default; srcAccessMask barrier fix (2026-03-18) resolved prior VK_ERROR_DEVICE_LOST on AMD/Mali GPUs
- Water refraction: implemented but disabled by default (can cause VK_ERROR_DEVICE_LOST on some GPUs); currently requires FSR to be active
## Where To Look

View file

@ -375,10 +375,6 @@ public:
std::shared_ptr<Entity> getFocus() const;
bool hasFocus() const { return focusGuid != 0; }
// Mouseover targeting — set each frame by the nameplate renderer
void setMouseoverGuid(uint64_t guid) { mouseoverGuid_ = guid; }
uint64_t getMouseoverGuid() const { return mouseoverGuid_; }
// Advanced targeting
void targetLastTarget();
void targetEnemy(bool reverse = false);
@ -746,8 +742,6 @@ public:
}
// Send CMSG_PET_ACTION to issue a pet command
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
// Toggle autocast for a pet spell via CMSG_PET_SPELL_AUTOCAST
void togglePetSpellAutocast(uint32_t spellId);
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells; }
// ---- Pet Stable ----
@ -809,9 +803,6 @@ public:
int getCraftQueueRemaining() const { return craftQueueRemaining_; }
uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; }
// 400ms spell-queue window: next spell to cast when current finishes
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
// Unit cast state (tracked per GUID for target frame + boss frames)
struct UnitCastState {
bool casting = false;
@ -894,10 +885,6 @@ public:
const std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() const { return actionBar; }
void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id);
// Client-side macro text storage (server sends only macro index; text is stored locally)
const std::string& getMacroText(uint32_t macroId) const;
void setMacroText(uint32_t macroId, const std::string& text);
void saveCharacterConfig();
void loadCharacterConfig();
static std::string getCharacterConfigDir();
@ -944,10 +931,6 @@ public:
using SpellCastAnimCallback = std::function<void(uint64_t guid, bool start, bool isChannel)>;
void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); }
// Fired when the player's own spell cast fails (spellId of the failed spell).
using SpellCastFailedCallback = std::function<void(uint32_t spellId)>;
void setSpellCastFailedCallback(SpellCastFailedCallback cb) { spellCastFailedCallback_ = std::move(cb); }
// Unit animation hint: signal jump (animId=38) for other players/NPCs
using UnitAnimHintCallback = std::function<void(uint64_t guid, uint32_t animId)>;
void setUnitAnimHintCallback(UnitAnimHintCallback cb) { unitAnimHintCallback_ = std::move(cb); }
@ -1188,10 +1171,6 @@ public:
bool isPlayerGhost() const { return releasedSpirit_; }
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
bool showResurrectDialog() const { return resurrectRequestPending_; }
/** True when SMSG_PRE_RESURRECT arrived — Reincarnation/Twisting Nether available. */
bool canSelfRes() const { return selfResAvailable_; }
/** Send CMSG_SELF_RES to use Reincarnation / Twisting Nether. */
void useSelfRes();
const std::string& getResurrectCasterName() const { return resurrectCasterName_; }
bool showTalentWipeConfirmDialog() const { return talentWipePending_; }
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
@ -1204,8 +1183,6 @@ public:
void cancelPetUnlearn() { petUnlearnPending_ = false; }
/** True when ghost is within 40 yards of corpse position (same map). */
bool canReclaimCorpse() const;
/** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */
float getCorpseReclaimDelaySec() const;
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
float getCorpseDistance() const {
if (corpseMapId_ == 0 || currentMapId_ != corpseMapId_) return -1.0f;
@ -1901,7 +1878,6 @@ public:
bool isMounted() const { return currentMountDisplayId_ != 0; }
bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
bool isHostileFactionPublic(uint32_t factionTemplateId) const { return isHostileFaction(factionTemplateId); }
float getServerRunSpeed() const { return serverRunSpeed_; }
float getServerWalkSpeed() const { return serverWalkSpeed_; }
float getServerSwimSpeed() const { return serverSwimSpeed_; }
@ -2007,9 +1983,6 @@ public:
void autoEquipItemInBag(int bagIndex, int slotIndex);
void useItemBySlot(int backpackIndex);
void useItemInBag(int bagIndex, int slotIndex);
// CMSG_OPEN_ITEM — for locked containers (lockboxes); server checks keyring automatically
void openItemBySlot(int backpackIndex);
void openItemInBag(int bagIndex, int slotIndex);
void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1);
void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot);
void swapBagSlots(int srcBagIndex, int dstBagIndex);
@ -2137,22 +2110,6 @@ public:
if (index < 0 || index >= static_cast<int>(backpackSlotGuids_.size())) return 0;
return backpackSlotGuids_[index];
}
uint64_t getEquipSlotGuid(int slot) const {
if (slot < 0 || slot >= static_cast<int>(equipSlotGuids_.size())) return 0;
return equipSlotGuids_[slot];
}
// Returns the permanent and temporary enchant IDs for an item by GUID (0 if unknown).
std::pair<uint32_t, uint32_t> getItemEnchantIds(uint64_t guid) const {
auto it = onlineItems_.find(guid);
if (it == onlineItems_.end()) return {0, 0};
return {it->second.permanentEnchantId, it->second.temporaryEnchantId};
}
// Returns the socket gem enchant IDs (3 slots; 0 = empty socket) for an item by GUID.
std::array<uint32_t, 3> getItemSocketEnchantIds(uint64_t guid) const {
auto it = onlineItems_.find(guid);
if (it == onlineItems_.end()) return {};
return it->second.socketEnchantIds;
}
uint64_t getVendorGuid() const { return currentVendorItems.vendorGuid; }
/**
@ -2569,7 +2526,6 @@ private:
uint64_t targetGuid = 0;
uint64_t focusGuid = 0; // Focus target
uint64_t lastTargetGuid = 0; // Previous target
uint64_t mouseoverGuid_ = 0; // Set each frame by nameplate renderer
std::vector<uint64_t> tabCycleList;
int tabCycleIndex = -1;
bool tabCycleStale = true;
@ -2649,21 +2605,10 @@ private:
uint32_t stackCount = 1;
uint32_t curDurability = 0;
uint32_t maxDurability = 0;
uint32_t permanentEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 0 (enchanting)
uint32_t temporaryEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 1 (sharpening stones, poisons)
std::array<uint32_t, 3> socketEnchantIds{}; // ITEM_ENCHANTMENT_SLOT 2-4 (gems)
};
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
std::unordered_set<uint32_t> pendingItemQueries_;
// Deferred SMSG_ITEM_PUSH_RESULT notifications for items whose info wasn't
// cached at arrival time; emitted once the query response arrives.
struct PendingItemPushNotif {
uint32_t itemId = 0;
uint32_t count = 1;
};
std::vector<PendingItemPushNotif> pendingItemPushNotifs_;
std::array<uint64_t, 23> equipSlotGuids_{};
std::array<uint64_t, 16> backpackSlotGuids_{};
std::array<uint64_t, 32> keyringSlotGuids_{};
@ -2774,9 +2719,6 @@ private:
// Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes
uint32_t craftQueueSpellId_ = 0;
int craftQueueRemaining_ = 0;
// Spell queue: next spell to cast within the 400ms window before current cast ends
uint32_t queuedSpellId_ = 0;
uint64_t queuedSpellTarget_ = 0;
// Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START)
std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
uint64_t pendingGameObjectInteractGuid_ = 0;
@ -2808,7 +2750,6 @@ private:
float castTimeTotal = 0.0f;
std::array<ActionBarSlot, ACTION_BAR_SLOTS> actionBar{};
std::unordered_map<uint32_t, std::string> macros_; // client-side macro text (persisted in char config)
std::vector<AuraSlot> playerAuras;
std::vector<AuraSlot> targetAuras;
std::unordered_map<uint64_t, std::vector<AuraSlot>> unitAurasCache_; // per-unit aura cache
@ -3321,7 +3262,6 @@ private:
MeleeSwingCallback meleeSwingCallback_;
uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing
SpellCastAnimCallback spellCastAnimCallback_;
SpellCastFailedCallback spellCastFailedCallback_;
UnitAnimHintCallback unitAnimHintCallback_;
UnitMoveFlagsCallback unitMoveFlagsCallback_;
NpcSwingCallback npcSwingCallback_;
@ -3358,9 +3298,6 @@ private:
uint32_t corpseMapId_ = 0;
float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f;
uint64_t corpseGuid_ = 0;
// Absolute time (ms since epoch) when PvP corpse-reclaim delay expires.
// 0 means no active delay (reclaim allowed immediately upon proximity).
uint64_t corpseReclaimAvailableMs_ = 0;
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
std::array<RuneSlot, 6> playerRunes_ = [] {
std::array<RuneSlot, 6> r{};
@ -3372,7 +3309,6 @@ private:
uint64_t pendingSpiritHealerGuid_ = 0;
bool resurrectPending_ = false;
bool resurrectRequestPending_ = false;
bool selfResAvailable_ = false; // SMSG_PRE_RESURRECT received — Reincarnation/Twisting Nether
// ---- Talent wipe confirm dialog ----
bool talentWipePending_ = false;
uint64_t talentWipeNpcGuid_ = 0;

View file

@ -125,10 +125,6 @@ public:
int findFreeBackpackSlot() const;
bool addItem(const ItemDef& item);
// Sort all bag slots (backpack + equip bags) by quality desc → itemId asc → stackCount desc.
// Purely client-side: reorders the local inventory struct without server interaction.
void sortBags();
// Test data
void populateTestItems();

View file

@ -2027,12 +2027,6 @@ public:
static network::Packet build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0);
};
/** CMSG_OPEN_ITEM packet builder (for locked containers / lockboxes) */
class OpenItemPacket {
public:
static network::Packet build(uint8_t bagIndex, uint8_t slotIndex);
};
/** CMSG_AUTOEQUIP_ITEM packet builder */
class AutoEquipItemPacket {
public:

View file

@ -55,13 +55,6 @@ private:
std::vector<std::string> chatSentHistory_;
int chatHistoryIdx_ = -1; // -1 = not browsing history
// Set to true by /stopmacro; checked in executeMacroText to halt remaining commands.
bool macroStopped_ = false;
// Action bar error-flash: spellId → wall-clock time (seconds) when the flash ends.
// Populated by the SpellCastFailedCallback; queried during action bar button rendering.
std::unordered_map<uint32_t, float> actionFlashEndTimes_;
// Tab-completion state for slash commands
std::string chatTabPrefix_; // prefix captured on first Tab press
std::vector<std::string> chatTabMatches_; // matching command list
@ -113,8 +106,6 @@ private:
std::vector<UIErrorEntry> uiErrors_;
bool uiErrorCallbackSet_ = false;
static constexpr float kUIErrorLifetime = 2.5f;
bool castFailedCallbackSet_ = false;
static constexpr float kActionFlashDuration = 0.5f; // seconds for error-red overlay to fade
// Reputation change toast: brief colored slide-in below minimap
struct RepToastEntry { std::string factionName; int32_t delta = 0; int32_t standing = 0; float age = 0.0f; };
@ -179,7 +170,7 @@ private:
int pendingResIndex = 0;
bool pendingShadows = true;
float pendingShadowDistance = 300.0f;
bool pendingWaterRefraction = true;
bool pendingWaterRefraction = false;
int pendingBrightness = 50; // 0-100, maps to 0.0-2.0 (50 = 1.0 default)
int pendingMasterVolume = 100;
int pendingMusicVolume = 30;
@ -210,10 +201,6 @@ private:
// Keybinding customization
int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index
bool awaitingKeyPress = false;
// Macro editor popup state
uint32_t macroEditorId_ = 0; // macro index being edited
bool macroEditorOpen_ = false; // deferred OpenPopup flag
char macroEditorBuf_[256] = {}; // edit buffer
bool pendingUseOriginalSoundtrack = true;
bool pendingShowActionBar2 = true; // Show second action bar above main bar
float pendingActionBarScale = 1.0f; // Multiplier for action bar slot size (0.51.5)
@ -287,7 +274,6 @@ private:
* Send chat message
*/
void sendChatMessage(game::GameHandler& gameHandler);
void executeMacroText(game::GameHandler& gameHandler, const std::string& macroText);
/**
* Get chat type name

View file

@ -99,7 +99,7 @@ private:
std::unordered_map<uint32_t, VkDescriptorSet> iconCache_;
public:
VkDescriptorSet getItemIcon(uint32_t displayInfoId);
void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr, uint64_t itemGuid = 0);
void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr);
private:
// Character model preview
@ -161,7 +161,7 @@ private:
SlotKind kind, int backpackIndex,
game::EquipSlot equipSlot,
int bagIndex = -1, int bagSlotIndex = -1);
void renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory = nullptr, uint64_t itemGuid = 0);
void renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory = nullptr);
// Held item helpers
void pickupFromBackpack(game::Inventory& inv, int index);

View file

@ -6908,10 +6908,6 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
};
// --- Geosets ---
// Mirror the same group-range logic as CharacterPreview::applyEquipment to
// keep other-player rendering consistent with the local character preview.
// Group 4 (4xx) = forearms/gloves, 5 (5xx) = shins/boots, 8 (8xx) = wrists/sleeves,
// 13 (13xx) = legs/trousers. Missing defaults caused the shin-mesh gap (status.md).
std::unordered_set<uint16_t> geosets;
// Body parts (group 0: IDs 0-99, some models use up to 27)
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
@ -6919,6 +6915,8 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
uint8_t hairStyleId = static_cast<uint8_t>((st.appearanceBytes >> 16) & 0xFF);
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
geosets.insert(static_cast<uint16_t>(200 + st.facialFeatures + 1));
geosets.insert(401); // Body joint patches (knees)
geosets.insert(402); // Body joint patches (elbows)
geosets.insert(701); // Ears
geosets.insert(902); // Kneepads
geosets.insert(2002); // Bare feet mesh
@ -6926,47 +6924,39 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
const uint32_t geosetGroup1Field = idiL ? (*idiL)["GeosetGroup1"] : 7;
const uint32_t geosetGroup3Field = idiL ? (*idiL)["GeosetGroup3"] : 9;
// Per-group defaults — overridden below when equipment provides a geoset value.
uint16_t geosetGloves = 401; // Bare forearms (group 4, no gloves)
uint16_t geosetBoots = 502; // Bare shins (group 5, no boots)
uint16_t geosetSleeves = 801; // Bare wrists (group 8, no chest/sleeves)
uint16_t geosetPants = 1301; // Bare legs (group 13, no leggings)
// Chest/Shirt/Robe (invType 4,5,20) → wrist/sleeve group 8
// Chest/Shirt/Robe (invType 4,5,20)
{
uint32_t did = findDisplayIdByInvType({4, 5, 20});
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
if (gg1 > 0) geosetSleeves = static_cast<uint16_t>(801 + gg1);
// Robe kilt → leg group 13
geosets.insert(static_cast<uint16_t>(gg1 > 0 ? 501 + gg1 : 501));
uint32_t gg3 = getGeosetGroup(did, geosetGroup3Field);
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
if (gg3 > 0) geosets.insert(static_cast<uint16_t>(1301 + gg3));
}
// Legs (invType 7) → leg group 13
// Legs (invType 7)
{
uint32_t did = findDisplayIdByInvType({7});
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
if (gg1 > 0) geosetPants = static_cast<uint16_t>(1301 + gg1);
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
geosets.insert(static_cast<uint16_t>(gg1 > 0 ? 1301 + gg1 : 1301));
}
}
// Feet/Boots (invType 8) → shin group 5
// Feet (invType 8): 401/402 are body patches (always on), 403+ are boot meshes
{
uint32_t did = findDisplayIdByInvType({8});
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
if (gg1 > 0) geosetBoots = static_cast<uint16_t>(501 + gg1);
if (gg1 > 0) geosets.insert(static_cast<uint16_t>(402 + gg1));
}
// Hands/Gloves (invType 10) → forearm group 4
// Hands (invType 10)
{
uint32_t did = findDisplayIdByInvType({10});
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
if (gg1 > 0) geosetGloves = static_cast<uint16_t>(401 + gg1);
geosets.insert(static_cast<uint16_t>(gg1 > 0 ? 301 + gg1 : 301));
}
geosets.insert(geosetGloves);
geosets.insert(geosetBoots);
geosets.insert(geosetSleeves);
geosets.insert(geosetPants);
// Back/Cloak (invType 16)
geosets.insert(hasInvType({16}) ? 1502 : 1501);
// Tabard (invType 19)

View file

@ -1956,22 +1956,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
queryItemInfo(itemId, 0);
if (showInChat) {
std::string itemName = "item #" + std::to_string(itemId);
uint32_t quality = 1; // white default
if (const ItemQueryResponseData* info = getItemInfo(itemId)) {
// Item info already cached — emit immediately.
std::string itemName = info->name.empty() ? ("item #" + std::to_string(itemId)) : info->name;
uint32_t quality = info->quality;
std::string link = buildItemLink(itemId, quality, itemName);
std::string msg = "Received: " + link;
if (count > 1) msg += " x" + std::to_string(count);
addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playLootItem();
}
if (itemLootCallback_) itemLootCallback_(itemId, count, quality, itemName);
} else {
// Item info not yet cached; defer until SMSG_ITEM_QUERY_SINGLE_RESPONSE.
pendingItemPushNotifs_.push_back({itemId, count});
if (!info->name.empty()) itemName = info->name;
quality = info->quality;
}
std::string link = buildItemLink(itemId, quality, itemName);
std::string msg = "Received: " + link;
if (count > 1) msg += " x" + std::to_string(count);
addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playLootItem();
}
if (itemLootCallback_) {
itemLootCallback_(itemId, count, quality, itemName);
}
}
LOG_INFO("Item push: itemId=", itemId, " count=", count,
@ -2252,11 +2252,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
lastInteractedGoGuid_ = 0;
// Cancel craft queue and spell queue on cast failure
// Cancel craft queue on cast failure
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
// Pass player's power type so result 85 says "Not enough rage/energy/etc."
int playerPowerType = -1;
if (auto pe = entityManager.getEntity(playerGuid)) {
@ -2267,7 +2265,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
std::string errMsg = reason ? reason
: ("Spell cast failed (error " + std::to_string(castResult) + ")");
addUIError(errMsg);
if (spellCastFailedCallback_) spellCastFailedCallback_(castResultSpellId);
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.language = ChatLanguage::UNIVERSAL;
@ -2648,31 +2645,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_CORPSE_RECLAIM_DELAY: {
// uint32 delayMs before player can reclaim corpse (PvP deaths)
// uint32 delayMs before player can reclaim corpse
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t delayMs = packet.readUInt32();
auto nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
corpseReclaimAvailableMs_ = nowMs + delayMs;
LOG_INFO("SMSG_CORPSE_RECLAIM_DELAY: ", delayMs, "ms");
uint32_t delaySec = (delayMs + 999) / 1000;
addSystemChatMessage("You can reclaim your corpse in " +
std::to_string(delaySec) + " seconds.");
LOG_DEBUG("SMSG_CORPSE_RECLAIM_DELAY: ", delayMs, "ms");
}
break;
}
case Opcode::SMSG_DEATH_RELEASE_LOC: {
// uint32 mapId + float x + float y + float z
// This is the GRAVEYARD / ghost-spawn position, NOT the actual corpse location.
// The corpse remains at the death position (already cached when health dropped to 0,
// and updated when the corpse object arrives via SMSG_UPDATE_OBJECT).
// Do NOT overwrite corpseX_/Y_/Z_/MapId_ here — that would break canReclaimCorpse()
// by making it check distance to the graveyard instead of the real corpse.
// uint32 mapId + float x + float y + float z — corpse/spirit healer position
if (packet.getSize() - packet.getReadPos() >= 16) {
uint32_t relMapId = packet.readUInt32();
float relX = packet.readFloat();
float relY = packet.readFloat();
float relZ = packet.readFloat();
LOG_INFO("SMSG_DEATH_RELEASE_LOC (graveyard spawn): map=", relMapId,
" x=", relX, " y=", relY, " z=", relZ);
corpseMapId_ = packet.readUInt32();
corpseX_ = packet.readFloat();
corpseY_ = packet.readFloat();
corpseZ_ = packet.readFloat();
LOG_INFO("SMSG_DEATH_RELEASE_LOC: map=", corpseMapId_,
" x=", corpseX_, " y=", corpseY_, " z=", corpseZ_);
}
break;
}
@ -3249,21 +3240,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
break;
case Opcode::SMSG_ATTACKSWING_NOTSTANDING:
case Opcode::SMSG_ATTACKSWING_CANT_ATTACK:
autoAttackOutOfRange_ = false;
autoAttackOutOfRangeTime_ = 0.0f;
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
addSystemChatMessage("You need to stand up to fight.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
break;
case Opcode::SMSG_ATTACKSWING_CANT_ATTACK:
// Target is permanently non-attackable (critter, civilian, already dead, etc.).
// Stop the auto-attack loop so the client doesn't spam the server.
stopAutoAttack();
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
addSystemChatMessage("You can't attack that.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
break;
case Opcode::SMSG_ATTACKERSTATEUPDATE:
handleAttackerStateUpdate(packet);
@ -3368,10 +3347,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
castIsChannel = false;
currentCastSpellId = 0;
lastInteractedGoGuid_ = 0;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* ssm = renderer->getSpellSoundManager()) {
ssm->stopPrecast();
@ -4031,9 +4006,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
worldStateMapId_ = packet.readUInt32();
worldStateZoneId_ = packet.readUInt32();
// WotLK adds areaId (uint32) before count; Classic/TBC/Turtle use the shorter format
// WotLK adds areaId (uint32) before count; detect by checking if payload would be consistent
size_t remaining = packet.getSize() - packet.getReadPos();
bool isWotLKFormat = isActiveExpansion("wotlk");
bool isWotLKFormat = isActiveExpansion("wotlk") || isActiveExpansion("turtle");
if (isWotLKFormat && remaining >= 6) {
packet.readUInt32(); // areaId (WotLK only)
}
@ -4191,10 +4166,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (delayMs == 0) break;
float delaySec = delayMs / 1000.0f;
if (caster == playerGuid) {
if (casting) {
castTimeRemaining += delaySec;
castTimeTotal += delaySec; // keep progress percentage correct
}
if (casting) castTimeRemaining += delaySec;
} else {
auto it = unitCastStates_.find(caster);
if (it != unitCastStates_.end() && it->second.casting) {
@ -4439,10 +4411,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
ActionBarSlot slot;
switch (type) {
case 0x00: slot.type = ActionBarSlot::SPELL; slot.id = id; break;
case 0x01: slot.type = ActionBarSlot::ITEM; slot.id = id; break; // Classic item
case 0x80: slot.type = ActionBarSlot::ITEM; slot.id = id; break; // TBC/WotLK item
case 0x40: slot.type = ActionBarSlot::MACRO; slot.id = id; break; // macro (all expansions)
default: continue; // unknown — leave as-is
case 0x01: slot.type = ActionBarSlot::ITEM; slot.id = id; break;
case 0x80: slot.type = ActionBarSlot::ITEM; slot.id = id; break;
default: continue; // macro or unknown — leave as-is
}
actionBar[i] = slot;
}
@ -7339,15 +7310,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
// ---- Pre-resurrect state ----
case Opcode::SMSG_PRE_RESURRECT: {
// SMSG_PRE_RESURRECT: packed GUID of the player who can self-resurrect.
// Sent when the dead player has Reincarnation (Shaman), Twisting Nether (Warlock),
// or Deathpact (Death Knight passive). The client must send CMSG_SELF_RES to accept.
uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (targetGuid == playerGuid || targetGuid == 0) {
selfResAvailable_ = true;
LOG_INFO("SMSG_PRE_RESURRECT: self-resurrection available (guid=0x",
std::hex, targetGuid, std::dec, ")");
}
// packed GUID of the player to enter pre-resurrect
(void)UpdateObjectParser::readPackedGuid(packet);
break;
}
@ -9113,14 +9077,9 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
lastInteractedGoGuid_ = 0;
castTimeRemaining = 0.0f;
castTimeTotal = 0.0f;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
playerDead_ = false;
releasedSpirit_ = false;
corpseGuid_ = 0;
corpseReclaimAvailableMs_ = 0;
targetGuid = 0;
focusGuid = 0;
lastTargetGuid = 0;
@ -9227,7 +9186,6 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
movementInfo.jumpXYSpeed = 0.0f;
resurrectPending_ = false;
resurrectRequestPending_ = false;
selfResAvailable_ = false;
onTaxiFlight_ = false;
taxiMountActive_ = false;
taxiActivatePending_ = false;
@ -10727,21 +10685,6 @@ void GameHandler::sendMovement(Opcode opcode) {
}
}
// Cancel any timed (non-channeled) cast the moment the player starts moving.
// Channeled spells end via MSG_CHANNEL_UPDATE / SMSG_CHANNEL_NOTIFY from the server.
// Turning (MSG_MOVE_START_TURN_*) is allowed while casting.
if (casting && !castIsChannel) {
const bool isPositionalMove =
opcode == Opcode::MSG_MOVE_START_FORWARD ||
opcode == Opcode::MSG_MOVE_START_BACKWARD ||
opcode == Opcode::MSG_MOVE_START_STRAFE_LEFT ||
opcode == Opcode::MSG_MOVE_START_STRAFE_RIGHT ||
opcode == Opcode::MSG_MOVE_JUMP;
if (isPositionalMove) {
cancelCast();
}
}
// Update movement flags based on opcode
switch (opcode) {
case Opcode::MSG_MOVE_START_FORWARD:
@ -11035,11 +10978,9 @@ void GameHandler::forceClearTaxiAndMovementState() {
vehicleId_ = 0;
resurrectPending_ = false;
resurrectRequestPending_ = false;
selfResAvailable_ = false;
playerDead_ = false;
releasedSpirit_ = false;
corpseGuid_ = 0;
corpseReclaimAvailableMs_ = 0;
repopPending_ = false;
pendingSpiritHealerGuid_ = 0;
resurrectCasterGuid_ = 0;
@ -11678,26 +11619,14 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
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));
const uint16_t enchBase = (fieldIndex(UF::ITEM_FIELD_STACK_COUNT) != 0xFFFF)
? static_cast<uint16_t>(fieldIndex(UF::ITEM_FIELD_STACK_COUNT) + 8u) : 0xFFFFu;
auto permEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase) : block.fields.end();
auto tempEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 3u) : block.fields.end();
auto sock1EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 6u) : block.fields.end();
auto sock2EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 9u) : block.fields.end();
auto sock3EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 12u) : block.fields.end();
if (entryIt != block.fields.end() && entryIt->second != 0) {
// Preserve existing info when doing partial updates
OnlineItemInfo info = onlineItems_.count(block.guid)
? onlineItems_[block.guid] : OnlineItemInfo{};
info.entry = entryIt->second;
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;
if (permEnchIt != block.fields.end()) info.permanentEnchantId = permEnchIt->second;
if (tempEnchIt != block.fields.end()) info.temporaryEnchantId = tempEnchIt->second;
if (sock1EnchIt != block.fields.end()) info.socketEnchantIds[0] = sock1EnchIt->second;
if (sock2EnchIt != block.fields.end()) info.socketEnchantIds[1] = sock2EnchIt->second;
if (sock3EnchIt != block.fields.end()) info.socketEnchantIds[2] = sock3EnchIt->second;
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());
onlineItems_[block.guid] = info;
if (isNew) newItemCreated = true;
@ -11949,7 +11878,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
} else if (wasDead && !nowDead) {
playerDead_ = false;
releasedSpirit_ = false;
selfResAvailable_ = false;
LOG_INFO("Player resurrected (dynamic flags)");
}
} else if (entity->getType() == ObjectType::UNIT) {
@ -12231,10 +12159,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
playerDead_ = false;
repopPending_ = false;
resurrectPending_ = false;
selfResAvailable_ = false;
corpseMapId_ = 0; // corpse reclaimed
corpseGuid_ = 0;
corpseReclaimAvailableMs_ = 0;
LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)");
if (ghostStateCallback_) ghostStateCallback_(false);
}
@ -12282,15 +12208,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY);
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
// ITEM_FIELD_ENCHANTMENT starts 8 fields after ITEM_FIELD_STACK_COUNT (fixed offset
// across all expansions: +DURATION, +5×SPELL_CHARGES, +FLAGS = +8).
// Slot 0 = permanent (field +0), slot 1 = temp (+3), slots 2-4 = sockets (+6,+9,+12).
const uint16_t itemEnchBase = (itemStackField != 0xFFFF) ? (itemStackField + 8u) : 0xFFFF;
const uint16_t itemPermEnchField = itemEnchBase;
const uint16_t itemTempEnchField = (itemEnchBase != 0xFFFF) ? (itemEnchBase + 3u) : 0xFFFF;
const uint16_t itemSock1EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 6u) : 0xFFFF;
const uint16_t itemSock2EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 9u) : 0xFFFF;
const uint16_t itemSock3EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 12u) : 0xFFFF;
auto it = onlineItems_.find(block.guid);
bool isItemInInventory = (it != onlineItems_.end());
@ -12303,61 +12220,14 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
}
} else if (key == itemDurField && isItemInInventory) {
if (it->second.curDurability != val) {
const uint32_t prevDur = it->second.curDurability;
it->second.curDurability = val;
inventoryChanged = true;
// Warn once when durability drops below 20% for an equipped item.
const uint32_t maxDur = it->second.maxDurability;
if (maxDur > 0 && val < maxDur / 5u && prevDur >= maxDur / 5u) {
// Check if this item is in an equip slot (not bag inventory).
bool isEquipped = false;
for (uint64_t slotGuid : equipSlotGuids_) {
if (slotGuid == block.guid) { isEquipped = true; break; }
}
if (isEquipped) {
std::string itemName;
const auto* info = getItemInfo(it->second.entry);
if (info) itemName = info->name;
char buf[128];
if (!itemName.empty())
std::snprintf(buf, sizeof(buf), "%s is about to break!", itemName.c_str());
else
std::snprintf(buf, sizeof(buf), "An equipped item is about to break!");
addUIError(buf);
addSystemChatMessage(buf);
}
}
}
} else if (key == itemMaxDurField && isItemInInventory) {
if (it->second.maxDurability != val) {
it->second.maxDurability = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemPermEnchField != 0xFFFF && key == itemPermEnchField) {
if (it->second.permanentEnchantId != val) {
it->second.permanentEnchantId = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemTempEnchField != 0xFFFF && key == itemTempEnchField) {
if (it->second.temporaryEnchantId != val) {
it->second.temporaryEnchantId = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock1EnchField != 0xFFFF && key == itemSock1EnchField) {
if (it->second.socketEnchantIds[0] != val) {
it->second.socketEnchantIds[0] = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock2EnchField != 0xFFFF && key == itemSock2EnchField) {
if (it->second.socketEnchantIds[1] != val) {
it->second.socketEnchantIds[1] = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock3EnchField != 0xFFFF && key == itemSock3EnchField) {
if (it->second.socketEnchantIds[2] != val) {
it->second.socketEnchantIds[2] = val;
inventoryChanged = true;
}
}
}
// Update container slot GUIDs on bag content changes
@ -14025,7 +13895,7 @@ void GameHandler::stopCasting() {
socket->send(packet);
}
// Reset casting state and clear any queued spell so it doesn't fire later
// Reset casting state
casting = false;
castIsChannel = false;
currentCastSpellId = 0;
@ -14033,10 +13903,6 @@ void GameHandler::stopCasting() {
lastInteractedGoGuid_ = 0;
castTimeRemaining = 0.0f;
castTimeTotal = 0.0f;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
LOG_INFO("Cancelled spell cast");
}
@ -14050,12 +13916,7 @@ void GameHandler::releaseSpirit() {
}
auto packet = RepopRequestPacket::build();
socket->send(packet);
// Do NOT set releasedSpirit_ = true here. Setting it optimistically races
// with PLAYER_FLAGS field updates that arrive before the server processes
// CMSG_REPOP_REQUEST: the PLAYER_FLAGS handler sees wasGhost=true/nowGhost=false
// and fires the "ghost cleared" path, wiping corpseMapId_/corpseGuid_.
// Let the server drive ghost state via PLAYER_FLAGS_GHOST (field update path).
selfResAvailable_ = false; // self-res window closes when spirit is released
releasedSpirit_ = true;
repopPending_ = true;
lastRepopRequestMs_ = static_cast<uint64_t>(now);
LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)");
@ -14063,47 +13924,26 @@ void GameHandler::releaseSpirit() {
}
bool GameHandler::canReclaimCorpse() const {
// Need: ghost state + corpse object GUID (required by CMSG_RECLAIM_CORPSE) +
// corpse map known + same map + within 40 yards.
if (!releasedSpirit_ || corpseGuid_ == 0 || corpseMapId_ == 0) return false;
if (!releasedSpirit_ || corpseMapId_ == 0) return false;
// Only if ghost is on the same map as their corpse
if (currentMapId_ != corpseMapId_) return false;
// movementInfo.x/y are canonical (x=north=server_y, y=west=server_x).
// corpseX_/Y_ are raw server coords (x=west, y=north).
// Convert corpse to canonical before comparing.
float dx = movementInfo.x - corpseY_; // canonical north - server.y
float dy = movementInfo.y - corpseX_; // canonical west - server.x
float dz = movementInfo.z - corpseZ_;
return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f);
}
float GameHandler::getCorpseReclaimDelaySec() const {
if (corpseReclaimAvailableMs_ == 0) return 0.0f;
auto nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
if (nowMs >= corpseReclaimAvailableMs_) return 0.0f;
return static_cast<float>(corpseReclaimAvailableMs_ - nowMs) / 1000.0f;
}
void GameHandler::reclaimCorpse() {
if (!canReclaimCorpse() || !socket) return;
// CMSG_RECLAIM_CORPSE requires the corpse object's own GUID.
// Servers look up the corpse by this GUID; sending the player GUID silently fails.
if (corpseGuid_ == 0) {
LOG_WARNING("reclaimCorpse: corpse GUID not yet known (corpse object not received); cannot reclaim");
return;
}
auto packet = ReclaimCorpsePacket::build(corpseGuid_);
// Reclaim expects the corpse object guid when known; fallback to player guid.
uint64_t reclaimGuid = (corpseGuid_ != 0) ? corpseGuid_ : playerGuid;
auto packet = ReclaimCorpsePacket::build(reclaimGuid);
socket->send(packet);
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, corpseGuid_, std::dec);
}
void GameHandler::useSelfRes() {
if (!selfResAvailable_ || !socket) return;
// CMSG_SELF_RES: empty body — server confirms resurrection via SMSG_UPDATE_OBJECT.
network::Packet pkt(wireOpcode(Opcode::CMSG_SELF_RES));
socket->send(pkt);
selfResAvailable_ = false;
LOG_INFO("Sent CMSG_SELF_RES (Reincarnation / Twisting Nether)");
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for guid=0x", std::hex, reclaimGuid, std::dec,
(corpseGuid_ == 0 ? " (fallback player guid)" : ""));
}
void GameHandler::activateSpiritHealer(uint64_t npcGuid) {
@ -14496,25 +14336,6 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) {
rebuildOnlineInventory();
maybeDetectVisibleItemLayout();
// Flush any deferred loot notifications waiting on this item's name/quality.
for (auto it = pendingItemPushNotifs_.begin(); it != pendingItemPushNotifs_.end(); ) {
if (it->itemId == data.entry) {
std::string itemName = data.name.empty() ? ("item #" + std::to_string(data.entry)) : data.name;
std::string link = buildItemLink(data.entry, data.quality, itemName);
std::string msg = "Received: " + link;
if (it->count > 1) msg += " x" + std::to_string(it->count);
addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playLootItem();
}
if (itemLootCallback_) itemLootCallback_(data.entry, it->count, data.quality, itemName);
it = pendingItemPushNotifs_.erase(it);
} else {
++it;
}
}
// Selectively re-emit only players whose equipment references this item entry
const uint32_t resolvedEntry = data.entry;
for (const auto& [guid, entries] : otherPlayerVisibleItemEntries_) {
@ -18070,17 +17891,7 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
return;
}
if (casting) {
// Spell queue: if we're within 400ms of the cast completing (and not channeling),
// store the spell so it fires automatically when the cast finishes.
if (!castIsChannel && castTimeRemaining > 0.0f && castTimeRemaining <= 0.4f) {
queuedSpellId_ = spellId;
queuedSpellTarget_ = targetGuid != 0 ? targetGuid : this->targetGuid;
LOG_INFO("Spell queue: queued spellId=", spellId, " (", castTimeRemaining * 1000.0f,
"ms remaining)");
}
return;
}
if (casting) return; // Already casting
// Hearthstone: cast spell directly (server checks item in inventory)
// Using CMSG_CAST_SPELL is more reliable than CMSG_USE_ITEM which
@ -18182,11 +17993,9 @@ void GameHandler::cancelCast() {
castIsChannel = false;
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
// Cancel craft queue and spell queue when player manually cancels cast
// Cancel craft queue when player manually cancels cast
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
}
void GameHandler::startCraftQueue(uint32_t spellId, int count) {
@ -18295,24 +18104,6 @@ void GameHandler::dismissPet() {
socket->send(packet);
}
void GameHandler::togglePetSpellAutocast(uint32_t spellId) {
if (petGuid_ == 0 || spellId == 0 || state != WorldState::IN_WORLD || !socket) return;
bool currentlyOn = petAutocastSpells_.count(spellId) != 0;
uint8_t newState = currentlyOn ? 0 : 1;
// CMSG_PET_SPELL_AUTOCAST: petGuid(8) + spellId(4) + state(1)
network::Packet pkt(wireOpcode(Opcode::CMSG_PET_SPELL_AUTOCAST));
pkt.writeUInt64(petGuid_);
pkt.writeUInt32(spellId);
pkt.writeUInt8(newState);
socket->send(pkt);
// Optimistically update local state; server will confirm via SMSG_PET_SPELLS
if (newState)
petAutocastSpells_.insert(spellId);
else
petAutocastSpells_.erase(spellId);
LOG_DEBUG("togglePetSpellAutocast: spellId=", spellId, " autocast=", (int)newState);
}
void GameHandler::renamePet(const std::string& newName) {
if (petGuid_ == 0 || state != WorldState::IN_WORLD || !socket) return;
if (newName.empty() || newName.size() > 12) return; // Server enforces max 12 chars
@ -18478,10 +18269,6 @@ void GameHandler::handleCastFailed(network::Packet& packet) {
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
lastInteractedGoGuid_ = 0;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
// Stop precast sound — spell failed before completing
if (auto* renderer = core::Application::getInstance().getRenderer()) {
@ -18654,16 +18441,6 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
if (spellCastAnimCallback_) {
spellCastAnimCallback_(playerGuid, false, false);
}
// Spell queue: fire the next queued spell now that casting has ended
if (queuedSpellId_ != 0) {
uint32_t nextSpell = queuedSpellId_;
uint64_t nextTarget = queuedSpellTarget_;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
LOG_INFO("Spell queue: firing queued spellId=", nextSpell);
castSpell(nextSpell, nextTarget);
}
} else {
if (spellCastAnimCallback_) {
// End cast animation on other unit
@ -19887,12 +19664,14 @@ void GameHandler::interactWithGameObject(uint64_t guid) {
void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
if (guid == 0) return;
if (state != WorldState::IN_WORLD || !socket) return;
bool turtleMode = isActiveExpansion("turtle");
// Rate-limit to prevent spamming the server
static uint64_t lastInteractGuid = 0;
static std::chrono::steady_clock::time_point lastInteractTime{};
auto now = std::chrono::steady_clock::now();
// Keep duplicate suppression, but allow quick retry clicks.
constexpr int64_t minRepeatMs = 150;
int64_t minRepeatMs = turtleMode ? 150 : 150;
if (guid == lastInteractGuid &&
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastInteractTime).count() < minRepeatMs) {
return;
@ -21165,26 +20944,6 @@ void GameHandler::useItemInBag(int bagIndex, int slotIndex) {
}
}
void GameHandler::openItemBySlot(int backpackIndex) {
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
if (inventory.getBackpackSlot(backpackIndex).empty()) return;
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = OpenItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex));
LOG_INFO("openItemBySlot: CMSG_OPEN_ITEM bag=0xFF slot=", (23 + backpackIndex));
socket->send(packet);
}
void GameHandler::openItemInBag(int bagIndex, int slotIndex) {
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return;
if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return;
if (inventory.getBagSlot(bagIndex, slotIndex).empty()) return;
if (state != WorldState::IN_WORLD || !socket) return;
uint8_t wowBag = static_cast<uint8_t>(19 + bagIndex);
auto packet = OpenItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex));
LOG_INFO("openItemInBag: CMSG_OPEN_ITEM bag=", (int)wowBag, " slot=", slotIndex);
socket->send(packet);
}
void GameHandler::useItemById(uint32_t itemId) {
if (itemId == 0) return;
LOG_DEBUG("useItemById: searching for itemId=", itemId);
@ -22204,14 +21963,6 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
hostileAttackers_.clear();
stopAutoAttack();
tabCycleStale = true;
casting = false;
castIsChannel = false;
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
if (socket) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
@ -22270,10 +22021,6 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
pendingGameObjectInteractGuid_ = 0;
lastInteractedGoGuid_ = 0;
castTimeRemaining = 0.0f;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
// Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready
if (socket) {
@ -23598,21 +23345,6 @@ std::string GameHandler::getCharacterConfigDir() {
return dir;
}
static const std::string EMPTY_MACRO_TEXT;
const std::string& GameHandler::getMacroText(uint32_t macroId) const {
auto it = macros_.find(macroId);
return (it != macros_.end()) ? it->second : EMPTY_MACRO_TEXT;
}
void GameHandler::setMacroText(uint32_t macroId, const std::string& text) {
if (text.empty())
macros_.erase(macroId);
else
macros_[macroId] = text;
saveCharacterConfig();
}
void GameHandler::saveCharacterConfig() {
const Character* ch = getActiveCharacter();
if (!ch || ch->name.empty()) return;
@ -23639,21 +23371,6 @@ void GameHandler::saveCharacterConfig() {
out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n";
}
// Save client-side macro text (escape newlines as \n literal)
for (const auto& [id, text] : macros_) {
if (!text.empty()) {
std::string escaped;
escaped.reserve(text.size());
for (char c : text) {
if (c == '\n') { escaped += "\\n"; }
else if (c == '\r') { /* skip CR */ }
else if (c == '\\') { escaped += "\\\\"; }
else { escaped += c; }
}
out << "macro_" << id << "_text=" << escaped << "\n";
}
}
// Save quest log
out << "quest_log_count=" << questLog_.size() << "\n";
for (size_t i = 0; i < questLog_.size(); i++) {
@ -23694,28 +23411,6 @@ void GameHandler::loadCharacterConfig() {
try { savedGender = std::stoi(val); } catch (...) {}
} else if (key == "use_female_model") {
try { savedUseFemaleModel = std::stoi(val); } catch (...) {}
} else if (key.rfind("macro_", 0) == 0) {
// Parse macro_N_text
size_t firstUnder = 6; // length of "macro_"
size_t secondUnder = key.find('_', firstUnder);
if (secondUnder == std::string::npos) continue;
uint32_t macroId = 0;
try { macroId = static_cast<uint32_t>(std::stoul(key.substr(firstUnder, secondUnder - firstUnder))); } catch (...) { continue; }
if (key.substr(secondUnder + 1) == "text" && !val.empty()) {
// Unescape \n and \\ sequences
std::string unescaped;
unescaped.reserve(val.size());
for (size_t i = 0; i < val.size(); ++i) {
if (val[i] == '\\' && i + 1 < val.size()) {
if (val[i+1] == 'n') { unescaped += '\n'; ++i; }
else if (val[i+1] == '\\') { unescaped += '\\'; ++i; }
else { unescaped += val[i]; }
} else {
unescaped += val[i];
}
}
macros_[macroId] = std::move(unescaped);
}
} else if (key.rfind("action_bar_", 0) == 0) {
// Parse action_bar_N_type or action_bar_N_id
size_t firstUnderscore = 11; // length of "action_bar_"
@ -24792,32 +24487,27 @@ void GameHandler::resetTradeState() {
}
void GameHandler::handleTradeStatusExtended(network::Packet& packet) {
// SMSG_TRADE_STATUS_EXTENDED format differs by expansion:
//
// Classic/TBC:
// uint8 isSelf + uint32 slotCount + [slots] + uint64 coins
// Per slot tail (after isWrapped): giftCreatorGuid(8) + enchants(24) +
// randomPropertyId(4) + suffixFactor(4) + durability(4) + maxDurability(4) = 48 bytes
//
// WotLK 3.3.5a adds:
// uint32 tradeId (after isSelf, before slotCount)
// Per slot: + createPlayedTime(4) at end of trail → trail = 52 bytes
//
// Minimum: isSelf(1) + [tradeId(4)] + slotCount(4) = 5 or 9 bytes
const bool isWotLK = isActiveExpansion("wotlk");
size_t minHdr = isWotLK ? 9u : 5u;
if (packet.getSize() - packet.getReadPos() < minHdr) return;
// WotLK 3.3.5a SMSG_TRADE_STATUS_EXTENDED format:
// uint8 isSelfState (1 = my trade window, 0 = peer's)
// uint32 tradeId
// uint32 slotCount (7: 6 normal + 1 extra for enchanting)
// Per slot (up to slotCount):
// uint8 slotIndex
// uint32 itemId
// uint32 displayId
// uint32 stackCount
// uint8 isWrapped
// uint64 giftCreatorGuid
// uint32 enchantId (and several more enchant/stat fields)
// ... (complex; we parse only the essential fields)
// uint64 coins (gold offered by the sender of this message)
uint8_t isSelf = packet.readUInt8();
if (isWotLK) {
/*uint32_t tradeId =*/ packet.readUInt32(); // WotLK-only field
}
uint32_t slotCount = packet.readUInt32();
size_t rem = packet.getSize() - packet.getReadPos();
if (rem < 9) return;
// Per-slot tail bytes after isWrapped:
// Classic/TBC: giftCreatorGuid(8) + enchants(24) + stats(16) = 48
// WotLK: same + createPlayedTime(4) = 52
const size_t SLOT_TRAIL = isWotLK ? 52u : 48u;
uint8_t isSelf = packet.readUInt8();
uint32_t tradeId = packet.readUInt32(); (void)tradeId;
uint32_t slotCount= packet.readUInt32();
auto& slots = isSelf ? myTradeSlots_ : peerTradeSlots_;
@ -24831,6 +24521,12 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 1) {
isWrapped = (packet.readUInt8() != 0);
}
// AzerothCore 3.3.5a SendUpdateTrade() field order after isWrapped:
// giftCreatorGuid (8) + PERM enchant (4) + SOCK enchants×3 (12)
// + BONUS enchant (4) + TEMP enchant (4) [total enchants: 24]
// + randomPropertyId (4) + suffixFactor (4)
// + durability (4) + maxDurability (4) + createPlayedTime (4) = 52 bytes
constexpr size_t SLOT_TRAIL = 52;
if (packet.getSize() - packet.getReadPos() >= SLOT_TRAIL) {
packet.setReadPos(packet.getReadPos() + SLOT_TRAIL);
} else {

View file

@ -1,6 +1,5 @@
#include "game/inventory.hpp"
#include "core/logger.hpp"
#include <algorithm>
namespace wowee {
namespace game {
@ -186,44 +185,6 @@ bool Inventory::addItem(const ItemDef& item) {
return true;
}
void Inventory::sortBags() {
// Collect all items from backpack and equip bags into a flat list.
std::vector<ItemDef> items;
items.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE);
for (int i = 0; i < BACKPACK_SLOTS; ++i) {
if (!backpack[i].empty())
items.push_back(backpack[i].item);
}
for (int b = 0; b < NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < bags[b].size; ++s) {
if (!bags[b].slots[s].empty())
items.push_back(bags[b].slots[s].item);
}
}
// Sort: quality descending → itemId ascending → stackCount descending.
std::stable_sort(items.begin(), items.end(), [](const ItemDef& a, const ItemDef& b) {
if (a.quality != b.quality)
return static_cast<int>(a.quality) > static_cast<int>(b.quality);
if (a.itemId != b.itemId)
return a.itemId < b.itemId;
return a.stackCount > b.stackCount;
});
// Write sorted items back, filling backpack first then equip bags.
int idx = 0;
int n = static_cast<int>(items.size());
for (int i = 0; i < BACKPACK_SLOTS; ++i)
backpack[i].item = (idx < n) ? items[idx++] : ItemDef{};
for (int b = 0; b < NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < bags[b].size; ++s)
bags[b].slots[s].item = (idx < n) ? items[idx++] : ItemDef{};
}
}
void Inventory::populateTestItems() {
// Equipment
{

View file

@ -1403,14 +1403,15 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data)
SpellGoMissEntry m;
m.targetGuid = packet.readUInt64(); // full GUID in TBC
m.missType = packet.readUInt8();
if (m.missType == 11) { // SPELL_MISS_REFLECT
if (packet.getReadPos() + 1 > packet.getSize()) {
if (m.missType == 11) {
if (packet.getReadPos() + 5 > packet.getSize()) {
LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i,
"/", (int)rawMissCount);
truncatedTargets = true;
break;
}
(void)packet.readUInt8(); // reflectResult
(void)packet.readUInt32();
(void)packet.readUInt8();
}
if (i < storedMissLimit) {
data.missTargets.push_back(m);

View file

@ -3891,53 +3891,45 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
const uint8_t rawMissCount = packet.readUInt8();
if (rawMissCount > 128) {
LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount,
") spell=", data.spellId, " hits=", (int)data.hitCount,
" remaining=", packet.getSize() - packet.getReadPos());
LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount, ")");
}
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
data.missTargets.reserve(storedMissLimit);
for (uint16_t i = 0; i < rawMissCount; ++i) {
// Each miss entry: packed GUID(1-8 bytes) + missType(1 byte).
// REFLECT additionally appends uint8 reflectResult.
// REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult.
if (!hasFullPackedGuid(packet)) {
LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount,
" spell=", data.spellId, " hits=", (int)data.hitCount);
LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount);
truncatedTargets = true;
break;
}
SpellGoMissEntry m;
m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK
if (packet.getSize() - packet.getReadPos() < 1) {
LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount,
" spell=", data.spellId);
LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount);
truncatedTargets = true;
break;
}
m.missType = packet.readUInt8();
if (m.missType == 11) { // SPELL_MISS_REFLECT
if (packet.getSize() - packet.getReadPos() < 1) {
if (m.missType == 11) {
if (packet.getSize() - packet.getReadPos() < 5) {
LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount);
truncatedTargets = true;
break;
}
(void)packet.readUInt8(); // reflectResult
(void)packet.readUInt32();
(void)packet.readUInt8();
}
if (i < storedMissLimit) {
data.missTargets.push_back(m);
}
}
data.missCount = static_cast<uint8_t>(data.missTargets.size());
// If miss targets were truncated, salvage the successfully-parsed hit data
// rather than discarding the entire spell. The server already applied effects;
// we just need the hit list for UI feedback (combat text, health bars).
if (truncatedTargets) {
LOG_DEBUG("Spell go: salvaging ", (int)data.hitCount, " hits despite miss truncation");
packet.setReadPos(packet.getSize()); // consume remaining bytes
return true;
packet.setReadPos(startPos);
return false;
}
data.missCount = static_cast<uint8_t>(data.missTargets.size());
// WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that
// any trailing fields after the target section are not misaligned for
@ -4279,13 +4271,6 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64
return packet;
}
network::Packet OpenItemPacket::build(uint8_t bagIndex, uint8_t slotIndex) {
network::Packet packet(wireOpcode(Opcode::CMSG_OPEN_ITEM));
packet.writeUInt8(bagIndex);
packet.writeUInt8(slotIndex);
return packet;
}
network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOEQUIP_ITEM));
packet.writeUInt8(srcBag);

View file

@ -4008,67 +4008,14 @@ void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tran
}
void M2Renderer::removeInstance(uint32_t instanceId) {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return;
size_t idx = idxIt->second;
if (idx >= instances.size()) return;
auto& inst = instances[idx];
// Remove from spatial grid incrementally (same pattern as the move-update path)
GridCell minCell = toCell(inst.worldBoundsMin);
GridCell maxCell = toCell(inst.worldBoundsMax);
for (int z = minCell.z; z <= maxCell.z; z++) {
for (int y = minCell.y; y <= maxCell.y; y++) {
for (int x = minCell.x; x <= maxCell.x; x++) {
auto gIt = spatialGrid.find(GridCell{x, y, z});
if (gIt != spatialGrid.end()) {
auto& vec = gIt->second;
vec.erase(std::remove(vec.begin(), vec.end(), instanceId), vec.end());
}
}
for (auto it = instances.begin(); it != instances.end(); ++it) {
if (it->id == instanceId) {
destroyInstanceBones(*it);
instances.erase(it);
rebuildSpatialIndex();
return;
}
}
// Remove from dedup map
if (!inst.cachedIsGroundDetail) {
DedupKey dk{inst.modelId,
static_cast<int32_t>(std::round(inst.position.x * 10.0f)),
static_cast<int32_t>(std::round(inst.position.y * 10.0f)),
static_cast<int32_t>(std::round(inst.position.z * 10.0f))};
instanceDedupMap_.erase(dk);
}
destroyInstanceBones(inst);
// Swap-remove: move last element to the hole and pop_back to avoid O(n) shift
instanceIndexById.erase(instanceId);
if (idx < instances.size() - 1) {
uint32_t movedId = instances.back().id;
instances[idx] = std::move(instances.back());
instances.pop_back();
instanceIndexById[movedId] = idx;
} else {
instances.pop_back();
}
// Rebuild the lightweight auxiliary index vectors (smoke, portal, etc.)
// These are small vectors of indices that are rebuilt cheaply.
smokeInstanceIndices_.clear();
portalInstanceIndices_.clear();
animatedInstanceIndices_.clear();
particleOnlyInstanceIndices_.clear();
particleInstanceIndices_.clear();
for (size_t i = 0; i < instances.size(); i++) {
auto& ri = instances[i];
if (ri.cachedIsSmoke) smokeInstanceIndices_.push_back(i);
if (ri.cachedIsInstancePortal) portalInstanceIndices_.push_back(i);
if (ri.cachedHasParticleEmitters) particleInstanceIndices_.push_back(i);
if (ri.cachedHasAnimation && !ri.cachedDisableAnimation)
animatedInstanceIndices_.push_back(i);
else if (ri.cachedHasParticleEmitters)
particleOnlyInstanceIndices_.push_back(i);
}
}
void M2Renderer::setSkipCollision(uint32_t instanceId, bool skip) {

View file

@ -1142,14 +1142,10 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
};
// Color source: final render pass layout is PRESENT_SRC.
// srcAccessMask must be COLOR_ATTACHMENT_WRITE (not 0) so that GPU cache flushes
// happen before the transfer read. Using srcAccessMask=0 with BOTTOM_OF_PIPE
// causes VK_ERROR_DEVICE_LOST on strict drivers (AMD/Mali) because color writes
// are not made visible to the transfer unit before the copy begins.
barrier2(srcColorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
0, VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,

File diff suppressed because it is too large Load diff

View file

@ -1019,7 +1019,7 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
float contentH = rows * (slotSize + 4.0f) + 10.0f;
if (bagIndex < 0) {
int keyringRows = (inventory.getKeyringSize() + columns - 1) / columns;
contentH += 36.0f; // separator + sort button + money display
contentH += 25.0f; // money display for backpack
contentH += 30.0f + keyringRows * (slotSize + 4.0f); // keyring header + slots
}
float gridW = columns * (slotSize + 4.0f) + 30.0f;
@ -1094,29 +1094,16 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
}
}
// Footer for backpack: sort button + money display
if (bagIndex < 0) {
// Money display at bottom of backpack
if (bagIndex < 0 && moneyCopper > 0) {
ImGui::Spacing();
ImGui::Separator();
// Sort Bags button — client-side reorder by quality/type
if (ImGui::SmallButton("Sort Bags")) {
inventory.sortBags();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Sort all bag slots by quality (highest first),\nthen by item ID, then by stack size.");
}
if (moneyCopper > 0) {
ImGui::SameLine();
uint64_t gold = moneyCopper / 10000;
uint64_t silver = (moneyCopper / 100) % 100;
uint64_t copper = moneyCopper % 100;
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc",
static_cast<unsigned long long>(gold),
static_cast<unsigned long long>(silver),
static_cast<unsigned long long>(copper));
}
uint64_t gold = moneyCopper / 10000;
uint64_t silver = (moneyCopper / 100) % 100;
uint64_t copper = moneyCopper % 100;
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc",
static_cast<unsigned long long>(gold),
static_cast<unsigned long long>(silver),
static_cast<unsigned long long>(copper));
}
ImGui::End();
@ -2356,14 +2343,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
} else if (item.inventoryType > 0) {
gameHandler_->autoEquipItemBySlot(backpackIndex);
} else {
// itemClass==1 (Container) with inventoryType==0 means a lockbox;
// use CMSG_OPEN_ITEM so the server checks keyring automatically.
auto* info = gameHandler_->getItemInfo(item.itemId);
if (info && info->valid && info->itemClass == 1) {
gameHandler_->openItemBySlot(backpackIndex);
} else {
gameHandler_->useItemBySlot(backpackIndex);
}
gameHandler_->useItemBySlot(backpackIndex);
}
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
LOG_INFO("Right-click bag item: name='", item.name,
@ -2376,12 +2356,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
} else if (item.inventoryType > 0) {
gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex);
} else {
auto* info = gameHandler_->getItemInfo(item.itemId);
if (info && info->valid && info->itemClass == 1) {
gameHandler_->openItemInBag(bagIndex, bagSlotIndex);
} else {
gameHandler_->useItemInBag(bagIndex, bagSlotIndex);
}
gameHandler_->useItemInBag(bagIndex, bagSlotIndex);
}
}
}
@ -2413,36 +2388,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
if (ImGui::IsItemHovered() && !holdingItem) {
// Pass inventory for backpack/bag items only; equipped items compare against themselves otherwise
const game::Inventory* tooltipInv = (kind == SlotKind::EQUIPMENT) ? nullptr : &inventory;
uint64_t slotGuid = 0;
if (kind == SlotKind::EQUIPMENT && gameHandler_)
slotGuid = gameHandler_->getEquipSlotGuid(static_cast<int>(equipSlot));
renderItemTooltip(item, tooltipInv, slotGuid);
renderItemTooltip(item, tooltipInv);
}
}
}
void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory, uint64_t itemGuid) {
// Shared SpellItemEnchantment name lookup — used for socket gems, permanent and temp enchants.
static std::unordered_map<uint32_t, std::string> s_enchLookupB;
static bool s_enchLookupLoadedB = false;
if (!s_enchLookupLoadedB && assetManager_) {
s_enchLookupLoadedB = true;
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nf = lay ? lay->field("Name") : 8u;
if (nf == 0xFFFFFFFF) nf = 8;
uint32_t fc = dbc->getFieldCount();
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t eid = dbc->getUInt32(r, 0);
if (eid == 0 || nf >= fc) continue;
std::string en = dbc->getString(r, nf);
if (!en.empty()) s_enchLookupB[eid] = std::move(en);
}
}
}
void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory) {
ImGui::BeginTooltip();
ImVec4 qColor = getQualityColor(item.quality);
@ -2827,33 +2778,39 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
};
// Get socket gem enchant IDs for this item (filled from item update fields)
std::array<uint32_t, 3> sockGems{};
if (itemGuid != 0 && gameHandler_)
sockGems = gameHandler_->getItemSocketEnchantIds(itemGuid);
bool hasSocket = false;
for (int i = 0; i < 3; ++i) {
if (qi2->socketColor[i] == 0) continue;
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
for (const auto& st : kSocketTypes) {
if (qi2->socketColor[i] & st.mask) {
if (sockGems[i] != 0) {
auto git = s_enchLookupB.find(sockGems[i]);
if (git != s_enchLookupB.end())
ImGui::TextColored(st.col, "%s: %s", st.label, git->second.c_str());
else
ImGui::TextColored(st.col, "%s: (gem %u)", st.label, sockGems[i]);
} else {
ImGui::TextColored(st.col, "%s", st.label);
}
ImGui::TextColored(st.col, "%s", st.label);
break;
}
}
}
if (hasSocket && qi2->socketBonus != 0) {
auto enchIt = s_enchLookupB.find(qi2->socketBonus);
if (enchIt != s_enchLookupB.end())
static std::unordered_map<uint32_t, std::string> s_enchantNamesD;
static bool s_enchantNamesLoadedD = false;
if (!s_enchantNamesLoadedD && assetManager_) {
s_enchantNamesLoadedD = true;
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nameField = lay ? lay->field("Name") : 8u;
if (nameField == 0xFFFFFFFF) nameField = 8;
uint32_t fc = dbc->getFieldCount();
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t eid = dbc->getUInt32(r, 0);
if (eid == 0 || nameField >= fc) continue;
std::string ename = dbc->getString(r, nameField);
if (!ename.empty()) s_enchantNamesD[eid] = std::move(ename);
}
}
}
auto enchIt = s_enchantNamesD.find(qi2->socketBonus);
if (enchIt != s_enchantNamesD.end())
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
else
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", qi2->socketBonus);
@ -2945,21 +2902,6 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
}
}
// Weapon/armor enchant display for equipped items (reads from item update fields)
if (itemGuid != 0 && gameHandler_) {
auto [permId, tempId] = gameHandler_->getItemEnchantIds(itemGuid);
if (permId != 0) {
auto it2 = s_enchLookupB.find(permId);
const char* ename = (it2 != s_enchLookupB.end()) ? it2->second.c_str() : nullptr;
if (ename) ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "Enchanted: %s", ename);
}
if (tempId != 0) {
auto it2 = s_enchLookupB.find(tempId);
const char* ename = (it2 != s_enchLookupB.end()) ? it2->second.c_str() : nullptr;
if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename);
}
}
// "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");
@ -3112,28 +3054,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
// ---------------------------------------------------------------------------
// Tooltip overload for ItemQueryResponseData (used by loot window, etc.)
// ---------------------------------------------------------------------------
void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory, uint64_t itemGuid) {
// Shared SpellItemEnchantment name lookup — used for socket gems, socket bonus, and enchants.
static std::unordered_map<uint32_t, std::string> s_enchLookup;
static bool s_enchLookupLoaded = false;
if (!s_enchLookupLoaded && assetManager_) {
s_enchLookupLoaded = true;
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nf = lay ? lay->field("Name") : 8u;
if (nf == 0xFFFFFFFF) nf = 8;
uint32_t fc = dbc->getFieldCount();
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t eid = dbc->getUInt32(r, 0);
if (eid == 0 || nf >= fc) continue;
std::string en = dbc->getString(r, nf);
if (!en.empty()) s_enchLookup[eid] = std::move(en);
}
}
}
void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory) {
ImGui::BeginTooltip();
ImVec4 qColor = getQualityColor(static_cast<game::ItemQuality>(info.quality));
@ -3468,54 +3389,46 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
};
// Get socket gem enchant IDs for this item (filled from item update fields)
std::array<uint32_t, 3> sockGems{};
if (itemGuid != 0 && gameHandler_)
sockGems = gameHandler_->getItemSocketEnchantIds(itemGuid);
bool hasSocket = false;
for (int i = 0; i < 3; ++i) {
if (info.socketColor[i] == 0) continue;
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
for (const auto& st : kSocketTypes) {
if (info.socketColor[i] & st.mask) {
if (sockGems[i] != 0) {
auto git = s_enchLookup.find(sockGems[i]);
if (git != s_enchLookup.end())
ImGui::TextColored(st.col, "%s: %s", st.label, git->second.c_str());
else
ImGui::TextColored(st.col, "%s: (gem %u)", st.label, sockGems[i]);
} else {
ImGui::TextColored(st.col, "%s", st.label);
}
ImGui::TextColored(st.col, "%s", st.label);
break;
}
}
}
if (hasSocket && info.socketBonus != 0) {
auto enchIt = s_enchLookup.find(info.socketBonus);
if (enchIt != s_enchLookup.end())
// Socket bonus is a SpellItemEnchantment ID — look up via SpellItemEnchantment.dbc
static std::unordered_map<uint32_t, std::string> s_enchantNames;
static bool s_enchantNamesLoaded = false;
if (!s_enchantNamesLoaded && assetManager_) {
s_enchantNamesLoaded = true;
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nameField = lay ? lay->field("Name") : 8u;
if (nameField == 0xFFFFFFFF) nameField = 8;
uint32_t fc = dbc->getFieldCount();
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t eid = dbc->getUInt32(r, 0);
if (eid == 0 || nameField >= fc) continue;
std::string ename = dbc->getString(r, nameField);
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
}
}
}
auto enchIt = s_enchantNames.find(info.socketBonus);
if (enchIt != s_enchantNames.end())
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
else
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", info.socketBonus);
}
}
// Weapon/armor enchant display for equipped items
if (itemGuid != 0 && gameHandler_) {
auto [permId, tempId] = gameHandler_->getItemEnchantIds(itemGuid);
if (permId != 0) {
auto it2 = s_enchLookup.find(permId);
const char* ename = (it2 != s_enchLookup.end()) ? it2->second.c_str() : nullptr;
if (ename) ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "Enchanted: %s", ename);
}
if (tempId != 0) {
auto it2 = s_enchLookup.find(tempId);
const char* ename = (it2 != s_enchLookup.end()) ? it2->second.c_str() : nullptr;
if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename);
}
}
// Item set membership
if (info.itemSetId != 0) {
// Lazy-load full ItemSet.dbc data (name + item IDs + bonus spells/thresholds)