diff --git a/docs/status.md b/docs/status.md index 991d813d..06722c2f 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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 diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 47997040..e75fedb5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -375,10 +375,6 @@ public: std::shared_ptr 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& 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& 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 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 setSpellCastFailedCallback(SpellCastFailedCallback cb) { spellCastFailedCallback_ = std::move(cb); } - // Unit animation hint: signal jump (animId=38) for other players/NPCs using UnitAnimHintCallback = std::function; 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(backpackSlotGuids_.size())) return 0; return backpackSlotGuids_[index]; } - uint64_t getEquipSlotGuid(int slot) const { - if (slot < 0 || slot >= static_cast(equipSlotGuids_.size())) return 0; - return equipSlotGuids_[slot]; - } - // Returns the permanent and temporary enchant IDs for an item by GUID (0 if unknown). - std::pair 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 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 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 socketEnchantIds{}; // ITEM_ENCHANTMENT_SLOT 2-4 (gems) }; std::unordered_map onlineItems_; std::unordered_map itemInfoCache_; std::unordered_set 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 pendingItemPushNotifs_; std::array equipSlotGuids_{}; std::array backpackSlotGuids_{}; std::array 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 unitCastStates_; uint64_t pendingGameObjectInteractGuid_ = 0; @@ -2808,7 +2750,6 @@ private: float castTimeTotal = 0.0f; std::array actionBar{}; - std::unordered_map macros_; // client-side macro text (persisted in char config) std::vector playerAuras; std::vector targetAuras; std::unordered_map> 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 playerRunes_ = [] { std::array 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; diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index cf092ac4..ea6f6110 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -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(); diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c2aa581f..c7fc0ef4 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -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: diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 0054bf05..0e73c552 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -55,13 +55,6 @@ private: std::vector 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 actionFlashEndTimes_; - // Tab-completion state for slash commands std::string chatTabPrefix_; // prefix captured on first Tab press std::vector chatTabMatches_; // matching command list @@ -113,8 +106,6 @@ private: std::vector 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.5–1.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 diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 21ccdc00..b9c30c6c 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -99,7 +99,7 @@ private: std::unordered_map 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); diff --git a/src/core/application.cpp b/src/core/application.cpp index 4ff3aae1..22e93abc 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 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((st.appearanceBytes >> 16) & 0xFF); geosets.insert(static_cast(100 + hairStyleId + 1)); geosets.insert(static_cast(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(801 + gg1); - // Robe kilt → leg group 13 + geosets.insert(static_cast(gg1 > 0 ? 501 + gg1 : 501)); + uint32_t gg3 = getGeosetGroup(did, geosetGroup3Field); - if (gg3 > 0) geosetPants = static_cast(1301 + gg3); + if (gg3 > 0) geosets.insert(static_cast(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(1301 + gg1); + if (geosets.count(1302) == 0 && geosets.count(1303) == 0) { + geosets.insert(static_cast(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(501 + gg1); + if (gg1 > 0) geosets.insert(static_cast(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(401 + gg1); + geosets.insert(static_cast(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) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3591d97a..2eacb363 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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( - std::chrono::duration_cast( - 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(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(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( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - if (nowMs >= corpseReclaimAvailableMs_) return 0.0f; - return static_cast(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(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(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(19 + bagIndex); - auto packet = OpenItemPacket::build(wowBag, static_cast(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(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 { diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index a6de6dcb..0d694aba 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -1,6 +1,5 @@ #include "game/inventory.hpp" #include "core/logger.hpp" -#include 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 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(a.quality) > static_cast(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(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 { diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index c1397460..8e8fbd25 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -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); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index e6f6d872..aaf18ca2 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -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(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(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(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); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 390ee2c5..ea815963 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -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(std::round(inst.position.x * 10.0f)), - static_cast(std::round(inst.position.y * 10.0f)), - static_cast(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) { diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 6dd0b26f..2bbff1a3 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -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, diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f4f8cd11..8cd7a6c8 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -256,16 +255,6 @@ bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabInde return (tab.typeMask & typeBit) != 0; } -// Forward declaration — defined near sendChatMessage below -static std::string firstMacroCommand(const std::string& macroText); -static std::vector allMacroCommands(const std::string& macroText); -static std::string evaluateMacroConditionals(const std::string& rawArg, - game::GameHandler& gameHandler, - uint64_t& targetOverride); -// Returns the spell/item name from #showtooltip [Name], or "__auto__" for bare -// #showtooltip (use first /cast target), or "" if no directive is present. -static std::string getMacroShowtooltipArg(const std::string& macroText); - void GameScreen::render(game::GameHandler& gameHandler) { // Set up chat bubble callback (once) if (!chatBubbleCallbackSet_) { @@ -414,16 +403,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { uiErrorCallbackSet_ = true; } - // Flash the action bar button whose spell just failed (0.5 s red overlay). - if (!castFailedCallbackSet_) { - gameHandler.setSpellCastFailedCallback([this](uint32_t spellId) { - if (spellId == 0) return; - float now = static_cast(ImGui::GetTime()); - actionFlashEndTimes_[spellId] = now + kActionFlashDuration; - }); - castFailedCallbackSet_ = true; - } - // Set up reputation change toast callback (once) if (!repChangeCallbackSet_) { gameHandler.setRepChangeCallback([this](const std::string& name, int32_t delta, int32_t standing) { @@ -2611,21 +2590,16 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { for (auto& ch : lowerWord) ch = static_cast(std::tolower(static_cast(ch))); static const std::vector kCmds = { - "/afk", "/away", "/cancelaura", "/cancelform", "/cancelshapeshift", - "/cast", "/chathelp", "/clear", + "/afk", "/away", "/cast", "/chathelp", "/clear", "/dance", "/do", "/dnd", "/e", "/emote", - "/cl", "/combatlog", "/dismount", "/equip", "/follow", - "/g", "/guild", "/guildinfo", + "/cl", "/combatlog", "/equip", "/follow", "/g", "/guild", "/guildinfo", "/gmticket", "/grouploot", "/i", "/instance", "/invite", "/j", "/join", "/kick", "/l", "/leave", "/local", "/me", - "/p", "/party", "/petaggressive", "/petattack", "/petdefensive", - "/petdismiss", "/petfollow", "/pethalt", "/petpassive", "/petstay", - "/r", "/raid", + "/p", "/party", "/r", "/raid", "/raidwarning", "/random", "/reply", "/roll", - "/s", "/say", "/setloot", "/shout", "/sit", "/stand", - "/startattack", "/stopattack", "/stopfollow", "/stopcasting", - "/t", "/target", "/time", + "/s", "/say", "/setloot", "/shout", + "/stopattack", "/stopfollow", "/t", "/time", "/trade", "/uninvite", "/use", "/w", "/whisper", "/who", "/wts", "/wtb", "/y", "/yell", "/zone" }; @@ -2859,8 +2833,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { gameHandler.castSpell(bar[slotIdx].id, target); } else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) { gameHandler.useItemById(bar[slotIdx].id); - } else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) { - executeMacroText(gameHandler, gameHandler.getMacroText(bar[slotIdx].id)); } } } @@ -3899,10 +3871,6 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { uint64_t targetGuid = (actionId > 5) ? gameHandler.getTargetGuid() : 0u; gameHandler.sendPetAction(slotVal, targetGuid); } - // Right-click toggles autocast for castable pet spells (actionId > 6) - if (actionId > 6 && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - gameHandler.togglePetSpellAutocast(actionId); - } // Tooltip: rich spell info for pet spells, simple label for built-in commands if (ImGui::IsItemHovered()) { @@ -3924,10 +3892,8 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { if (nm.empty()) nm = "Spell #" + std::to_string(actionId); ImGui::Text("%s", nm.c_str()); } - ImGui::TextColored(autocastOn - ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) - : ImVec4(0.6f, 0.6f, 0.6f, 1.0f), - "Autocast: %s (right-click to toggle)", autocastOn ? "On" : "Off"); + if (autocastOn) + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Autocast: On"); if (petOnCd) { if (petCd >= 60.0f) ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), @@ -4404,27 +4370,6 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (ImGui::IsItemClicked()) { gameHandler.setTarget(totGuid); } - - // Compact health bar for the ToT — essential for healers tracking boss target - if (totEnt) { - auto totUnit = std::dynamic_pointer_cast(totEnt); - if (totUnit && totUnit->getMaxHealth() > 0) { - uint32_t totHp = totUnit->getHealth(); - uint32_t totMaxHp = totUnit->getMaxHealth(); - float totPct = static_cast(totHp) / static_cast(totMaxHp); - ImVec4 totBarColor = - totPct > 0.5f ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) : - totPct > 0.2f ? ImVec4(0.75f, 0.75f, 0.2f, 1.0f) : - ImVec4(0.75f, 0.2f, 0.2f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, totBarColor); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); - char totOverlay[32]; - snprintf(totOverlay, sizeof(totOverlay), "%u%%", - static_cast(totPct * 100.0f + 0.5f)); - ImGui::ProgressBar(totPct, ImVec2(-1, 10), totOverlay); - ImGui::PopStyleColor(2); - } - } } } } @@ -5263,61 +5208,6 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { } } - // Target-of-Focus: who the focus target is currently targeting - { - uint64_t fofGuid = 0; - const auto& fFields = focus->getFields(); - auto fItLo = fFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); - if (fItLo != fFields.end()) { - fofGuid = fItLo->second; - auto fItHi = fFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); - if (fItHi != fFields.end()) - fofGuid |= (static_cast(fItHi->second) << 32); - } - if (fofGuid != 0) { - auto fofEnt = gameHandler.getEntityManager().getEntity(fofGuid); - std::string fofName; - ImVec4 fofColor(0.7f, 0.7f, 0.7f, 1.0f); - if (fofGuid == gameHandler.getPlayerGuid()) { - fofName = "You"; - fofColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); - } else if (fofEnt) { - fofName = getEntityName(fofEnt); - uint8_t fcid = entityClassId(fofEnt.get()); - if (fcid != 0) fofColor = classColorVec4(fcid); - } - if (!fofName.empty()) { - ImGui::TextDisabled("▶"); - ImGui::SameLine(0, 2); - ImGui::TextColored(fofColor, "%s", fofName.c_str()); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Focus's target: %s\nClick to target", fofName.c_str()); - if (ImGui::IsItemClicked()) - gameHandler.setTarget(fofGuid); - - // Compact health bar for target-of-focus - if (fofEnt) { - auto fofUnit = std::dynamic_pointer_cast(fofEnt); - if (fofUnit && fofUnit->getMaxHealth() > 0) { - float fofPct = static_cast(fofUnit->getHealth()) / - static_cast(fofUnit->getMaxHealth()); - ImVec4 fofBarColor = - fofPct > 0.5f ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) : - fofPct > 0.2f ? ImVec4(0.75f, 0.75f, 0.2f, 1.0f) : - ImVec4(0.75f, 0.2f, 0.2f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, fofBarColor); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); - char fofOverlay[32]; - snprintf(fofOverlay, sizeof(fofOverlay), "%u%%", - static_cast(fofPct * 100.0f + 0.5f)); - ImGui::ProgressBar(fofPct, ImVec2(-1, 10), fofOverlay); - ImGui::PopStyleColor(2); - } - } - } - } - } - // Distance to focus target { const auto& mv = gameHandler.getMovementInfo(); @@ -5339,339 +5229,6 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } -// Returns the first executable line of a macro text block, skipping blank lines -// and # directive lines (e.g. #showtooltip). Returns empty string if none found. -static std::string firstMacroCommand(const std::string& macroText) { - size_t pos = 0; - while (pos <= macroText.size()) { - size_t nl = macroText.find('\n', pos); - std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos); - if (!line.empty() && line.back() == '\r') line.pop_back(); - size_t start = line.find_first_not_of(" \t"); - if (start != std::string::npos) line = line.substr(start); - if (!line.empty() && line.front() != '#') - return line; - if (nl == std::string::npos) break; - pos = nl + 1; - } - return {}; -} - -// Collect all non-comment, non-empty lines from a macro body. -static std::vector allMacroCommands(const std::string& macroText) { - std::vector cmds; - size_t pos = 0; - while (pos <= macroText.size()) { - size_t nl = macroText.find('\n', pos); - std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos); - if (!line.empty() && line.back() == '\r') line.pop_back(); - size_t start = line.find_first_not_of(" \t"); - if (start != std::string::npos) line = line.substr(start); - if (!line.empty() && line.front() != '#') - cmds.push_back(std::move(line)); - if (nl == std::string::npos) break; - pos = nl + 1; - } - return cmds; -} - -// Returns the #showtooltip argument from a macro body: -// "#showtooltip Spell" → "Spell" -// "#showtooltip" → "__auto__" (derive from first /cast) -// (none) → "" -static std::string getMacroShowtooltipArg(const std::string& macroText) { - size_t pos = 0; - while (pos <= macroText.size()) { - size_t nl = macroText.find('\n', pos); - std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos); - if (!line.empty() && line.back() == '\r') line.pop_back(); - size_t fs = line.find_first_not_of(" \t"); - if (fs != std::string::npos) line = line.substr(fs); - if (line.rfind("#showtooltip", 0) == 0 || line.rfind("#show", 0) == 0) { - size_t sp = line.find(' '); - if (sp != std::string::npos) { - std::string arg = line.substr(sp + 1); - size_t as = arg.find_first_not_of(" \t"); - if (as != std::string::npos) arg = arg.substr(as); - size_t ae = arg.find_last_not_of(" \t"); - if (ae != std::string::npos) arg.resize(ae + 1); - if (!arg.empty()) return arg; - } - return "__auto__"; - } - if (nl == std::string::npos) break; - pos = nl + 1; - } - return {}; -} - -// --------------------------------------------------------------------------- -// WoW macro conditional evaluator -// Parses: [cond1,cond2] Spell1; [cond3] Spell2; DefaultSpell -// Returns the first matching alternative's argument, or "" if none matches. -// targetOverride is set to a specific GUID if [target=X] was in the conditions, -// or left as UINT64_MAX to mean "use the normal target". -// --------------------------------------------------------------------------- -static std::string evaluateMacroConditionals(const std::string& rawArg, - game::GameHandler& gameHandler, - uint64_t& targetOverride) { - targetOverride = static_cast(-1); - - auto& input = core::Input::getInstance(); - - const bool shiftHeld = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || - input.isKeyPressed(SDL_SCANCODE_RSHIFT); - const bool ctrlHeld = input.isKeyPressed(SDL_SCANCODE_LCTRL) || - input.isKeyPressed(SDL_SCANCODE_RCTRL); - const bool altHeld = input.isKeyPressed(SDL_SCANCODE_LALT) || - input.isKeyPressed(SDL_SCANCODE_RALT); - const bool anyMod = shiftHeld || ctrlHeld || altHeld; - - // Split rawArg on ';' → alternatives - std::vector alts; - { - std::string cur; - for (char c : rawArg) { - if (c == ';') { alts.push_back(cur); cur.clear(); } - else cur += c; - } - alts.push_back(cur); - } - - // Evaluate a single comma-separated condition token. - // tgt is updated if a target= or @ specifier is found. - auto evalCond = [&](const std::string& raw, uint64_t& tgt) -> bool { - std::string c = raw; - // trim - size_t s = c.find_first_not_of(" \t"); if (s) c = (s != std::string::npos) ? c.substr(s) : ""; - size_t e = c.find_last_not_of(" \t"); if (e != std::string::npos) c.resize(e + 1); - if (c.empty()) return true; - - // @target specifiers: @player, @focus, @mouseover, @target - if (!c.empty() && c[0] == '@') { - std::string spec = c.substr(1); - if (spec == "player") tgt = gameHandler.getPlayerGuid(); - else if (spec == "focus") tgt = gameHandler.getFocusGuid(); - else if (spec == "target") tgt = gameHandler.getTargetGuid(); - else if (spec == "mouseover") { - uint64_t mo = gameHandler.getMouseoverGuid(); - if (mo != 0) tgt = mo; - else return false; // no mouseover — skip this alternative - } - return true; - } - // target=X specifiers - if (c.rfind("target=", 0) == 0) { - std::string spec = c.substr(7); - if (spec == "player") tgt = gameHandler.getPlayerGuid(); - else if (spec == "focus") tgt = gameHandler.getFocusGuid(); - else if (spec == "target") tgt = gameHandler.getTargetGuid(); - else if (spec == "mouseover") { - uint64_t mo = gameHandler.getMouseoverGuid(); - if (mo != 0) tgt = mo; - else return false; // no mouseover — skip this alternative - } - return true; - } - - // mod / nomod - if (c == "nomod" || c == "mod:none") return !anyMod; - if (c.rfind("mod:", 0) == 0) { - std::string mods = c.substr(4); - bool ok = true; - if (mods.find("shift") != std::string::npos && !shiftHeld) ok = false; - if (mods.find("ctrl") != std::string::npos && !ctrlHeld) ok = false; - if (mods.find("alt") != std::string::npos && !altHeld) ok = false; - return ok; - } - - // combat / nocombat - if (c == "combat") return gameHandler.isInCombat(); - if (c == "nocombat") return !gameHandler.isInCombat(); - - // Helper to get the effective target entity - auto effTarget = [&]() -> std::shared_ptr { - if (tgt != static_cast(-1) && tgt != 0) - return gameHandler.getEntityManager().getEntity(tgt); - return gameHandler.getTarget(); - }; - - // exists / noexists - if (c == "exists") return effTarget() != nullptr; - if (c == "noexists") return effTarget() == nullptr; - - // dead / nodead - if (c == "dead") { - auto t = effTarget(); - auto u = t ? std::dynamic_pointer_cast(t) : nullptr; - return u && u->getHealth() == 0; - } - if (c == "nodead") { - auto t = effTarget(); - auto u = t ? std::dynamic_pointer_cast(t) : nullptr; - return u && u->getHealth() > 0; - } - - // help (friendly) / harm (hostile) and their no- variants - auto unitHostile = [&](const std::shared_ptr& t) -> bool { - if (!t) return false; - auto u = std::dynamic_pointer_cast(t); - return u && gameHandler.isHostileFactionPublic(u->getFactionTemplate()); - }; - if (c == "harm" || c == "nohelp") { return unitHostile(effTarget()); } - if (c == "help" || c == "noharm") { return !unitHostile(effTarget()); } - - // noform / nostance — player is NOT in a shapeshift/stance - if (c == "noform" || c == "nostance") { - for (const auto& a : gameHandler.getPlayerAuras()) - if (!a.isEmpty() && a.maxDurationMs == -1) return false; - return true; - } - // form:0 same as noform - if (c == "form:0" || c == "stance:0") { - for (const auto& a : gameHandler.getPlayerAuras()) - if (!a.isEmpty() && a.maxDurationMs == -1) return false; - return true; - } - - // buff:SpellName / nobuff:SpellName — check if the effective target (or player - // if no target specified) has a buff with the given name. - // debuff:SpellName / nodebuff:SpellName — same for debuffs (harmful auras). - auto checkAuraByName = [&](const std::string& spellName, bool wantDebuff, - bool negate) -> bool { - // Determine which aura list to check: effective target or player - const std::vector* auras = nullptr; - if (tgt != static_cast(-1) && tgt != 0 && tgt != gameHandler.getPlayerGuid()) { - // Check target's auras - auras = &gameHandler.getTargetAuras(); - } else { - auras = &gameHandler.getPlayerAuras(); - } - std::string nameLow = spellName; - for (char& ch : nameLow) ch = static_cast(std::tolower(static_cast(ch))); - for (const auto& a : *auras) { - if (a.isEmpty() || a.spellId == 0) continue; - // Filter: debuffs have the HARMFUL flag (0x80) or spell has a dispel type - bool isDebuff = (a.flags & 0x80) != 0; - if (wantDebuff ? !isDebuff : isDebuff) continue; - std::string sn = gameHandler.getSpellName(a.spellId); - for (char& ch : sn) ch = static_cast(std::tolower(static_cast(ch))); - if (sn == nameLow) return !negate; - } - return negate; - }; - if (c.rfind("buff:", 0) == 0 && c.size() > 5) - return checkAuraByName(c.substr(5), false, false); - if (c.rfind("nobuff:", 0) == 0 && c.size() > 7) - return checkAuraByName(c.substr(7), false, true); - if (c.rfind("debuff:", 0) == 0 && c.size() > 7) - return checkAuraByName(c.substr(7), true, false); - if (c.rfind("nodebuff:", 0) == 0 && c.size() > 9) - return checkAuraByName(c.substr(9), true, true); - - // mounted / nomounted - if (c == "mounted") return gameHandler.isMounted(); - if (c == "nomounted") return !gameHandler.isMounted(); - - // group (any group) / nogroup / raid - if (c == "group") return !gameHandler.getPartyData().isEmpty(); - if (c == "nogroup") return gameHandler.getPartyData().isEmpty(); - if (c == "raid") { - const auto& pd = gameHandler.getPartyData(); - return pd.groupType >= 1; // groupType 1 = raid, 0 = party - } - - // channeling:SpellName — player is currently channeling that spell - if (c.rfind("channeling:", 0) == 0 && c.size() > 11) { - if (!gameHandler.isChanneling()) return false; - std::string want = c.substr(11); - for (char& ch : want) ch = static_cast(std::tolower(static_cast(ch))); - uint32_t castSpellId = gameHandler.getCurrentCastSpellId(); - std::string sn = gameHandler.getSpellName(castSpellId); - for (char& ch : sn) ch = static_cast(std::tolower(static_cast(ch))); - return sn == want; - } - if (c == "channeling") return gameHandler.isChanneling(); - if (c == "nochanneling") return !gameHandler.isChanneling(); - - // casting (any active cast or channel) - if (c == "casting") return gameHandler.isCasting(); - if (c == "nocasting") return !gameHandler.isCasting(); - - // vehicle / novehicle (WotLK) - if (c == "vehicle") return gameHandler.getVehicleId() != 0; - if (c == "novehicle") return gameHandler.getVehicleId() == 0; - - // Unknown → permissive (don't block) - return true; - }; - - for (auto& alt : alts) { - // trim - size_t fs = alt.find_first_not_of(" \t"); - if (fs == std::string::npos) continue; - alt = alt.substr(fs); - size_t ls = alt.find_last_not_of(" \t"); - if (ls != std::string::npos) alt.resize(ls + 1); - - if (!alt.empty() && alt[0] == '[') { - size_t close = alt.find(']'); - if (close == std::string::npos) continue; - std::string condStr = alt.substr(1, close - 1); - std::string argPart = alt.substr(close + 1); - // Trim argPart - size_t as = argPart.find_first_not_of(" \t"); - argPart = (as != std::string::npos) ? argPart.substr(as) : ""; - - // Evaluate comma-separated conditions - uint64_t tgt = static_cast(-1); - bool pass = true; - size_t cp = 0; - while (pass) { - size_t comma = condStr.find(',', cp); - std::string tok = condStr.substr(cp, comma == std::string::npos ? std::string::npos : comma - cp); - if (!evalCond(tok, tgt)) { pass = false; break; } - if (comma == std::string::npos) break; - cp = comma + 1; - } - if (pass) { - if (tgt != static_cast(-1)) targetOverride = tgt; - return argPart; - } - } else { - // No condition block — default fallback always matches - return alt; - } - } - return {}; -} - -// Execute all non-comment lines of a macro body in sequence. -// In WoW, every line executes per click; the server enforces spell-cast limits. -// /stopmacro (with optional conditionals) halts the remaining commands early. -void GameScreen::executeMacroText(game::GameHandler& gameHandler, const std::string& macroText) { - macroStopped_ = false; - for (const auto& cmd : allMacroCommands(macroText)) { - strncpy(chatInputBuffer, cmd.c_str(), sizeof(chatInputBuffer) - 1); - chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0'; - sendChatMessage(gameHandler); - if (macroStopped_) break; - } - macroStopped_ = false; -} - -// /castsequence persistent state — shared across all macros using the same spell list. -// Keyed by the normalized (lowercase, comma-joined) spell sequence string. -namespace { -struct CastSeqState { - size_t index = 0; - float lastPressSec = 0.0f; - uint64_t lastTargetGuid = 0; - bool lastInCombat = false; -}; -std::unordered_map s_castSeqStates; -} // namespace - void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { if (strlen(chatInputBuffer) > 0) { std::string input(chatInputBuffer); @@ -5721,29 +5278,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /stopmacro [conditions] - // Halts execution of the current macro (remaining lines are skipped). - // With a condition block, only stops if the conditions evaluate to true. - // /stopmacro → always stops - // /stopmacro [combat] → stops only while in combat - // /stopmacro [nocombat] → stops only when not in combat - if (cmdLower == "stopmacro") { - bool shouldStop = true; - if (spacePos != std::string::npos) { - std::string condArg = command.substr(spacePos + 1); - while (!condArg.empty() && condArg.front() == ' ') condArg.erase(condArg.begin()); - if (!condArg.empty() && condArg.front() == '[') { - // Append a sentinel action so evaluateMacroConditionals can signal a match. - uint64_t tgtOver = static_cast(-1); - std::string hit = evaluateMacroConditionals(condArg + " __stop__", gameHandler, tgtOver); - shouldStop = !hit.empty(); - } - } - if (shouldStop) macroStopped_ = true; - chatInputBuffer[0] = '\0'; - return; - } - // /invite command if (cmdLower == "invite" && spacePos != std::string::npos) { std::string targetName = command.substr(spacePos + 1); @@ -5856,7 +5390,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { " /gleader /groster /ginfo /gcreate /gdisband", "Combat: /startattack /stopattack /stopcasting /cast /duel /pvp", " /forfeit /follow /stopfollow /assist", - "Items: /use /equip /equipset [name]", + "Items: /use /equip ", "Target: /target /cleartarget /focus /clearfocus", "Movement: /sit /stand /kneel /dismount", "Misc: /played /time /zone /afk [msg] /dnd [msg] /inspect", @@ -6056,96 +5590,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // Pet control commands (common macro use) - // Action IDs: 1=passive, 2=follow, 3=stay, 4=defensive, 5=attack, 6=aggressive - if (cmdLower == "petattack") { - uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; - gameHandler.sendPetAction(5, target); - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "petfollow") { - gameHandler.sendPetAction(2, 0); - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "petstay" || cmdLower == "pethalt") { - gameHandler.sendPetAction(3, 0); - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "petpassive") { - gameHandler.sendPetAction(1, 0); - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "petdefensive") { - gameHandler.sendPetAction(4, 0); - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "petaggressive") { - gameHandler.sendPetAction(6, 0); - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "petdismiss") { - gameHandler.dismissPet(); - chatInputBuffer[0] = '\0'; - return; - } - - // /cancelform / /cancelshapeshift — leave current shapeshift/stance - if (cmdLower == "cancelform" || cmdLower == "cancelshapeshift") { - // Cancel the first permanent shapeshift aura the player has - for (const auto& aura : gameHandler.getPlayerAuras()) { - if (aura.spellId == 0) continue; - // Permanent shapeshift auras have the permanent flag (0x20) set - if (aura.flags & 0x20) { - gameHandler.cancelAura(aura.spellId); - break; - } - } - chatInputBuffer[0] = '\0'; - return; - } - - // /cancelaura — cancel a specific buff by name or ID - if (cmdLower == "cancelaura" && spacePos != std::string::npos) { - std::string auraArg = command.substr(spacePos + 1); - while (!auraArg.empty() && auraArg.front() == ' ') auraArg.erase(auraArg.begin()); - while (!auraArg.empty() && auraArg.back() == ' ') auraArg.pop_back(); - // Try numeric ID first - { - std::string numStr = auraArg; - if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin()); - bool isNum = !numStr.empty() && - std::all_of(numStr.begin(), numStr.end(), - [](unsigned char c){ return std::isdigit(c); }); - if (isNum) { - uint32_t spellId = 0; - try { spellId = static_cast(std::stoul(numStr)); } catch (...) {} - if (spellId) gameHandler.cancelAura(spellId); - chatInputBuffer[0] = '\0'; - return; - } - } - // Name match against player auras - std::string argLow = auraArg; - for (char& c : argLow) c = static_cast(std::tolower(static_cast(c))); - for (const auto& aura : gameHandler.getPlayerAuras()) { - if (aura.spellId == 0) continue; - std::string sn = gameHandler.getSpellName(aura.spellId); - for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); - if (sn == argLow) { - gameHandler.cancelAura(aura.spellId); - break; - } - } - chatInputBuffer[0] = '\0'; - return; - } - // /sit command if (cmdLower == "sit") { gameHandler.setStandState(1); // 1 = sit @@ -6211,79 +5655,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { // /assist command if (cmdLower == "assist") { - // /assist → assist current target (use their target) - // /assist PlayerName → find PlayerName, target their target - // /assist [target=X] → evaluate conditional, target that entity's target - auto assistEntityTarget = [&](uint64_t srcGuid) { - auto srcEnt = gameHandler.getEntityManager().getEntity(srcGuid); - if (!srcEnt) { gameHandler.assistTarget(); return; } - uint64_t atkGuid = 0; - const auto& flds = srcEnt->getFields(); - auto iLo = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); - if (iLo != flds.end()) { - atkGuid = iLo->second; - auto iHi = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); - if (iHi != flds.end()) atkGuid |= (static_cast(iHi->second) << 32); - } - if (atkGuid != 0) { - gameHandler.setTarget(atkGuid); - } else { - std::string sn = getEntityName(srcEnt); - game::MessageChatData msg; - msg.type = game::ChatType::SYSTEM; - msg.language = game::ChatLanguage::UNIVERSAL; - msg.message = (sn.empty() ? "Target" : sn) + " has no target."; - gameHandler.addLocalChatMessage(msg); - } - }; - - if (spacePos != std::string::npos) { - std::string assistArg = command.substr(spacePos + 1); - while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin()); - - // Evaluate conditionals if present - uint64_t assistOver = static_cast(-1); - if (!assistArg.empty() && assistArg.front() == '[') { - assistArg = evaluateMacroConditionals(assistArg, gameHandler, assistOver); - if (assistArg.empty() && assistOver == static_cast(-1)) { - chatInputBuffer[0] = '\0'; return; // no condition matched - } - while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin()); - while (!assistArg.empty() && assistArg.back() == ' ') assistArg.pop_back(); - } - - if (assistOver != static_cast(-1) && assistOver != 0) { - assistEntityTarget(assistOver); - } else if (!assistArg.empty()) { - // Name search - std::string argLow = assistArg; - for (char& c : argLow) c = static_cast(std::tolower(static_cast(c))); - uint64_t bestGuid = 0; float bestDist = std::numeric_limits::max(); - const auto& pmi = gameHandler.getMovementInfo(); - for (const auto& [guid, ent] : gameHandler.getEntityManager().getEntities()) { - if (!ent || ent->getType() == game::ObjectType::OBJECT) continue; - std::string nm = getEntityName(ent); - std::string nml = nm; - for (char& c : nml) c = static_cast(std::tolower(static_cast(c))); - if (nml.find(argLow) != 0) continue; - float d2 = (ent->getX()-pmi.x)*(ent->getX()-pmi.x) - + (ent->getY()-pmi.y)*(ent->getY()-pmi.y); - if (d2 < bestDist) { bestDist = d2; bestGuid = guid; } - } - if (bestGuid) assistEntityTarget(bestGuid); - else { - game::MessageChatData msg; - msg.type = game::ChatType::SYSTEM; - msg.language = game::ChatLanguage::UNIVERSAL; - msg.message = "No unit matching '" + assistArg + "' found."; - gameHandler.addLocalChatMessage(msg); - } - } else { - gameHandler.assistTarget(); - } - } else { - gameHandler.assistTarget(); - } + gameHandler.assistTarget(); chatInputBuffer[0] = '\0'; return; } @@ -6587,57 +5959,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /mark [icon] — set or clear a raid target mark on the current target. - // Icon names (case-insensitive): star, circle, diamond, triangle, moon, square, cross, skull - // /mark clear | /mark 0 — remove all marks (sets icon 0xFF = clear) - // /mark — no arg marks with skull (icon 7) - if (cmdLower == "mark" || cmdLower == "marktarget" || cmdLower == "raidtarget") { - if (!gameHandler.hasTarget()) { - game::MessageChatData noTgt; - noTgt.type = game::ChatType::SYSTEM; - noTgt.language = game::ChatLanguage::UNIVERSAL; - noTgt.message = "No target selected."; - gameHandler.addLocalChatMessage(noTgt); - chatInputBuffer[0] = '\0'; - return; - } - static const char* kMarkWords[] = { - "star", "circle", "diamond", "triangle", "moon", "square", "cross", "skull" - }; - uint8_t icon = 7; // default: skull - if (spacePos != std::string::npos) { - std::string arg = command.substr(spacePos + 1); - while (!arg.empty() && arg.front() == ' ') arg.erase(arg.begin()); - std::string argLow = arg; - for (auto& c : argLow) c = static_cast(std::tolower(c)); - if (argLow == "clear" || argLow == "0" || argLow == "none") { - gameHandler.setRaidMark(gameHandler.getTargetGuid(), 0xFF); - chatInputBuffer[0] = '\0'; - return; - } - bool found = false; - for (int mi = 0; mi < 8; ++mi) { - if (argLow == kMarkWords[mi]) { icon = static_cast(mi); found = true; break; } - } - if (!found && !argLow.empty() && argLow[0] >= '1' && argLow[0] <= '8') { - icon = static_cast(argLow[0] - '1'); - found = true; - } - if (!found) { - game::MessageChatData badArg; - badArg.type = game::ChatType::SYSTEM; - badArg.language = game::ChatLanguage::UNIVERSAL; - badArg.message = "Unknown mark. Use: star circle diamond triangle moon square cross skull"; - gameHandler.addLocalChatMessage(badArg); - chatInputBuffer[0] = '\0'; - return; - } - } - gameHandler.setRaidMark(gameHandler.getTargetGuid(), icon); - chatInputBuffer[0] = '\0'; - return; - } - // Combat and Trade commands if (cmdLower == "duel") { if (gameHandler.hasTarget()) { @@ -6675,29 +5996,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } if (cmdLower == "startattack") { - // Support macro conditionals: /startattack [harm,nodead] - bool condPass = true; - uint64_t saOverride = static_cast(-1); - if (spacePos != std::string::npos) { - std::string saArg = command.substr(spacePos + 1); - while (!saArg.empty() && saArg.front() == ' ') saArg.erase(saArg.begin()); - if (!saArg.empty() && saArg.front() == '[') { - std::string result = evaluateMacroConditionals(saArg, gameHandler, saOverride); - condPass = !(result.empty() && saOverride == static_cast(-1)); - } - } - if (condPass) { - uint64_t atkTarget = (saOverride != static_cast(-1) && saOverride != 0) - ? saOverride : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); - if (atkTarget != 0) { - gameHandler.startAutoAttack(atkTarget); - } else { - game::MessageChatData msg; - msg.type = game::ChatType::SYSTEM; - msg.language = game::ChatLanguage::UNIVERSAL; - msg.message = "You have no target."; - gameHandler.addLocalChatMessage(msg); - } + if (gameHandler.hasTarget()) { + gameHandler.startAutoAttack(gameHandler.getTargetGuid()); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You have no target."; + gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; @@ -6715,225 +6021,12 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /equipset [name] — equip a saved equipment set by name (partial match, case-insensitive) - // /equipset — list available sets in chat - if (cmdLower == "equipset") { - const auto& sets = gameHandler.getEquipmentSets(); - auto sysSay = [&](const std::string& msg) { - game::MessageChatData m; - m.type = game::ChatType::SYSTEM; - m.language = game::ChatLanguage::UNIVERSAL; - m.message = msg; - gameHandler.addLocalChatMessage(m); - }; - if (spacePos == std::string::npos) { - // No argument: list available sets - if (sets.empty()) { - sysSay("[System] No equipment sets saved."); - } else { - sysSay("[System] Equipment sets:"); - for (const auto& es : sets) - sysSay(" " + es.name); - } - } else { - std::string setName = command.substr(spacePos + 1); - while (!setName.empty() && setName.front() == ' ') setName.erase(setName.begin()); - while (!setName.empty() && setName.back() == ' ') setName.pop_back(); - // Case-insensitive prefix match - std::string setLower = setName; - std::transform(setLower.begin(), setLower.end(), setLower.begin(), ::tolower); - const game::GameHandler::EquipmentSetInfo* found = nullptr; - for (const auto& es : sets) { - std::string nameLow = es.name; - std::transform(nameLow.begin(), nameLow.end(), nameLow.begin(), ::tolower); - if (nameLow == setLower || nameLow.find(setLower) == 0) { - found = &es; - break; - } - } - if (found) { - gameHandler.useEquipmentSet(found->setId); - } else { - sysSay("[System] No equipment set matching '" + setName + "'."); - } - } - chatInputBuffer[0] = '\0'; - return; - } - - // /castsequence [conds] [reset=N/target/combat] Spell1, Spell2, ... - // Cycles through the spell list on successive presses; resets per the reset= spec. - if (cmdLower == "castsequence" && spacePos != std::string::npos) { - std::string seqArg = command.substr(spacePos + 1); - while (!seqArg.empty() && seqArg.front() == ' ') seqArg.erase(seqArg.begin()); - - // Macro conditionals - uint64_t seqTgtOver = static_cast(-1); - if (!seqArg.empty() && seqArg.front() == '[') { - seqArg = evaluateMacroConditionals(seqArg, gameHandler, seqTgtOver); - if (seqArg.empty() && seqTgtOver == static_cast(-1)) { - chatInputBuffer[0] = '\0'; return; - } - while (!seqArg.empty() && seqArg.front() == ' ') seqArg.erase(seqArg.begin()); - while (!seqArg.empty() && seqArg.back() == ' ') seqArg.pop_back(); - } - - // Optional reset= spec (may contain slash-separated conditions: reset=5/target) - std::string resetSpec; - if (seqArg.rfind("reset=", 0) == 0) { - size_t spAfter = seqArg.find(' '); - if (spAfter != std::string::npos) { - resetSpec = seqArg.substr(6, spAfter - 6); - seqArg = seqArg.substr(spAfter + 1); - while (!seqArg.empty() && seqArg.front() == ' ') seqArg.erase(seqArg.begin()); - } - } - - // Parse comma-separated spell list - std::vector seqSpells; - { - std::string cur; - for (char c : seqArg) { - if (c == ',') { - while (!cur.empty() && cur.front() == ' ') cur.erase(cur.begin()); - while (!cur.empty() && cur.back() == ' ') cur.pop_back(); - if (!cur.empty()) seqSpells.push_back(cur); - cur.clear(); - } else { cur += c; } - } - while (!cur.empty() && cur.front() == ' ') cur.erase(cur.begin()); - while (!cur.empty() && cur.back() == ' ') cur.pop_back(); - if (!cur.empty()) seqSpells.push_back(cur); - } - if (seqSpells.empty()) { chatInputBuffer[0] = '\0'; return; } - - // Build stable key from lowercase spell list - std::string seqKey; - for (size_t k = 0; k < seqSpells.size(); ++k) { - if (k) seqKey += ','; - std::string sl = seqSpells[k]; - for (char& c : sl) c = static_cast(std::tolower(static_cast(c))); - seqKey += sl; - } - - auto& seqState = s_castSeqStates[seqKey]; - - // Check reset conditions (slash-separated: e.g. "5/target") - float nowSec = static_cast(ImGui::GetTime()); - bool shouldReset = false; - if (!resetSpec.empty()) { - size_t rpos = 0; - while (rpos <= resetSpec.size()) { - size_t slash = resetSpec.find('/', rpos); - std::string part = (slash != std::string::npos) - ? resetSpec.substr(rpos, slash - rpos) - : resetSpec.substr(rpos); - std::string plow = part; - for (char& c : plow) c = static_cast(std::tolower(static_cast(c))); - bool isNum = !plow.empty() && std::all_of(plow.begin(), plow.end(), - [](unsigned char c){ return std::isdigit(c) || c == '.'; }); - if (isNum) { - float rSec = 0.0f; - try { rSec = std::stof(plow); } catch (...) {} - if (rSec > 0.0f && nowSec - seqState.lastPressSec > rSec) shouldReset = true; - } else if (plow == "target") { - if (gameHandler.getTargetGuid() != seqState.lastTargetGuid) shouldReset = true; - } else if (plow == "combat") { - if (gameHandler.isInCombat() != seqState.lastInCombat) shouldReset = true; - } - if (slash == std::string::npos) break; - rpos = slash + 1; - } - } - if (shouldReset || seqState.index >= seqSpells.size()) seqState.index = 0; - - const std::string& seqSpell = seqSpells[seqState.index]; - seqState.index = (seqState.index + 1) % seqSpells.size(); - seqState.lastPressSec = nowSec; - seqState.lastTargetGuid = gameHandler.getTargetGuid(); - seqState.lastInCombat = gameHandler.isInCombat(); - - // Cast the selected spell — mirrors /cast spell lookup - std::string ssLow = seqSpell; - for (char& c : ssLow) c = static_cast(std::tolower(static_cast(c))); - if (!ssLow.empty() && ssLow.front() == '!') ssLow.erase(ssLow.begin()); - - uint64_t seqTargetGuid = (seqTgtOver != static_cast(-1) && seqTgtOver != 0) - ? seqTgtOver : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); - - // Numeric ID - if (!ssLow.empty() && ssLow.front() == '#') ssLow.erase(ssLow.begin()); - bool ssNumeric = !ssLow.empty() && std::all_of(ssLow.begin(), ssLow.end(), - [](unsigned char c){ return std::isdigit(c); }); - if (ssNumeric) { - uint32_t ssId = 0; - try { ssId = static_cast(std::stoul(ssLow)); } catch (...) {} - if (ssId) gameHandler.castSpell(ssId, seqTargetGuid); - } else { - uint32_t ssBest = 0; int ssBestRank = -1; - for (uint32_t sid : gameHandler.getKnownSpells()) { - const std::string& sn = gameHandler.getSpellName(sid); - if (sn.empty()) continue; - std::string snl = sn; - for (char& c : snl) c = static_cast(std::tolower(static_cast(c))); - if (snl != ssLow) continue; - int sRnk = 0; - const std::string& rk = gameHandler.getSpellRank(sid); - if (!rk.empty()) { - std::string rkl = rk; - for (char& c : rkl) c = static_cast(std::tolower(static_cast(c))); - if (rkl.rfind("rank ", 0) == 0) { try { sRnk = std::stoi(rkl.substr(5)); } catch (...) {} } - } - if (sRnk > ssBestRank) { ssBestRank = sRnk; ssBest = sid; } - } - if (ssBest) gameHandler.castSpell(ssBest, seqTargetGuid); - } - chatInputBuffer[0] = '\0'; - return; - } - if (cmdLower == "cast" && spacePos != std::string::npos) { std::string spellArg = command.substr(spacePos + 1); // Trim leading/trailing whitespace while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin()); while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back(); - // Evaluate WoW macro conditionals: /cast [mod:shift] Greater Heal; Flash Heal - uint64_t castTargetOverride = static_cast(-1); - if (!spellArg.empty() && spellArg.front() == '[') { - spellArg = evaluateMacroConditionals(spellArg, gameHandler, castTargetOverride); - if (spellArg.empty()) { - chatInputBuffer[0] = '\0'; - return; // No conditional matched — skip cast - } - while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin()); - while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back(); - } - - // Strip leading '!' (WoW /cast !Spell forces recast without toggling off) - if (!spellArg.empty() && spellArg.front() == '!') spellArg.erase(spellArg.begin()); - - // Support numeric spell ID: /cast 133 or /cast #133 - { - std::string numStr = spellArg; - if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin()); - bool isNumeric = !numStr.empty() && - std::all_of(numStr.begin(), numStr.end(), - [](unsigned char c){ return std::isdigit(c); }); - if (isNumeric) { - uint32_t spellId = 0; - try { spellId = static_cast(std::stoul(numStr)); } catch (...) {} - if (spellId != 0) { - uint64_t targetGuid = (castTargetOverride != static_cast(-1)) - ? castTargetOverride - : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); - gameHandler.castSpell(spellId, targetGuid); - } - chatInputBuffer[0] = '\0'; - return; - } - } - // Parse optional "(Rank N)" suffix: "Fireball(Rank 3)" or "Fireball (Rank 3)" int requestedRank = -1; // -1 = highest rank std::string spellName = spellArg; @@ -6986,9 +6079,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } if (bestSpellId) { - uint64_t targetGuid = (castTargetOverride != static_cast(-1)) - ? castTargetOverride - : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); + uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; gameHandler.castSpell(bestSpellId, targetGuid); } else { game::MessageChatData sysMsg; @@ -7003,65 +6094,11 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /use - // Supports: item name, numeric item ID (#N or N), bag/slot (/use 0 1 = backpack slot 1, - // /use 1-4 slot = bag slot), equipment slot number (/use 16 = main hand) + // /use — use an item from backpack/bags by name if (cmdLower == "use" && spacePos != std::string::npos) { std::string useArg = command.substr(spacePos + 1); while (!useArg.empty() && useArg.front() == ' ') useArg.erase(useArg.begin()); while (!useArg.empty() && useArg.back() == ' ') useArg.pop_back(); - - // Handle macro conditionals: /use [mod:shift] ItemName; OtherItem - if (!useArg.empty() && useArg.front() == '[') { - uint64_t dummy = static_cast(-1); - useArg = evaluateMacroConditionals(useArg, gameHandler, dummy); - if (useArg.empty()) { chatInputBuffer[0] = '\0'; return; } - while (!useArg.empty() && useArg.front() == ' ') useArg.erase(useArg.begin()); - while (!useArg.empty() && useArg.back() == ' ') useArg.pop_back(); - } - - // Check for bag/slot notation: two numbers separated by whitespace - { - std::istringstream iss(useArg); - int bagNum = -1, slotNum = -1; - iss >> bagNum >> slotNum; - if (!iss.fail() && slotNum >= 1) { - if (bagNum == 0) { - // Backpack: bag=0, slot 1-based → 0-based - gameHandler.useItemBySlot(slotNum - 1); - chatInputBuffer[0] = '\0'; - return; - } else if (bagNum >= 1 && bagNum <= game::Inventory::NUM_BAG_SLOTS) { - // Equip bag: bags are 1-indexed (bag 1 = bagIndex 0) - gameHandler.useItemInBag(bagNum - 1, slotNum - 1); - chatInputBuffer[0] = '\0'; - return; - } - } - } - - // Numeric equip slot: /use 16 = slot 16 (1-based, WoW equip slot enum) - { - std::string numStr = useArg; - if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin()); - bool isNumeric = !numStr.empty() && - std::all_of(numStr.begin(), numStr.end(), - [](unsigned char c){ return std::isdigit(c); }); - if (isNumeric) { - // Treat as equip slot (1-based, maps to EquipSlot enum 0-based) - int slotNum = 0; - try { slotNum = std::stoi(numStr); } catch (...) {} - if (slotNum >= 1 && slotNum <= static_cast(game::EquipSlot::BAG4) + 1) { - auto eslot = static_cast(slotNum - 1); - const auto& esl = gameHandler.getInventory().getEquipSlot(eslot); - if (!esl.empty()) - gameHandler.useItemById(esl.item.itemId); - } - chatInputBuffer[0] = '\0'; - return; - } - } - std::string useArgLower = useArg; for (char& c : useArgLower) c = static_cast(std::tolower(static_cast(c))); @@ -7157,62 +6194,17 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { // Targeting commands if (cmdLower == "cleartarget") { - // Support macro conditionals: /cleartarget [dead] clears only if target is dead - bool ctCondPass = true; - if (spacePos != std::string::npos) { - std::string ctArg = command.substr(spacePos + 1); - while (!ctArg.empty() && ctArg.front() == ' ') ctArg.erase(ctArg.begin()); - if (!ctArg.empty() && ctArg.front() == '[') { - uint64_t ctOver = static_cast(-1); - std::string res = evaluateMacroConditionals(ctArg, gameHandler, ctOver); - ctCondPass = !(res.empty() && ctOver == static_cast(-1)); - } - } - if (ctCondPass) gameHandler.clearTarget(); + gameHandler.clearTarget(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "target" && spacePos != std::string::npos) { - // Search visible entities for name match (case-insensitive prefix). - // Among all matches, pick the nearest living unit to the player. - // Supports WoW macro conditionals: /target [target=mouseover]; /target [mod:shift] Boss + // Search visible entities for name match (case-insensitive prefix) std::string targetArg = command.substr(spacePos + 1); - - // Evaluate conditionals if present - uint64_t targetCmdOverride = static_cast(-1); - if (!targetArg.empty() && targetArg.front() == '[') { - targetArg = evaluateMacroConditionals(targetArg, gameHandler, targetCmdOverride); - if (targetArg.empty() && targetCmdOverride == static_cast(-1)) { - // No condition matched — silently skip (macro fallthrough) - chatInputBuffer[0] = '\0'; - return; - } - while (!targetArg.empty() && targetArg.front() == ' ') targetArg.erase(targetArg.begin()); - while (!targetArg.empty() && targetArg.back() == ' ') targetArg.pop_back(); - } - - // If conditionals resolved to a specific GUID, target it directly - if (targetCmdOverride != static_cast(-1) && targetCmdOverride != 0) { - gameHandler.setTarget(targetCmdOverride); - chatInputBuffer[0] = '\0'; - return; - } - - // If no name remains (bare conditional like [target=mouseover] with 0 guid), skip silently - if (targetArg.empty()) { - chatInputBuffer[0] = '\0'; - return; - } - std::string targetArgLower = targetArg; for (char& c : targetArgLower) c = static_cast(std::tolower(static_cast(c))); uint64_t bestGuid = 0; - float bestDist = std::numeric_limits::max(); - const auto& pmi = gameHandler.getMovementInfo(); - const float playerX = pmi.x; - const float playerY = pmi.y; - const float playerZ = pmi.z; for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { if (!entity || entity->getType() == game::ObjectType::OBJECT) continue; std::string name; @@ -7225,14 +6217,8 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { std::string nameLower = name; for (char& c : nameLower) c = static_cast(std::tolower(static_cast(c))); if (nameLower.find(targetArgLower) == 0) { - float dx = entity->getX() - playerX; - float dy = entity->getY() - playerY; - float dz = entity->getZ() - playerZ; - float dist = dx*dx + dy*dy + dz*dz; - if (dist < bestDist) { - bestDist = dist; - bestGuid = guid; - } + bestGuid = guid; + if (nameLower == targetArgLower) break; // Exact match wins } } if (bestGuid) { @@ -7279,64 +6265,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } if (cmdLower == "focus") { - // /focus → set current target as focus - // /focus PlayerName → search for entity by name and set as focus - // /focus [target=X] Name → macro conditional: set focus to resolved target - if (spacePos != std::string::npos) { - std::string focusArg = command.substr(spacePos + 1); - - // Evaluate conditionals if present - uint64_t focusCmdOverride = static_cast(-1); - if (!focusArg.empty() && focusArg.front() == '[') { - focusArg = evaluateMacroConditionals(focusArg, gameHandler, focusCmdOverride); - if (focusArg.empty() && focusCmdOverride == static_cast(-1)) { - chatInputBuffer[0] = '\0'; - return; - } - while (!focusArg.empty() && focusArg.front() == ' ') focusArg.erase(focusArg.begin()); - while (!focusArg.empty() && focusArg.back() == ' ') focusArg.pop_back(); - } - - if (focusCmdOverride != static_cast(-1) && focusCmdOverride != 0) { - // Conditional resolved to a specific GUID (e.g. [target=mouseover]) - gameHandler.setFocus(focusCmdOverride); - } else if (!focusArg.empty()) { - // Name search — same logic as /target - std::string focusArgLower = focusArg; - for (char& c : focusArgLower) c = static_cast(std::tolower(static_cast(c))); - uint64_t bestGuid = 0; - float bestDist = std::numeric_limits::max(); - const auto& pmi = gameHandler.getMovementInfo(); - for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { - if (!entity || entity->getType() == game::ObjectType::OBJECT) continue; - std::string name; - if (entity->getType() == game::ObjectType::PLAYER || - entity->getType() == game::ObjectType::UNIT) { - auto unit = std::static_pointer_cast(entity); - name = unit->getName(); - } - if (name.empty()) continue; - std::string nameLower = name; - for (char& c : nameLower) c = static_cast(std::tolower(static_cast(c))); - if (nameLower.find(focusArgLower) == 0) { - float dx = entity->getX() - pmi.x; - float dy = entity->getY() - pmi.y; - float dz = entity->getZ() - pmi.z; - float dist = dx*dx + dy*dy + dz*dz; - if (dist < bestDist) { bestDist = dist; bestGuid = guid; } - } - } - if (bestGuid) { - gameHandler.setFocus(bestGuid); - } else { - game::MessageChatData msg; - msg.type = game::ChatType::SYSTEM; - msg.language = game::ChatLanguage::UNIVERSAL; - msg.message = "No unit matching '" + focusArg + "' found."; - gameHandler.addLocalChatMessage(msg); - } - } - } else if (gameHandler.hasTarget()) { + if (gameHandler.hasTarget()) { gameHandler.setFocus(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; @@ -8444,59 +7373,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId); } - // Macro icon: #showtooltip [SpellName] → show that spell's icon on the button - if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0 && !iconTex) { - const std::string& macroText = gameHandler.getMacroText(slot.id); - if (!macroText.empty()) { - std::string showArg = getMacroShowtooltipArg(macroText); - if (showArg.empty() || showArg == "__auto__") { - // No explicit #showtooltip arg — derive spell from first /cast line - for (const auto& cmdLine : allMacroCommands(macroText)) { - if (cmdLine.size() < 6) continue; - std::string cl = cmdLine; - for (char& c : cl) c = static_cast(std::tolower(static_cast(c))); - if (cl.rfind("/cast ", 0) != 0 && cl != "/cast") continue; - size_t sp2 = cmdLine.find(' '); - if (sp2 == std::string::npos) continue; - showArg = cmdLine.substr(sp2 + 1); - // Strip conditionals [...] - if (!showArg.empty() && showArg.front() == '[') { - size_t ce = showArg.find(']'); - if (ce != std::string::npos) showArg = showArg.substr(ce + 1); - } - // Take first alternative before ';' - size_t semi = showArg.find(';'); - if (semi != std::string::npos) showArg = showArg.substr(0, semi); - // Trim and strip '!' - size_t ss = showArg.find_first_not_of(" \t!"); - if (ss != std::string::npos) showArg = showArg.substr(ss); - size_t se = showArg.find_last_not_of(" \t"); - if (se != std::string::npos) showArg.resize(se + 1); - break; - } - } - // Look up the spell icon by name - if (!showArg.empty() && showArg != "__auto__") { - std::string showLower = showArg; - for (char& c : showLower) c = static_cast(std::tolower(static_cast(c))); - // Also strip "(Rank N)" suffix for matching - size_t rankParen = showLower.find('('); - if (rankParen != std::string::npos) showLower.resize(rankParen); - while (!showLower.empty() && showLower.back() == ' ') showLower.pop_back(); - for (uint32_t sid : gameHandler.getKnownSpells()) { - const std::string& sn = gameHandler.getSpellName(sid); - if (sn.empty()) continue; - std::string snl = sn; - for (char& c : snl) c = static_cast(std::tolower(static_cast(c))); - if (snl == showLower) { - iconTex = assetMgr ? getSpellIcon(sid, assetMgr) : VK_NULL_HANDLE; - if (iconTex) break; - } - } - } - } - } - // Item-missing check: grey out item slots whose item is not in the player's inventory. const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0 && barItemDef == nullptr && !onCooldown); @@ -8570,25 +7446,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } - // Error-flash overlay: red fade on spell cast failure (~0.5 s). - if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) { - auto flashIt = actionFlashEndTimes_.find(slot.id); - if (flashIt != actionFlashEndTimes_.end()) { - float now = static_cast(ImGui::GetTime()); - float remaining = flashIt->second - now; - if (remaining > 0.0f) { - float alpha = remaining / kActionFlashDuration; // 1→0 - ImVec2 rMin = ImGui::GetItemRectMin(); - ImVec2 rMax = ImGui::GetItemRectMax(); - ImGui::GetWindowDrawList()->AddRectFilled( - rMin, rMax, - ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.1f, 0.1f, 0.55f * alpha))); - } else { - actionFlashEndTimes_.erase(flashIt); - } - } - } - bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseReleased(ImGuiMouseButton_Left); @@ -8614,8 +7471,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { gameHandler.castSpell(slot.id, target); } else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { gameHandler.useItemById(slot.id); - } else if (slot.type == game::ActionBarSlot::MACRO) { - executeMacroText(gameHandler, gameHandler.getMacroText(slot.id)); } } @@ -8644,19 +7499,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (ImGui::MenuItem("Use")) { gameHandler.useItemById(slot.id); } - } else if (slot.type == game::ActionBarSlot::MACRO) { - ImGui::TextDisabled("Macro #%u", slot.id); - ImGui::Separator(); - if (ImGui::MenuItem("Execute")) { - executeMacroText(gameHandler, gameHandler.getMacroText(slot.id)); - } - if (ImGui::MenuItem("Edit")) { - const std::string& txt = gameHandler.getMacroText(slot.id); - strncpy(macroEditorBuf_, txt.c_str(), sizeof(macroEditorBuf_) - 1); - macroEditorBuf_[sizeof(macroEditorBuf_) - 1] = '\0'; - macroEditorId_ = slot.id; - macroEditorOpen_ = true; - } } ImGui::Separator(); if (ImGui::MenuItem("Clear Slot")) { @@ -8715,17 +7557,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1f sec", cd); } ImGui::EndTooltip(); - } else if (slot.type == game::ActionBarSlot::MACRO) { - ImGui::BeginTooltip(); - ImGui::Text("Macro #%u", slot.id); - const std::string& macroText = gameHandler.getMacroText(slot.id); - if (!macroText.empty()) { - ImGui::Separator(); - ImGui::TextUnformatted(macroText.c_str()); - } else { - ImGui::TextDisabled("(no text — right-click to Edit)"); - } - ImGui::EndTooltip(); } else if (slot.type == game::ActionBarSlot::ITEM) { ImGui::BeginTooltip(); // Prefer full rich tooltip from ItemQueryResponseData (has stats, quality, set info) @@ -8937,28 +7768,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (i > 0) ImGui::SameLine(0, spacing); renderBarSlot(i, keyLabels1[i]); } - - // Macro editor modal — opened by "Edit" in action bar context menus - if (macroEditorOpen_) { - ImGui::OpenPopup("Edit Macro###MacroEdit"); - macroEditorOpen_ = false; - } - if (ImGui::BeginPopupModal("Edit Macro###MacroEdit", nullptr, - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) { - ImGui::Text("Macro #%u (all lines execute; [cond] Spell; Default supported)", macroEditorId_); - ImGui::SetNextItemWidth(320.0f); - ImGui::InputTextMultiline("##MacroText", macroEditorBuf_, sizeof(macroEditorBuf_), - ImVec2(320.0f, 80.0f)); - if (ImGui::Button("Save")) { - gameHandler.setMacroText(macroEditorId_, std::string(macroEditorBuf_)); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } } ImGui::End(); @@ -9857,32 +8666,13 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) { } } - // Queued spell icon (right edge): the next spell queued to fire within 400ms. - uint32_t queuedId = gameHandler.getQueuedSpellId(); - VkDescriptorSet queuedTex = (queuedId != 0 && assetMgr) - ? getSpellIcon(queuedId, assetMgr) : VK_NULL_HANDLE; - - const float iconSz = 20.0f; - const float reservedRight = (queuedTex) ? (iconSz + 4.0f) : 0.0f; - if (iconTex) { // Spell icon to the left of the progress bar - ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(iconSz, iconSz)); + ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(20, 20)); ImGui::SameLine(0, 4); - ImGui::ProgressBar(progress, ImVec2(-reservedRight - 1.0f, iconSz), overlay); + ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); } else { - ImGui::ProgressBar(progress, ImVec2(-reservedRight - 1.0f, iconSz), overlay); - } - // Draw queued-spell icon on the right with a ">" arrow prefix tooltip. - if (queuedTex) { - ImGui::SameLine(0, 4); - ImGui::Image((ImTextureID)(uintptr_t)queuedTex, ImVec2(iconSz, iconSz), - ImVec2(0,0), ImVec2(1,1), - ImVec4(1,1,1,0.8f), ImVec4(0,0,0,0)); // slightly dimmed - if (ImGui::IsItemHovered()) { - const std::string& qn = gameHandler.getSpellName(queuedId); - ImGui::SetTooltip("Queued: %s", qn.empty() ? "Unknown" : qn.c_str()); - } + ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); } ImGui::PopStyleColor(); } @@ -10788,9 +9578,6 @@ void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) { void GameScreen::renderNameplates(game::GameHandler& gameHandler) { if (gameHandler.getState() != game::WorldState::IN_WORLD) return; - // Reset mouseover each frame; we'll set it below when the cursor is over a nameplate - gameHandler.setMouseoverGuid(0); - auto* appRenderer = core::Application::getInstance().getRenderer(); if (!appRenderer) return; rendering::Camera* camera = appRenderer->getCamera(); @@ -11165,8 +9952,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { float nx1 = nameX + textSize.x + 2.0f; float ny1 = sy + barH + 2.0f; if (mouse.x >= nx0 && mouse.x <= nx1 && mouse.y >= ny0 && mouse.y <= ny1) { - // Track mouseover for [target=mouseover] macro conditionals - gameHandler.setMouseoverGuid(guid); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { gameHandler.setTarget(guid); } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { @@ -11500,9 +10285,6 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (ImGui::InvisibleButton("raidCell", ImVec2(CELL_W, CELL_H))) { gameHandler.setTarget(m.guid); } - if (ImGui::IsItemHovered()) { - gameHandler.setMouseoverGuid(m.guid); - } if (ImGui::BeginPopupContextItem("RaidMemberCtx")) { ImGui::TextDisabled("%s", m.name.c_str()); ImGui::Separator(); @@ -11613,10 +10395,6 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) { gameHandler.setTarget(member.guid); } - // Set mouseover for [target=mouseover] macro conditionals - if (ImGui::IsItemHovered()) { - gameHandler.setMouseoverGuid(member.guid); - } // Zone tooltip on name hover if (ImGui::IsItemHovered() && member.hasPartyStats && member.zoneId != 0) { std::string zoneName = gameHandler.getWhoAreaName(member.zoneId); @@ -16586,10 +15364,8 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); // "Release Spirit" dialog centered on screen - const bool hasSelfRes = gameHandler.canSelfRes(); float dlgW = 280.0f; - // Extra height when self-res button is available; +20 for the "wait for res" hint - float dlgH = hasSelfRes ? 190.0f : 150.0f; + float dlgH = 130.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); @@ -16608,13 +15384,13 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::SetCursorPosX((dlgW - textW) / 2); ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText); - // Respawn timer: show how long until the server auto-releases the spirit + // Respawn timer: show how long until forced release float timeLeft = kForcedReleaseSec - deathElapsed_; if (timeLeft > 0.0f) { int mins = static_cast(timeLeft) / 60; int secs = static_cast(timeLeft) % 60; char timerBuf[48]; - snprintf(timerBuf, sizeof(timerBuf), "Auto-release in %d:%02d", mins, secs); + snprintf(timerBuf, sizeof(timerBuf), "Release in %d:%02d", mins, secs); float tw = ImGui::CalcTextSize(timerBuf).x; ImGui::SetCursorPosX((dlgW - tw) / 2); ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "%s", timerBuf); @@ -16623,19 +15399,6 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::Spacing(); ImGui::Spacing(); - // Self-resurrection button (Reincarnation / Twisting Nether / Deathpact) - if (hasSelfRes) { - float btnW2 = 220.0f; - ImGui::SetCursorPosX((dlgW - btnW2) / 2); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.55f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.5f, 0.75f, 1.0f)); - if (ImGui::Button("Use Self-Resurrection", ImVec2(btnW2, 30))) { - gameHandler.useSelfRes(); - } - ImGui::PopStyleColor(2); - ImGui::Spacing(); - } - // Center the Release Spirit button float btnW = 180.0f; ImGui::SetCursorPosX((dlgW - btnW) / 2); @@ -16645,12 +15408,6 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { gameHandler.releaseSpirit(); } ImGui::PopStyleColor(2); - - // Hint: player can stay dead and wait for another player to cast Resurrection - const char* resHint = "Or wait for a player to resurrect you."; - float hw = ImGui::CalcTextSize(resHint).x; - ImGui::SetCursorPosX((dlgW - hw) / 2); - ImGui::TextColored(ImVec4(0.5f, 0.6f, 0.5f, 0.85f), "%s", resHint); } ImGui::End(); ImGui::PopStyleColor(2); @@ -16664,48 +15421,28 @@ void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) { float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; - float delaySec = gameHandler.getCorpseReclaimDelaySec(); - bool onDelay = (delaySec > 0.0f); - float btnW = 220.0f, btnH = 36.0f; - float winH = btnH + 16.0f + (onDelay ? 20.0f : 0.0f); ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, winH), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, btnH + 16.0f), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 8.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.7f)); if (ImGui::Begin("##ReclaimCorpse", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus)) { - if (onDelay) { - // Greyed-out button while PvP reclaim timer ticks down - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.25f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f)); - ImGui::BeginDisabled(true); - char delayLabel[64]; - snprintf(delayLabel, sizeof(delayLabel), "Resurrect from Corpse (%.0fs)", delaySec); - ImGui::Button(delayLabel, ImVec2(btnW, btnH)); - ImGui::EndDisabled(); - ImGui::PopStyleColor(2); - const char* waitMsg = "You cannot reclaim your corpse yet."; - float tw = ImGui::CalcTextSize(waitMsg).x; - ImGui::SetCursorPosX((btnW + 16.0f - tw) * 0.5f); - ImGui::TextColored(ImVec4(0.8f, 0.5f, 0.2f, 1.0f), "%s", waitMsg); - } else { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f)); - if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) { - gameHandler.reclaimCorpse(); - } - ImGui::PopStyleColor(2); - float corpDist = gameHandler.getCorpseDistance(); - if (corpDist >= 0.0f) { - char distBuf[48]; - snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist); - float dw = ImGui::CalcTextSize(distBuf).x; - ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f); - ImGui::TextDisabled("%s", distBuf); - } + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f)); + if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) { + gameHandler.reclaimCorpse(); + } + ImGui::PopStyleColor(2); + float corpDist = gameHandler.getCorpseDistance(); + if (corpDist >= 0.0f) { + char distBuf[48]; + snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist); + float dw = ImGui::CalcTextSize(distBuf).x; + ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f); + ImGui::TextDisabled("%s", distBuf); } } ImGui::End(); @@ -17053,11 +15790,18 @@ void GameScreen::renderSettingsWindow() { } } { - if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) { + bool fsrActive = renderer && (renderer->isFSREnabled() || renderer->isFSR2Enabled()); + if (!fsrActive && pendingWaterRefraction) { + // FSR was disabled while refraction was on — auto-disable + pendingWaterRefraction = false; + if (renderer) renderer->setWaterRefractionEnabled(false); + } + if (!fsrActive) ImGui::BeginDisabled(); + if (ImGui::Checkbox("Water Refraction (requires FSR)", &pendingWaterRefraction)) { if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction); - updateGraphicsPresetFromCurrentSettings(); saveSettings(); } + if (!fsrActive) ImGui::EndDisabled(); } { const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" }; diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index b4e2ac89..083096a7 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -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(gold), - static_cast(silver), - static_cast(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(gold), + static_cast(silver), + static_cast(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(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 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 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 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 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(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 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 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)