diff --git a/assets/shaders/minimap_display.frag.glsl b/assets/shaders/minimap_display.frag.glsl index 3f4d42e4..476017b9 100644 --- a/assets/shaders/minimap_display.frag.glsl +++ b/assets/shaders/minimap_display.frag.glsl @@ -40,7 +40,7 @@ void main() { float cs = cos(push.rotation); float sn = sin(push.rotation); vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs); - vec2 mapUV = push.playerUV + vec2(-rotated.x, rotated.y) * push.zoomRadius * 2.0; + vec2 mapUV = push.playerUV + vec2(rotated.x, rotated.y) * push.zoomRadius * 2.0; vec4 mapColor = texture(uComposite, mapUV); diff --git a/assets/shaders/minimap_display.frag.spv b/assets/shaders/minimap_display.frag.spv index 5c0ac7b0..f33deef2 100644 Binary files a/assets/shaders/minimap_display.frag.spv and b/assets/shaders/minimap_display.frag.spv differ diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 11223bbe..e8762167 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -786,6 +786,12 @@ public: float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastTimeRemaining() const { return castTimeRemaining; } + // Repeat-craft queue + void startCraftQueue(uint32_t spellId, int count); + void cancelCraftQueue(); + int getCraftQueueRemaining() const { return craftQueueRemaining_; } + uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; } + // Unit cast state (tracked per GUID for target frame + boss frames) struct UnitCastState { bool casting = false; @@ -970,6 +976,7 @@ public: const std::map& getPlayerSkills() const { return playerSkills_; } const std::string& getSkillName(uint32_t skillId) const; uint32_t getSkillCategory(uint32_t skillId) const; + bool isProfessionSpell(uint32_t spellId) const; // World entry callback (online mode - triggered when entering world) // Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect @@ -2669,6 +2676,9 @@ private: bool castIsChannel = false; uint32_t currentCastSpellId = 0; float castTimeRemaining = 0.0f; + // Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes + uint32_t craftQueueSpellId_ = 0; + int craftQueueRemaining_ = 0; // Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START) std::unordered_map unitCastStates_; uint64_t pendingGameObjectInteractGuid_ = 0; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index fe033101..b229dc80 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -26,6 +26,10 @@ public: // Classic: none, TBC: u8, WotLK: u16. virtual uint8_t movementFlags2Size() const { return 2; } + // Wire-format movement flag that gates transport data in MSG_MOVE_* payloads. + // WotLK/TBC: 0x200, Classic/Turtle: 0x02000000. + virtual uint32_t wireOnTransportFlag() const { return 0x00000200; } + // --- Movement --- /** Parse movement block from SMSG_UPDATE_OBJECT */ @@ -361,6 +365,20 @@ public: // TBC/Classic SMSG_QUESTGIVER_QUEST_DETAILS lacks informUnit(u64), flags(u32), // isFinished(u8) that WotLK added; uses variable item counts + emote section. bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override; + // TBC 2.4.3 SMSG_GUILD_ROSTER: same rank structure as WotLK (variable rankCount + + // goldLimit + bank tabs), but NO gender byte per member (WotLK added it) + bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override; + // TBC 2.4.3 SMSG_QUESTGIVER_STATUS: uint32 status (WotLK uses uint8) + uint8_t readQuestGiverStatus(network::Packet& packet) override; + // TBC 2.4.3 SMSG_MESSAGECHAT: no senderGuid/unknown prefix before type-specific data + bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; + // TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE: 2 extra strings after names + // (iconName + castBarCaption); WotLK has 3 (adds unk1) + bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override; + // TBC 2.4.3 CMSG_JOIN_CHANNEL: name+password only (WotLK prepends channelId+hasVoice+joinedByZone) + network::Packet buildJoinChannel(const std::string& channelName, const std::string& password) override; + // TBC 2.4.3 CMSG_LEAVE_CHANNEL: name only (WotLK prepends channelId) + network::Packet buildLeaveChannel(const std::string& channelName) override; }; /** @@ -380,6 +398,7 @@ public: class ClassicPacketParsers : public TbcPacketParsers { public: uint8_t movementFlags2Size() const override { return 0; } + uint32_t wireOnTransportFlag() const override { return 0x02000000; } bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override; bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override; void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override; diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index ffaf6bb2..10c4a5cd 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -74,6 +74,7 @@ struct CombatLogEntry { int32_t amount = 0; uint32_t spellId = 0; bool isPlayerSource = false; + uint8_t powerType = 0; // For ENERGIZE/DRAIN: power type; for ENVIRONMENTAL: env damage type time_t timestamp = 0; // Wall-clock time (std::time(nullptr)) std::string sourceName; // Resolved display name of attacker/caster std::string targetName; // Resolved display name of victim/target diff --git a/include/game/warden_emulator.hpp b/include/game/warden_emulator.hpp index 320afd0d..c3dbc37c 100644 --- a/include/game/warden_emulator.hpp +++ b/include/game/warden_emulator.hpp @@ -152,6 +152,7 @@ private: // Memory allocation tracking std::map allocations_; + std::map freeBlocks_; // free-list keyed by base address uint32_t nextHeapAddr_; // Hook handles for cleanup diff --git a/include/game/warden_memory.hpp b/include/game/warden_memory.hpp index d01a27c3..39a2abf2 100644 --- a/include/game/warden_memory.hpp +++ b/include/game/warden_memory.hpp @@ -41,10 +41,12 @@ public: * @param expectedHash 20-byte expected HMAC-SHA1 digest * @param patternLen Length of the pattern to search for * @param imageOnly If true, search only executable sections (.text) + * @param hintOffset RVA hint from PAGE_A request — check this position first * @return true if a matching pattern was found in the PE image */ bool searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], - uint8_t patternLen, bool imageOnly) const; + uint8_t patternLen, bool imageOnly, + uint32_t hintOffset = 0, bool hintOnly = false) const; /** Write a little-endian uint32 at the given virtual address in the PE image. */ void writeLE32(uint32_t va, uint32_t value); diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 293953ea..c2e92f06 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2060,8 +2060,9 @@ public: /** SMSG_LOOT_RESPONSE parser */ class LootResponseParser { public: - // isWotlkFormat: true for WotLK 3.3.5a (22 bytes/item with randomSuffix+randomProp), - // false for Classic 1.12 and TBC 2.4.3 (14 bytes/item). + // isWotlkFormat: true for WotLK (has trailing quest item section), + // false for Classic/TBC (no quest item section). + // Per-item size is 22 bytes across all expansions. static bool parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat = true); }; diff --git a/include/rendering/minimap.hpp b/include/rendering/minimap.hpp index ca7c5345..906f4666 100644 --- a/include/rendering/minimap.hpp +++ b/include/rendering/minimap.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace wowee { @@ -73,7 +74,10 @@ private: bool trsParsed = false; // Tile texture cache: hash → VkTexture + // Evicted (FIFO) when the count of successfully-loaded tiles exceeds MAX_TILE_CACHE. + static constexpr size_t MAX_TILE_CACHE = 128; std::unordered_map> tileTextureCache; + std::deque tileInsertionOrder; // hashes of successfully loaded tiles, oldest first std::unique_ptr noDataTexture; // Composite render target (3x3 tiles = 768x768) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index aed61820..07c6ebd6 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -381,6 +381,13 @@ private: void initOverlayPipeline(); void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE); + // Brightness (1.0 = default, <1 darkens, >1 brightens) + float brightness_ = 1.0f; +public: + void setBrightness(float b) { brightness_ = b; } + float getBrightness() const { return brightness_; } +private: + // FSR 1.0 upscaling state struct FSRState { bool enabled = false; diff --git a/include/rendering/world_map.hpp b/include/rendering/world_map.hpp index 77a98ec0..e908ffaa 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -51,7 +51,8 @@ public: void compositePass(VkCommandBuffer cmd); /// ImGui overlay — call INSIDE the main render pass (during ImGui frame). - void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); + void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, + float playerYawDeg = 0.0f); void setMapName(const std::string& name); void setServerExplorationMask(const std::vector& masks, bool hasData); @@ -71,7 +72,8 @@ private: float& top, float& bottom) const; void loadZoneTextures(int zoneIdx); void requestComposite(int zoneIdx); - void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); + void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, + float playerYawDeg); void updateExploration(const glm::vec3& playerRenderPos); void zoomIn(const glm::vec3& playerRenderPos); void zoomOut(); diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index a6ebf9c0..75502dee 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -171,6 +171,7 @@ private: bool pendingShadows = true; float pendingShadowDistance = 300.0f; bool pendingWaterRefraction = false; + int pendingBrightness = 50; // 0-100, maps to 0.0-2.0 (50 = 1.0 default) int pendingMasterVolume = 100; int pendingMusicVolume = 30; int pendingAmbientVolume = 100; @@ -192,6 +193,7 @@ private: bool pendingMinimapNpcDots = false; bool pendingShowLatencyMeter = true; bool pendingSeparateBags = true; + bool pendingShowKeyring = true; bool pendingAutoLoot = false; // Keybinding customization diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 65ef41c9..b9c30c6c 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -39,6 +39,8 @@ public: bool isSeparateBags() const { return separateBags_; } void toggleCompactBags() { compactBags_ = !compactBags_; } bool isCompactBags() const { return compactBags_; } + void setShowKeyring(bool show) { showKeyring_ = show; } + bool isShowKeyring() const { return showKeyring_; } bool isBackpackOpen() const { return backpackOpen_; } bool isBagOpen(int idx) const { return idx >= 0 && idx < 4 ? bagOpen_[idx] : false; } @@ -79,6 +81,7 @@ private: bool bKeyWasDown = false; bool separateBags_ = true; bool compactBags_ = false; + bool showKeyring_ = true; bool backpackOpen_ = false; std::array bagOpen_{}; bool cKeyWasDown = false; diff --git a/src/core/application.cpp b/src/core/application.cpp index 76556a6e..22e93abc 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -576,6 +576,12 @@ void Application::run() { void Application::shutdown() { LOG_WARNING("Shutting down application..."); + // Hide the window immediately so the OS doesn't think the app is frozen + // during the (potentially slow) resource cleanup below. + if (window && window->getSDLWindow()) { + SDL_HideWindow(window->getSDLWindow()); + } + // Stop background world preloader before destroying AssetManager cancelWorldPreload(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 222bfabe..7decf08c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -80,6 +80,28 @@ bool isAuthCharPipelineOpcode(LogicalOpcode op) { } } +// Build a WoW-format item link for use in system chat messages. +// The chat renderer in game_screen.cpp parses this format and draws the +// item name in its quality colour with a small icon and tooltip. +// Format: |cff|Hitem::0:0:0:0:0:0:0:0|h[]|h|r +std::string buildItemLink(uint32_t itemId, uint32_t quality, const std::string& name) { + static const char* kQualHex[] = { + "9d9d9d", // 0 Poor + "ffffff", // 1 Common + "1eff00", // 2 Uncommon + "0070dd", // 3 Rare + "a335ee", // 4 Epic + "ff8000", // 5 Legendary + "e6cc80", // 6 Artifact + "e6cc80", // 7 Heirloom + }; + uint32_t qi = quality < 8 ? quality : 1u; + char buf[512]; + snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r", + kQualHex[qi], itemId, name.c_str()); + return buf; +} + bool isActiveExpansion(const char* expansionId) { auto& app = core::Application::getInstance(); auto* registry = app.getExpansionRegistry(); @@ -1937,9 +1959,14 @@ void GameHandler::handlePacket(network::Packet& packet) { if (!info->name.empty()) itemName = info->name; quality = info->quality; } - std::string msg = "Received: " + itemName; + 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); } @@ -2002,6 +2029,8 @@ void GameHandler::handlePacket(network::Packet& packet) { msg = buf; } addSystemChatMessage(msg); + addCombatText(CombatTextEntry::XP_GAIN, + static_cast(xpGained), 0, true); // XP is updated via PLAYER_XP update fields from the server. if (areaDiscoveryCallback_) areaDiscoveryCallback_(areaName, xpGained); @@ -2217,6 +2246,9 @@ void GameHandler::handlePacket(network::Packet& packet) { currentCastSpellId = 0; castTimeRemaining = 0.0f; lastInteractedGoGuid_ = 0; + // Cancel craft queue on cast failure + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; // Pass player's power type so result 85 says "Not enough rage/energy/etc." int playerPowerType = -1; if (auto pe = entityManager.getEntity(playerGuid)) { @@ -2810,10 +2842,9 @@ void GameHandler::handlePacket(network::Packet& packet) { /*uint32_t randProp =*/ packet.readUInt32(); } auto* info = getItemInfo(itemId); - char buf[256]; - std::snprintf(buf, sizeof(buf), "Everyone passed on [%s].", - info ? info->name.c_str() : std::to_string(itemId).c_str()); - addSystemChatMessage(buf); + std::string allPassName = info && !info->name.empty() ? info->name : std::to_string(itemId); + uint32_t allPassQuality = info ? info->quality : 1u; + addSystemChatMessage("Everyone passed on " + buildItemLink(itemId, allPassQuality, allPassName) + "."); pendingLootRollActive_ = false; break; } @@ -2833,23 +2864,33 @@ void GameHandler::handlePacket(network::Packet& packet) { if (!looterName.empty()) { queryItemInfo(itemId, 0); std::string itemName = "item #" + std::to_string(itemId); + uint32_t notifyQuality = 1; if (const ItemQueryResponseData* info = getItemInfo(itemId)) { if (!info->name.empty()) itemName = info->name; + notifyQuality = info->quality; } - char buf[256]; - if (count > 1) - std::snprintf(buf, sizeof(buf), "%s loots %s x%u.", looterName.c_str(), itemName.c_str(), count); - else - std::snprintf(buf, sizeof(buf), "%s loots %s.", looterName.c_str(), itemName.c_str()); - addSystemChatMessage(buf); + std::string itemLink2 = buildItemLink(itemId, notifyQuality, itemName); + std::string lootMsg = looterName + " loots " + itemLink2; + if (count > 1) lootMsg += " x" + std::to_string(count); + lootMsg += "."; + addSystemChatMessage(lootMsg); } } break; } - case Opcode::SMSG_LOOT_SLOT_CHANGED: - // uint64 objectGuid + uint32 slot + ... — consume - packet.setReadPos(packet.getSize()); + case Opcode::SMSG_LOOT_SLOT_CHANGED: { + // uint8 slotIndex — another player took the item from this slot in group loot + if (packet.getSize() - packet.getReadPos() >= 1) { + uint8_t slotIndex = packet.readUInt8(); + for (auto it = currentLoot.items.begin(); it != currentLoot.items.end(); ++it) { + if (it->slotIndex == slotIndex) { + currentLoot.items.erase(it); + break; + } + } + } break; + } // ---- Spell log miss ---- case Opcode::SMSG_SPELLLOGMISS: { @@ -3885,6 +3926,10 @@ void GameHandler::handlePacket(network::Packet& packet) { addSystemChatMessage("You have learned " + name + "."); else addSystemChatMessage("Spell learned."); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playQuestActivate(); + } break; } case Opcode::SMSG_TRAINER_BUY_FAILED: { @@ -3911,6 +3956,11 @@ void GameHandler::handlePacket(network::Packet& packet) { else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")"; addSystemChatMessage(msg); + // Play error sound so the player notices the failure + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playError(); + } break; } @@ -4247,17 +4297,17 @@ void GameHandler::handlePacket(network::Packet& packet) { } case Opcode::SMSG_ENVIRONMENTAL_DAMAGE_LOG: { // uint64 victimGuid + uint8 envDmgType + uint32 damage + uint32 absorbed + uint32 resisted - // envDmgType: 1=Exhausted(fatigue), 2=Drowning, 3=Fall, 4=Lava, 5=Slime, 6=Fire + // envDmgType: 0=Exhausted(fatigue), 1=Drowning, 2=Fall, 3=Lava, 4=Slime, 5=Fire if (packet.getSize() - packet.getReadPos() < 21) { packet.setReadPos(packet.getSize()); break; } uint64_t victimGuid = packet.readUInt64(); - /*uint8_t envType =*/ packet.readUInt8(); + uint8_t envType = packet.readUInt8(); uint32_t dmg = packet.readUInt32(); uint32_t envAbs = packet.readUInt32(); uint32_t envRes = packet.readUInt32(); if (victimGuid == playerGuid) { - // Environmental damage: no caster GUID, victim = player + // Environmental damage: pass envType via powerType field for display differentiation if (dmg > 0) - addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast(dmg), 0, false, 0, 0, victimGuid); + addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast(dmg), 0, false, envType, 0, victimGuid); if (envAbs > 0) addCombatText(CombatTextEntry::ABSORB, static_cast(envAbs), 0, false, 0, 0, victimGuid); if (envRes > 0) @@ -4368,6 +4418,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } if (newLevel > oldLevel) { addSystemChatMessage("You have reached level " + std::to_string(newLevel) + "!"); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playLevelUp(); + } if (levelUpCallback_) levelUpCallback_(newLevel); } } @@ -4485,6 +4539,10 @@ void GameHandler::handlePacket(network::Packet& packet) { " result=", static_cast(result)); if (result == 0) { pendingSellToBuyback_.erase(itemGuid); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playDropOnGround(); + } } else { bool removedPending = false; auto it = pendingSellToBuyback_.find(itemGuid); @@ -4521,6 +4579,10 @@ void GameHandler::handlePacket(network::Packet& packet) { }; const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error"; addSystemChatMessage(std::string("Sell failed: ") + msg); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playError(); + } LOG_WARNING("SMSG_SELL_ITEM error: ", (int)result, " (", msg, ")"); } } @@ -4614,6 +4676,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; addSystemChatMessage(msg); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playError(); + } } } break; @@ -4674,6 +4740,10 @@ void GameHandler::handlePacket(network::Packet& packet) { default: break; } addSystemChatMessage(msg); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playError(); + } } break; } @@ -4715,12 +4785,19 @@ void GameHandler::handlePacket(network::Packet& packet) { // Show purchase confirmation with item name if available if (pendingBuyItemId_ != 0) { std::string itemLabel; - if (const ItemQueryResponseData* info = getItemInfo(pendingBuyItemId_)) + uint32_t buyQuality = 1; + if (const ItemQueryResponseData* info = getItemInfo(pendingBuyItemId_)) { if (!info->name.empty()) itemLabel = info->name; + buyQuality = info->quality; + } if (itemLabel.empty()) itemLabel = "item #" + std::to_string(pendingBuyItemId_); - std::string msg = "Purchased: " + itemLabel; + std::string msg = "Purchased: " + buildItemLink(pendingBuyItemId_, buyQuality, itemLabel); if (itemCount > 1) msg += " x" + std::to_string(itemCount); addSystemChatMessage(msg); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playPickupBag(); + } } pendingBuyItemId_ = 0; pendingBuyItemSlot_ = 0; @@ -5001,6 +5078,11 @@ void GameHandler::handlePacket(network::Packet& packet) { if (questCompleteCallback_) { questCompleteCallback_(questId, it->title); } + // Play quest-complete sound + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playQuestComplete(); + } questLog_.erase(it); LOG_INFO(" Removed quest ", questId, " from quest log"); break; @@ -5103,8 +5185,10 @@ void GameHandler::handlePacket(network::Packet& packet) { queryItemInfo(itemId, 0); std::string itemLabel = "item #" + std::to_string(itemId); + uint32_t questItemQuality = 1; if (const ItemQueryResponseData* info = getItemInfo(itemId)) { if (!info->name.empty()) itemLabel = info->name; + questItemQuality = info->quality; } bool updatedAny = false; @@ -5128,7 +5212,7 @@ void GameHandler::handlePacket(network::Packet& packet) { quest.itemCounts[itemId] = count; updatedAny = true; } - addSystemChatMessage("Quest item: " + itemLabel + " (" + std::to_string(count) + ")"); + addSystemChatMessage("Quest item: " + buildItemLink(itemId, questItemQuality, itemLabel) + " (" + std::to_string(count) + ")"); if (questProgressCallback_ && updatedAny) { // Find the quest that tracks this item to get title and required count @@ -5848,13 +5932,15 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t itemEntry = packet.readUInt32(); ensureItemInfo(itemEntry); auto* info = getItemInfo(itemEntry); - std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry)); + std::string rawName = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); + uint32_t aucQuality = info ? info->quality : 1u; + std::string itemLink = buildItemLink(itemEntry, aucQuality, rawName); if (action == 1) - addSystemChatMessage("Your auction of " + itemName + " has expired."); + addSystemChatMessage("Your auction of " + itemLink + " has expired."); else if (action == 2) - addSystemChatMessage("A bid has been placed on your auction of " + itemName + "."); + addSystemChatMessage("A bid has been placed on your auction of " + itemLink + "."); else - addSystemChatMessage("Your auction of " + itemName + " has sold!"); + addSystemChatMessage("Your auction of " + itemLink + " has sold!"); } packet.setReadPos(packet.getSize()); break; @@ -5867,8 +5953,10 @@ void GameHandler::handlePacket(network::Packet& packet) { (void)auctionId; ensureItemInfo(itemEntry); auto* info = getItemInfo(itemEntry); - std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry)); - addSystemChatMessage("You have been outbid on " + itemName + "."); + std::string rawName2 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); + uint32_t bidQuality = info ? info->quality : 1u; + std::string bidLink = buildItemLink(itemEntry, bidQuality, rawName2); + addSystemChatMessage("You have been outbid on " + bidLink + "."); } packet.setReadPos(packet.getSize()); break; @@ -5881,8 +5969,10 @@ void GameHandler::handlePacket(network::Packet& packet) { /*uint32_t itemRandom =*/ packet.readUInt32(); ensureItemInfo(itemEntry); auto* info = getItemInfo(itemEntry); - std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry)); - addSystemChatMessage("Your auction of " + itemName + " has expired."); + std::string rawName3 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); + uint32_t remQuality = info ? info->quality : 1u; + std::string remLink = buildItemLink(itemEntry, remQuality, rawName3); + addSystemChatMessage("Your auction of " + remLink + " has expired."); } packet.setReadPos(packet.getSize()); break; @@ -5996,6 +6086,9 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Talents involuntarily reset ---- case Opcode::SMSG_TALENTS_INVOLUNTARILY_RESET: + // Clear cached talent data so the talent screen reflects the reset. + learnedTalents_[0].clear(); + learnedTalents_[1].clear(); addSystemChatMessage("Your talents have been reset by the server."); packet.setReadPos(packet.getSize()); break; @@ -6774,6 +6867,16 @@ void GameHandler::handlePacket(network::Packet& packet) { addSystemChatMessage(msg); LOG_DEBUG("SMSG_SPELLLOGEXECUTE CREATE_ITEM: spell=", exeSpellId, " item=", itemEntry, " name=", itemName); + + // Repeat-craft queue: re-cast if more crafts remaining + if (craftQueueRemaining_ > 0 && craftQueueSpellId_ == exeSpellId) { + --craftQueueRemaining_; + if (craftQueueRemaining_ > 0) { + castSpell(craftQueueSpellId_, 0); + } else { + craftQueueSpellId_ = 0; + } + } } } } else if (effectType == 26) { @@ -6807,7 +6910,8 @@ void GameHandler::handlePacket(network::Packet& packet) { const ItemQueryResponseData* info = getItemInfo(feedItem); std::string itemName = info && !info->name.empty() ? info->name : ("item #" + std::to_string(feedItem)); - addSystemChatMessage("You feed your pet " + itemName + "."); + uint32_t feedQuality = info ? info->quality : 1u; + addSystemChatMessage("You feed your pet " + buildItemLink(feedItem, feedQuality, itemName) + "."); LOG_DEBUG("SMSG_SPELLLOGEXECUTE FEED_PET: item=", feedItem, " name=", itemName); } } @@ -7199,10 +7303,35 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } - // ---- Real group update (status flags) ---- - case Opcode::SMSG_REAL_GROUP_UPDATE: - packet.setReadPos(packet.getSize()); + // ---- Real group update (group type, local player flags, leader) ---- + // Sent when the player's group configuration changes: group type, + // role/flags (assistant/MT/MA), or leader changes. + // Format: uint8 groupType | uint32 memberFlags | uint64 leaderGuid + case Opcode::SMSG_REAL_GROUP_UPDATE: { + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + if (rem() < 1) break; + uint8_t newGroupType = packet.readUInt8(); + if (rem() < 4) break; + uint32_t newMemberFlags = packet.readUInt32(); + if (rem() < 8) break; + uint64_t newLeaderGuid = packet.readUInt64(); + + partyData.groupType = newGroupType; + partyData.leaderGuid = newLeaderGuid; + + // Update local player's flags in the member list + uint64_t localGuid = playerGuid; + for (auto& m : partyData.members) { + if (m.guid == localGuid) { + m.flags = static_cast(newMemberFlags & 0xFF); + break; + } + } + LOG_DEBUG("SMSG_REAL_GROUP_UPDATE groupType=", static_cast(newGroupType), + " memberFlags=0x", std::hex, newMemberFlags, std::dec, + " leaderGuid=", newLeaderGuid); break; + } // ---- Play music (WotLK standard opcode) ---- case Opcode::SMSG_PLAY_MUSIC: { @@ -9135,7 +9264,7 @@ bool GameHandler::loadWardenCRFile(const std::string& moduleHashHex) { for (int i = 0; i < 9; i++) { char s[16]; snprintf(s, sizeof(s), "%s=0x%02X ", names[i], wardenCheckOpcodes_[i]); opcHex += s; } - LOG_DEBUG("Warden: Check opcodes: ", opcHex); + LOG_WARNING("Warden: Check opcodes: ", opcHex); } size_t entryCount = (static_cast(fileSize) - CR_HEADER_SIZE) / CR_ENTRY_SIZE; @@ -9542,6 +9671,10 @@ void GameHandler::handleWardenData(network::Packet& packet) { switch (ct) { case CT_TIMING: { + // Result byte: 0x01 = timing check ran successfully, + // 0x00 = timing check failed (Wine/VM — server skips anti-AFK). + // We return 0x01 so the server validates normally; our + // LastHardwareAction (now-2000) ensures a clean 2s delta. resultData.push_back(0x01); uint32_t ticks = static_cast( std::chrono::duration_cast( @@ -9561,7 +9694,8 @@ void GameHandler::handleWardenData(network::Packet& packet) { pos += 4; uint8_t readLen = decrypted[pos++]; LOG_WARNING("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), - " len=", (int)readLen); + " len=", (int)readLen, + (strIdx ? " module=\"" + moduleName + "\"" : "")); if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { uint32_t now = static_cast( std::chrono::duration_cast( @@ -9588,21 +9722,12 @@ void GameHandler::handleWardenData(network::Packet& packet) { LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet"); resultData.push_back(0x00); resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); - } else if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { - // Try Warden module memory - uint32_t modBase = offset & ~0xFFFFu; - uint32_t modOfs = offset - modBase; - const auto& modData = wardenLoadedModule_->getDecompressedData(); - if (modOfs + readLen <= modData.size()) { - std::memcpy(memBuf.data(), modData.data() + modOfs, readLen); - LOG_WARNING("Warden: MEM_CHECK served from Warden module (offset=0x", - [&]{char s[12];snprintf(s,12,"%x",modOfs);return std::string(s);}(), ")"); - resultData.push_back(0x00); - resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); - } else { - resultData.push_back(0xE9); - } } else { + // Address not in PE/KUSER — return 0xE9 (not readable). + // Real 32-bit WoW can't read kernel space (>=0x80000000) + // or arbitrary unallocated user-space addresses. + LOG_WARNING("Warden: MEM_CHECK -> 0xE9 (unmapped 0x", + [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); resultData.push_back(0xE9); } break; @@ -9620,22 +9745,15 @@ void GameHandler::handleWardenData(network::Packet& packet) { uint8_t patLen = p[28]; bool found = false; bool turtleFallback = false; - // Turtle fallback: if offset is within PE image range, - // this is an integrity check — skip the expensive 25-second - // brute-force search and return "found" immediately to stay - // within the server's Warden response timeout. - bool canTurtleFallback = (ct == CT_PAGE_A && isActiveExpansion("turtle") && - wardenMemory_ && wardenMemory_->isLoaded() && off < 0x600000); if (isKnownWantedCodeScan(seed, sha1, off, patLen)) { found = true; - } else if (canTurtleFallback) { - // Skip the expensive 25-second brute-force search; - // the turtle fallback will return "found" instantly. - found = true; - turtleFallback = true; } else if (wardenMemory_ && wardenMemory_->isLoaded() && patLen > 0) { - found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly); - if (!found && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { + // Hint + nearby window search (instant). + // Skip full brute-force for Turtle PAGE_A to avoid + // 25s delay that triggers response timeout. + bool hintOnly = (ct == CT_PAGE_A && isActiveExpansion("turtle")); + found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly, off, hintOnly); + if (!found && !hintOnly && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { const uint8_t* modMem = static_cast(wardenLoadedModule_->getModuleMemory()); size_t modSize = wardenLoadedModule_->getModuleSize(); if (modMem && modSize >= patLen) { @@ -9647,6 +9765,13 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } } + // Turtle PAGE_A fallback: patterns at runtime-patched + // offsets don't exist in the on-disk PE. The server + // expects "found" for these code integrity checks. + if (!found && ct == CT_PAGE_A && isActiveExpansion("turtle") && off < 0x600000) { + found = true; + turtleFallback = true; + } uint8_t pageResult = found ? 0x4A : 0x00; LOG_WARNING("Warden: ", pageName, " offset=0x", [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}(), @@ -9703,9 +9828,23 @@ void GameHandler::handleWardenData(network::Packet& packet) { bool isWanted = hmacSha1Matches(sb, "KERNEL32.DLL", rh); std::string mn = isWanted ? "KERNEL32.DLL" : "?"; if (!isWanted) { + // Cheat modules (unwanted — report not found) if (hmacSha1Matches(sb,"WPESPY.DLL",rh)) mn = "WPESPY.DLL"; else if (hmacSha1Matches(sb,"TAMIA.DLL",rh)) mn = "TAMIA.DLL"; else if (hmacSha1Matches(sb,"PRXDRVPE.DLL",rh)) mn = "PRXDRVPE.DLL"; + else if (hmacSha1Matches(sb,"SPEEDHACK-I386.DLL",rh)) mn = "SPEEDHACK-I386.DLL"; + else if (hmacSha1Matches(sb,"D3DHOOK.DLL",rh)) mn = "D3DHOOK.DLL"; + else if (hmacSha1Matches(sb,"NJUMD.DLL",rh)) mn = "NJUMD.DLL"; + // System DLLs (wanted — report found) + else if (hmacSha1Matches(sb,"USER32.DLL",rh)) { mn = "USER32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"NTDLL.DLL",rh)) { mn = "NTDLL.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WS2_32.DLL",rh)) { mn = "WS2_32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WSOCK32.DLL",rh)) { mn = "WSOCK32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"ADVAPI32.DLL",rh)) { mn = "ADVAPI32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"SHELL32.DLL",rh)) { mn = "SHELL32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"GDI32.DLL",rh)) { mn = "GDI32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"OPENGL32.DLL",rh)) { mn = "OPENGL32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WINMM.DLL",rh)) { mn = "WINMM.DLL"; isWanted = true; } } uint8_t mr = isWanted ? 0x4A : 0x00; LOG_WARNING("Warden: MODULE \"", mn, "\" -> 0x", @@ -9886,7 +10025,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { switch (ct) { case CT_TIMING: { // No additional request data - // Response: [uint8 result=1][uint32 ticks] + // Response: [uint8 result][uint32 ticks] + // 0x01 = timing check ran successfully (server validates anti-AFK) + // 0x00 = timing failed (Wine/VM — server skips check but flags client) resultData.push_back(0x01); uint32_t ticks = static_cast( std::chrono::duration_cast( @@ -9895,6 +10036,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { resultData.push_back((ticks >> 8) & 0xFF); resultData.push_back((ticks >> 16) & 0xFF); resultData.push_back((ticks >> 24) & 0xFF); + LOG_WARNING("Warden: (sync) TIMING ticks=", ticks); break; } case CT_MEM: { @@ -9918,31 +10060,27 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } + // Dynamically update LastHardwareAction before reading + // (anti-AFK scan compares this timestamp against TIMING ticks) + if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { + uint32_t now = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + wardenMemory_->writeLE32(0xCF0BC8, now - 2000); + } + // Read bytes from PE image (includes patched runtime globals) std::vector memBuf(readLen, 0); if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) { LOG_DEBUG("Warden: MEM_CHECK served from PE image"); - } else if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { - // Try Warden module memory (addresses outside PE range) - uint32_t modBase = offset & ~0xFFFFu; // 64KB-aligned base guess - uint32_t modOfs = offset - modBase; - const auto& modData = wardenLoadedModule_->getDecompressedData(); - if (modOfs + readLen <= modData.size()) { - std::memcpy(memBuf.data(), modData.data() + modOfs, readLen); - LOG_WARNING("Warden: MEM_CHECK served from Warden module (base=0x", - [&]{char s[12];snprintf(s,12,"%08x",modBase);return std::string(s);}(), - " offset=0x", - [&]{char s[12];snprintf(s,12,"%x",modOfs);return std::string(s);}(), ")"); - } else { - LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x", - [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}()); - } + resultData.push_back(0x00); + resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); } else { - LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x", - [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}()); + // Address not in PE/KUSER — return 0xE9 (not readable). + LOG_WARNING("Warden: (sync) MEM_CHECK -> 0xE9 (unmapped 0x", + [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); + resultData.push_back(0xE9); } - resultData.push_back(0x00); - resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); break; } case CT_PAGE_A: { @@ -9988,18 +10126,29 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (isKnownWantedCodeScan(seedBytes, reqHash, off, len)) { pageResult = 0x4A; } else if (wardenMemory_ && wardenMemory_->isLoaded() && len > 0) { - if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true)) + if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true, off)) pageResult = 0x4A; } - // Turtle fallback for integrity checks + // Turtle PAGE_A fallback: runtime-patched offsets aren't in the + // on-disk PE. Server expects "found" for code integrity checks. if (pageResult == 0x00 && isActiveExpansion("turtle") && off < 0x600000) { pageResult = 0x4A; LOG_WARNING("Warden: PAGE_A turtle-fallback for offset=0x", [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}()); } } - LOG_DEBUG("Warden: PAGE_A request bytes=", consume, - " result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + if (consume >= 29) { + uint32_t off2 = uint32_t((decrypted.data()+pos)[24]) | (uint32_t((decrypted.data()+pos)[25])<<8) | + (uint32_t((decrypted.data()+pos)[26])<<16) | (uint32_t((decrypted.data()+pos)[27])<<24); + uint8_t len2 = (decrypted.data()+pos)[28]; + LOG_WARNING("Warden: (sync) PAGE_A offset=0x", + [&]{char s[12];snprintf(s,12,"%08x",off2);return std::string(s);}(), + " patLen=", (int)len2, + " result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + } else { + LOG_WARNING("Warden: (sync) PAGE_A (short ", consume, "b) result=0x", + [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + } pos += consume; resultData.push_back(pageResult); break; @@ -10048,7 +10197,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (pos + 1 > checkEnd) { pos = checkEnd; break; } uint8_t strIdx = decrypted[pos++]; std::string filePath = resolveWardenString(strIdx); - LOG_DEBUG("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); + LOG_WARNING("Warden: (sync) MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); bool found = false; std::vector hash(20, 0); @@ -10083,10 +10232,15 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } - // Response: [uint8 result][20 sha1] - // result=0 => found/success, result=1 => not found/failure - resultData.push_back(found ? 0x00 : 0x01); - resultData.insert(resultData.end(), hash.begin(), hash.end()); + // Response: result=0 + 20-byte SHA1 if found; result=1 (no hash) if not found. + // Server only reads 20 hash bytes when result==0; extra bytes corrupt parsing. + if (found) { + resultData.push_back(0x00); + resultData.insert(resultData.end(), hash.begin(), hash.end()); + } else { + resultData.push_back(0x01); + } + LOG_WARNING("Warden: (sync) MPQ result=", found ? "FOUND" : "NOT_FOUND"); break; } case CT_LUA: { @@ -10094,7 +10248,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (pos + 1 > checkEnd) { pos = checkEnd; break; } uint8_t strIdx = decrypted[pos++]; std::string luaVar = resolveWardenString(strIdx); - LOG_DEBUG("Warden: LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\""); + LOG_WARNING("Warden: (sync) LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\""); // Response: [uint8 result=0][uint16 len=0] // Lua string doesn't exist resultData.push_back(0x01); // not found @@ -10106,9 +10260,10 @@ void GameHandler::handleWardenData(network::Packet& packet) { pos += 24; // skip seed + sha1 uint8_t strIdx = decrypted[pos++]; std::string driverName = resolveWardenString(strIdx); - LOG_DEBUG("Warden: DRIVER=\"", (driverName.empty() ? "?" : driverName), "\""); - // Response: [uint8 result=1] (driver NOT found = clean) - resultData.push_back(0x01); + LOG_WARNING("Warden: (sync) DRIVER=\"", (driverName.empty() ? "?" : driverName), "\" -> 0x00(not found)"); + // Response: [uint8 result=0] (driver NOT found = clean) + // VMaNGOS: result != 0 means "found". 0x01 would mean VM driver detected! + resultData.push_back(0x00); break; } case CT_MODULE: { @@ -10126,23 +10281,34 @@ void GameHandler::handleWardenData(network::Packet& packet) { std::memcpy(reqHash, p + 4, 20); pos += moduleSize; - // CMaNGOS uppercases module names before hashing. - // DB module scans: - // KERNEL32.DLL (wanted=true) - // WPESPY.DLL / SPEEDHACK-I386.DLL / TAMIA.DLL (wanted=false) bool shouldReportFound = false; - if (hmacSha1Matches(seedBytes, "KERNEL32.DLL", reqHash)) { - shouldReportFound = true; - } else if (hmacSha1Matches(seedBytes, "WPESPY.DLL", reqHash) || - hmacSha1Matches(seedBytes, "SPEEDHACK-I386.DLL", reqHash) || - hmacSha1Matches(seedBytes, "TAMIA.DLL", reqHash)) { - shouldReportFound = false; - } - resultData.push_back(shouldReportFound ? 0x4A : 0x01); + std::string modName = "?"; + // Wanted system modules + if (hmacSha1Matches(seedBytes, "KERNEL32.DLL", reqHash)) { modName = "KERNEL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "USER32.DLL", reqHash)) { modName = "USER32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "NTDLL.DLL", reqHash)) { modName = "NTDLL.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WS2_32.DLL", reqHash)) { modName = "WS2_32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WSOCK32.DLL", reqHash)) { modName = "WSOCK32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "ADVAPI32.DLL", reqHash)) { modName = "ADVAPI32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "SHELL32.DLL", reqHash)) { modName = "SHELL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "GDI32.DLL", reqHash)) { modName = "GDI32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "OPENGL32.DLL", reqHash)) { modName = "OPENGL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WINMM.DLL", reqHash)) { modName = "WINMM.DLL"; shouldReportFound = true; } + // Unwanted cheat modules + else if (hmacSha1Matches(seedBytes, "WPESPY.DLL", reqHash)) modName = "WPESPY.DLL"; + else if (hmacSha1Matches(seedBytes, "SPEEDHACK-I386.DLL", reqHash)) modName = "SPEEDHACK-I386.DLL"; + else if (hmacSha1Matches(seedBytes, "TAMIA.DLL", reqHash)) modName = "TAMIA.DLL"; + else if (hmacSha1Matches(seedBytes, "PRXDRVPE.DLL", reqHash)) modName = "PRXDRVPE.DLL"; + else if (hmacSha1Matches(seedBytes, "D3DHOOK.DLL", reqHash)) modName = "D3DHOOK.DLL"; + else if (hmacSha1Matches(seedBytes, "NJUMD.DLL", reqHash)) modName = "NJUMD.DLL"; + LOG_WARNING("Warden: (sync) MODULE \"", modName, + "\" -> 0x", [&]{char s[4];snprintf(s,4,"%02x",shouldReportFound?0x4A:0x00);return std::string(s);}(), + "(", shouldReportFound ? "found" : "not found", ")"); + resultData.push_back(shouldReportFound ? 0x4A : 0x00); break; } // Truncated module request fallback: module NOT loaded = clean - resultData.push_back(0x01); + resultData.push_back(0x00); break; } case CT_PROC: { @@ -10151,12 +10317,23 @@ void GameHandler::handleWardenData(network::Packet& packet) { int procSize = 30; if (pos + procSize > checkEnd) { pos = checkEnd; break; } pos += procSize; + LOG_WARNING("Warden: (sync) PROC check -> 0x01(not found)"); // Response: [uint8 result=1] (proc NOT found = clean) resultData.push_back(0x01); break; } default: { - LOG_WARNING("Warden: Unknown check type, cannot parse remaining"); + uint8_t rawByte = decrypted[pos - 1]; + uint8_t decoded = rawByte ^ xorByte; + LOG_WARNING("Warden: Unknown check type raw=0x", + [&]{char s[4];snprintf(s,4,"%02x",rawByte);return std::string(s);}(), + " decoded=0x", + [&]{char s[4];snprintf(s,4,"%02x",decoded);return std::string(s);}(), + " xorByte=0x", + [&]{char s[4];snprintf(s,4,"%02x",xorByte);return std::string(s);}(), + " opcodes=[", + [&]{std::string r;for(int i=0;i<9;i++){char s[6];snprintf(s,6,"0x%02x ",wardenCheckOpcodes_[i]);r+=s;}return r;}(), + "] pos=", pos, "/", checkEnd); pos = checkEnd; // stop parsing break; } @@ -10165,8 +10342,6 @@ void GameHandler::handleWardenData(network::Packet& packet) { // Log synchronous round summary at WARNING level for diagnostics { - int syncCounts[10] = {}; - // Re-count (we don't have per-check counters in sync path yet) LOG_WARNING("Warden: (sync) Parsed ", checkCount, " checks, resultSize=", resultData.size()); std::string fullHex; for (size_t bi = 0; bi < resultData.size(); bi++) { @@ -13336,6 +13511,10 @@ void GameHandler::handleDuelRequested(network::Packet& packet) { pendingDuelRequest_ = true; addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!"); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playTargetSelect(); + } LOG_INFO("SMSG_DUEL_REQUESTED: challenger=0x", std::hex, duelChallengerGuid_, " flag=0x", duelFlagGuid_, std::dec, " name=", duelChallengerName_); } @@ -15090,6 +15269,7 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint log.amount = amount; log.spellId = spellId; log.isPlayerSource = isPlayerSource; + log.powerType = powerType; log.timestamp = std::time(nullptr); // If the caller provided an explicit destination GUID but left source GUID as 0, // preserve "unknown/no source" (e.g. environmental damage) instead of @@ -16186,9 +16366,12 @@ void GameHandler::handleLfgPlayerReward(network::Packet& packet) { packet.readUInt8(); // unk if (i == 0) { std::string itemLabel = "item #" + std::to_string(itemId); - if (const ItemQueryResponseData* info = getItemInfo(itemId)) + uint32_t lfgItemQuality = 1; + if (const ItemQueryResponseData* info = getItemInfo(itemId)) { if (!info->name.empty()) itemLabel = info->name; - rewardMsg += ", " + itemLabel; + lfgItemQuality = info->quality; + } + rewardMsg += ", " + buildItemLink(itemId, lfgItemQuality, itemLabel); if (itemCount > 1) rewardMsg += " x" + std::to_string(itemCount); } } @@ -16785,6 +16968,32 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { info.z = packet.readFloat(); info.orientation = packet.readFloat(); + // Read transport data if the on-transport flag is set in wire-format move flags. + // The flag bit position differs between expansions (0x200 for WotLK/TBC, 0x02000000 for Classic/Turtle). + const uint32_t wireTransportFlag = packetParsers_ ? packetParsers_->wireOnTransportFlag() : 0x00000200; + const bool onTransport = (info.flags & wireTransportFlag) != 0; + uint64_t transportGuid = 0; + float tLocalX = 0, tLocalY = 0, tLocalZ = 0, tLocalO = 0; + if (onTransport) { + transportGuid = UpdateObjectParser::readPackedGuid(packet); + tLocalX = packet.readFloat(); + tLocalY = packet.readFloat(); + tLocalZ = packet.readFloat(); + tLocalO = packet.readFloat(); + // TBC and WotLK include a transport timestamp; Classic does not. + if (flags2Size >= 1) { + /*uint32_t transportTime =*/ packet.readUInt32(); + } + // WotLK adds a transport seat byte. + if (flags2Size >= 2) { + /*int8_t transportSeat =*/ packet.readUInt8(); + // Optional second transport time for interpolated movement. + if (info.flags2 & 0x0200) { + /*uint32_t transportTime2 =*/ packet.readUInt32(); + } + } + } + // Update entity position in entity manager auto entity = entityManager.getEntity(moverGuid); if (!entity) { @@ -16794,6 +17003,20 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { // Convert server coords to canonical glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(info.x, info.y, info.z)); float canYaw = core::coords::serverToCanonicalYaw(info.orientation); + + // Handle transport attachment: attach/detach the entity so it follows the transport + // smoothly between movement updates via updateAttachedTransportChildren(). + if (onTransport && transportGuid != 0 && transportManager_) { + glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(tLocalX, tLocalY, tLocalZ)); + setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, true, + core::coords::serverToCanonicalYaw(tLocalO)); + // Derive world position from transport system for best accuracy. + glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localCanonical); + canonical = worldPos; + } else if (!onTransport) { + // Player left transport — clear any stale attachment. + clearTransportAttachment(moverGuid); + } // Compute a smoothed interpolation window for this player. // Using a raw packet delta causes jitter when timing spikes (e.g. 50ms then 300ms). // An exponential moving average of intervals gives a stable playback speed that @@ -17648,6 +17871,21 @@ void GameHandler::cancelCast() { castIsChannel = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; + // Cancel craft queue when player manually cancels cast + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; +} + +void GameHandler::startCraftQueue(uint32_t spellId, int count) { + craftQueueSpellId_ = spellId; + craftQueueRemaining_ = count; + // Cast the first one immediately + castSpell(spellId, 0); +} + +void GameHandler::cancelCraftQueue() { + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; } void GameHandler::cancelAura(uint32_t spellId) { @@ -17890,6 +18128,11 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { } } + // Pre-load skill line DBCs so isProfessionSpell() works immediately + // (not just after opening a trainer window) + loadSkillLineDbc(); + loadSkillLineAbilityDbc(); + LOG_INFO("Learned ", knownSpells.size(), " spells"); } @@ -17974,14 +18217,17 @@ void GameHandler::handleSpellStart(network::Packet& packet) { castTimeRemaining = castTimeTotal; // Play precast (channeling) sound with correct magic school - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ssm = renderer->getSpellSoundManager()) { - loadSpellNameCache(); - auto it = spellNameCache_.find(data.spellId); - auto school = (it != spellNameCache_.end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM); + // Skip sound for profession/tradeskill spells (crafting should be silent) + if (!isProfessionSpell(data.spellId)) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + loadSpellNameCache(); + auto it = spellNameCache_.find(data.spellId); + auto school = (it != spellNameCache_.end() && it->second.schoolMask) + ? schoolMaskToMagicSchool(it->second.schoolMask) + : audio::SpellSoundManager::MagicSchool::ARCANE; + ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM); + } } } @@ -18007,14 +18253,17 @@ void GameHandler::handleSpellGo(network::Packet& packet) { // Cast completed if (data.casterUnit == playerGuid) { // Play cast-complete sound with correct magic school - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ssm = renderer->getSpellSoundManager()) { - loadSpellNameCache(); - auto it = spellNameCache_.find(data.spellId); - auto school = (it != spellNameCache_.end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playCast(school); + // Skip sound for profession/tradeskill spells (crafting should be silent) + if (!isProfessionSpell(data.spellId)) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + loadSpellNameCache(); + auto it = spellNameCache_.find(data.spellId); + auto school = (it != spellNameCache_.end() && it->second.schoolMask) + ? schoolMaskToMagicSchool(it->second.schoolMask) + : audio::SpellSoundManager::MagicSchool::ARCANE; + ssm->playCast(school); + } } } @@ -18034,8 +18283,16 @@ void GameHandler::handleSpellGo(network::Packet& packet) { isMeleeAbility = (currentCastSpellId != sid); } } - if (isMeleeAbility && meleeSwingCallback_) { - meleeSwingCallback_(); + if (isMeleeAbility) { + if (meleeSwingCallback_) meleeSwingCallback_(); + // Play weapon swing + impact sound for instant melee abilities (Sinister Strike, etc.) + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* csm = renderer->getCombatSoundManager()) { + csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false); + csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM, + audio::CombatSoundManager::ImpactType::FLESH, false); + } + } } // Capture cast state before clearing. Guard with spellId match so that @@ -18062,9 +18319,28 @@ void GameHandler::handleSpellGo(network::Packet& packet) { if (spellCastAnimCallback_) { spellCastAnimCallback_(playerGuid, false, false); } - } else if (spellCastAnimCallback_) { - // End cast animation on other unit - spellCastAnimCallback_(data.casterUnit, false, false); + } else { + if (spellCastAnimCallback_) { + // End cast animation on other unit + spellCastAnimCallback_(data.casterUnit, false, false); + } + // Play cast-complete sound for enemy spells targeting the player + bool targetsPlayer = false; + for (const auto& tgt : data.hitTargets) { + if (tgt == playerGuid) { targetsPlayer = true; break; } + } + if (targetsPlayer) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + loadSpellNameCache(); + auto it = spellNameCache_.find(data.spellId); + auto school = (it != spellNameCache_.end() && it->second.schoolMask) + ? schoolMaskToMagicSchool(it->second.schoolMask) + : audio::SpellSoundManager::MagicSchool::ARCANE; + ssm->playCast(school); + } + } + } } // Clear unit cast bar when the spell lands (for any tracked unit) @@ -18085,12 +18361,16 @@ void GameHandler::handleSpellGo(network::Packet& packet) { } } - // Play impact sound when player is hit by any spell (from self or others) + // Play impact sound for spell hits involving the player + // - When player is hit by an enemy spell + // - When player's spell hits an enemy target bool playerIsHit = false; + bool playerHitEnemy = false; for (const auto& tgt : data.hitTargets) { - if (tgt == playerGuid) { playerIsHit = true; break; } + if (tgt == playerGuid) { playerIsHit = true; } + if (data.casterUnit == playerGuid && tgt != playerGuid && tgt != 0) { playerHitEnemy = true; } } - if (playerIsHit && data.casterUnit != playerGuid) { + if (playerIsHit || playerHitEnemy) { if (auto* renderer = core::Application::getInstance().getRenderer()) { if (auto* ssm = renderer->getSpellSoundManager()) { loadSpellNameCache(); @@ -18271,6 +18551,12 @@ void GameHandler::handleRemovedSpell(network::Packet& packet) { knownSpells.erase(spellId); LOG_INFO("Removed spell: ", spellId); + const std::string& name = getSpellName(spellId); + if (!name.empty()) + addSystemChatMessage("You have unlearned: " + name + "."); + else + addSystemChatMessage("A spell has been removed."); + // Clear any action bar slots referencing this spell bool barChanged = false; for (auto& slot : actionBar) { @@ -18537,6 +18823,10 @@ void GameHandler::handleGroupInvite(network::Packet& packet) { if (!data.inviterName.empty()) { addSystemChatMessage(data.inviterName + " has invited you to a group."); } + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playTargetSelect(); + } } void GameHandler::handleGroupDecline(network::Packet& packet) { @@ -19279,7 +19569,7 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { float dy = entity->getY() - movementInfo.y; float dz = entity->getZ() - movementInfo.z; float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz); - if (dist3d > 6.0f) { + if (dist3d > 10.0f) { addSystemChatMessage("Too far away."); return; } @@ -19343,13 +19633,17 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { } } if (shouldSendLoot) { - lootTarget(guid); - // Some servers/scripts only make certain quest/chest GOs lootable after a short delay - // (use animation, state change). Queue one delayed loot attempt to catch that case. + // Don't send CMSG_LOOT immediately — give the server time to process + // CMSG_GAMEOBJ_USE first (chests need to transition to lootable state, + // gathering nodes start a spell cast). A premature CMSG_LOOT can cause + // an empty SMSG_LOOT_RESPONSE that clears our gather-cast loot state. pendingGameObjectLootOpens_.erase( std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(), [&](const PendingLootOpen& p) { return p.guid == guid; }), pendingGameObjectLootOpens_.end()); + // Short delay for chests (server makes them lootable quickly after USE), + // plus a longer retry to catch slow state transitions. + pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.20f}); pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.75f}); } else { // Non-lootable interaction (mailbox, door, button, etc.) — no CMSG_LOOT will be @@ -19357,12 +19651,9 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { // guid now so a subsequent timed cast completion can't fire a spurious CMSG_LOOT. lastInteractedGoGuid_ = 0; } - // Retry use briefly to survive packet loss/order races. - const bool retryLoot = shouldSendLoot; - const bool retryUse = turtleMode || isActiveExpansion("classic"); - if (retryUse || retryLoot) { - pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot}); - } + // Don't retry CMSG_GAMEOBJ_USE — resending can toggle chest state on some + // servers (opening→closing the chest). The delayed CMSG_LOOT retries above + // handle the case where the first loot attempt arrives too early. } void GameHandler::selectGossipOption(uint32_t optionId) { @@ -19901,6 +20192,12 @@ void GameHandler::acceptQuest() { pendingQuestAcceptTimeouts_[questId] = 5.0f; pendingQuestAcceptNpcGuids_[questId] = npcGuid; + // Play quest-accept sound + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playQuestActivate(); + } + questDetailsOpen = false; questDetailsOpenTime = std::chrono::steady_clock::time_point{}; currentQuestDetails = QuestDetailsData{}; @@ -20536,6 +20833,13 @@ void GameHandler::handleLootResponse(network::Packet& packet) { // WotLK 3.3.5a uses 22 bytes/item. const bool wotlkLoot = isActiveExpansion("wotlk"); if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return; + const bool hasLoot = !currentLoot.items.empty() || currentLoot.gold > 0; + // If we're mid-gather-cast and got an empty loot response (premature CMSG_LOOT + // before the node became lootable), ignore it — don't clear our gather state. + if (!hasLoot && casting && currentCastSpellId != 0 && lastInteractedGoGuid_ != 0) { + LOG_DEBUG("Ignoring empty SMSG_LOOT_RESPONSE during gather cast"); + return; + } lootWindowOpen = true; lastInteractedGoGuid_ = 0; // loot opened — no need to re-send in handleSpellGo pendingGameObjectLootOpens_.erase( @@ -20589,17 +20893,19 @@ void GameHandler::handleLootRemoved(network::Packet& packet) { for (auto it = currentLoot.items.begin(); it != currentLoot.items.end(); ++it) { if (it->slotIndex == slotIndex) { std::string itemName = "item #" + std::to_string(it->itemId); + uint32_t quality = 1; if (const ItemQueryResponseData* info = getItemInfo(it->itemId)) { - if (!info->name.empty()) { - itemName = info->name; - } + if (!info->name.empty()) itemName = info->name; + quality = info->quality; } - std::ostringstream msg; - msg << "Looted: " << itemName; - if (it->count > 1) { - msg << " x" << it->count; + std::string link = buildItemLink(it->itemId, quality, itemName); + std::string msgStr = "Looted: " + link; + if (it->count > 1) msgStr += " x" + std::to_string(it->count); + addSystemChatMessage(msgStr); + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playLootItem(); } - addSystemChatMessage(msg.str()); currentLoot.items.erase(it); break; } @@ -22619,6 +22925,15 @@ uint32_t GameHandler::getSkillCategory(uint32_t skillId) const { return (it != skillLineCategories_.end()) ? it->second : 0; } +bool GameHandler::isProfessionSpell(uint32_t spellId) const { + auto slIt = spellToSkillLine_.find(spellId); + if (slIt == spellToSkillLine_.end()) return false; + auto catIt = skillLineCategories_.find(slIt->second); + if (catIt == skillLineCategories_.end()) return false; + // Category 11 = profession (Blacksmithing, etc.), 9 = secondary (Cooking, First Aid, Fishing) + return catIt->second == 11 || catIt->second == 9; +} + void GameHandler::loadSkillLineDbc() { if (skillLineDbcLoaded_) return; skillLineDbcLoaded_ = true; @@ -24051,12 +24366,11 @@ void GameHandler::handleLootRoll(network::Packet& packet) { } auto* info = getItemInfo(itemId); - std::string iName = info ? info->name : std::to_string(itemId); + std::string iName = info && !info->name.empty() ? info->name : std::to_string(itemId); + uint32_t rollItemQuality = info ? info->quality : 1u; + std::string rollItemLink = buildItemLink(itemId, rollItemQuality, iName); - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s rolls %s (%d) on [%s]", - rollerName.c_str(), rollName, static_cast(rollNum), iName.c_str()); - addSystemChatMessage(buf); + addSystemChatMessage(rollerName + " rolls " + rollName + " (" + std::to_string(rollNum) + ") on " + rollItemLink); LOG_DEBUG("SMSG_LOOT_ROLL: ", rollerName, " rolled ", rollName, " (", rollNum, ") on item ", itemId); @@ -24097,12 +24411,11 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { } auto* info = getItemInfo(itemId); - std::string iName = info ? info->name : std::to_string(itemId); + std::string iName = info && !info->name.empty() ? info->name : std::to_string(itemId); + uint32_t wonItemQuality = info ? info->quality : 1u; + std::string wonItemLink = buildItemLink(itemId, wonItemQuality, iName); - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s wins [%s] (%s %d)!", - winnerName.c_str(), iName.c_str(), rollName, static_cast(rollNum)); - addSystemChatMessage(buf); + addSystemChatMessage(winnerName + " wins " + wonItemLink + " (" + rollName + " " + std::to_string(rollNum) + ")!"); // Dismiss roll popup — roll contest is over regardless of who won pendingLootRollActive_ = false; @@ -24245,6 +24558,10 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { earnedAchievements_.insert(achievementId); achievementDates_[achievementId] = earnDate; + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* sfx = renderer->getUiSoundManager()) + sfx->playAchievementAlert(); + } if (achievementEarnedCallback_) { achievementEarnedCallback_(achievementId, achName); } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 30dd8e05..a29a0beb 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1228,7 +1228,7 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector(typeVal); + + uint32_t langVal = packet.readUInt32(); + data.language = static_cast(langVal); + + // TBC: NO senderGuid or unknown field here (WotLK has senderGuid(u64) + unk(u32)) + + switch (data.type) { + case ChatType::MONSTER_SAY: + case ChatType::MONSTER_YELL: + case ChatType::MONSTER_EMOTE: + case ChatType::MONSTER_WHISPER: + case ChatType::MONSTER_PARTY: + case ChatType::RAID_BOSS_EMOTE: { + // senderGuid(u64) + nameLen(u32) + name + targetGuid(u64) + data.senderGuid = packet.readUInt64(); + uint32_t nameLen = packet.readUInt32(); + if (nameLen > 0 && nameLen < 256) { + data.senderName.resize(nameLen); + for (uint32_t i = 0; i < nameLen; ++i) { + data.senderName[i] = static_cast(packet.readUInt8()); + } + if (!data.senderName.empty() && data.senderName.back() == '\0') { + data.senderName.pop_back(); + } + } + data.receiverGuid = packet.readUInt64(); + break; + } + + case ChatType::SAY: + case ChatType::PARTY: + case ChatType::YELL: + case ChatType::WHISPER: + case ChatType::WHISPER_INFORM: + case ChatType::GUILD: + case ChatType::OFFICER: + case ChatType::RAID: + case ChatType::RAID_LEADER: + case ChatType::RAID_WARNING: + case ChatType::EMOTE: + case ChatType::TEXT_EMOTE: { + // senderGuid(u64) + senderGuid(u64) — written twice by server + data.senderGuid = packet.readUInt64(); + /*duplicateGuid*/ packet.readUInt64(); + break; + } + + case ChatType::CHANNEL: { + // channelName(string) + rank(u32) + senderGuid(u64) + data.channelName = packet.readString(); + /*uint32_t rank =*/ packet.readUInt32(); + data.senderGuid = packet.readUInt64(); + break; + } + + default: { + // All other types: senderGuid(u64) + senderGuid(u64) — written twice + data.senderGuid = packet.readUInt64(); + /*duplicateGuid*/ packet.readUInt64(); + break; + } + } + + // Read message length + message + uint32_t messageLen = packet.readUInt32(); + if (messageLen > 0 && messageLen < 8192) { + data.message.resize(messageLen); + for (uint32_t i = 0; i < messageLen; ++i) { + data.message[i] = static_cast(packet.readUInt8()); + } + if (!data.message.empty() && data.message.back() == '\0') { + data.message.pop_back(); + } + } + + // Read chat tag + if (packet.getReadPos() < packet.getSize()) { + data.chatTag = packet.readUInt8(); + } + + LOG_DEBUG("[TBC] SMSG_MESSAGECHAT: type=", getChatTypeString(data.type), + " sender=", data.senderName.empty() ? std::to_string(data.senderGuid) : data.senderName); + + return true; +} + +// ============================================================================ +// TBC 2.4.3 quest giver status +// TBC sends uint32 (like Classic), WotLK changed to uint8. +// TBC 2.4.3 enum: 0=NONE,1=UNAVAILABLE,2=CHAT,3=INCOMPLETE,4=REWARD_REP, +// 5=AVAILABLE_REP,6=AVAILABLE,7=REWARD2,8=REWARD +// ============================================================================ + +uint8_t TbcPacketParsers::readQuestGiverStatus(network::Packet& packet) { + uint32_t tbcStatus = packet.readUInt32(); + switch (tbcStatus) { + case 0: return 0; // NONE + case 1: return 1; // UNAVAILABLE + case 2: return 0; // CHAT → NONE (no marker) + case 3: return 5; // INCOMPLETE → WotLK INCOMPLETE + case 4: return 6; // REWARD_REP → WotLK REWARD_REP + case 5: return 7; // AVAILABLE_REP → WotLK AVAILABLE_LOW_LEVEL + case 6: return 8; // AVAILABLE → WotLK AVAILABLE + case 7: return 10; // REWARD2 → WotLK REWARD + case 8: return 10; // REWARD → WotLK REWARD + default: return 0; + } +} + +// ============================================================================ +// TBC 2.4.3 channel join/leave +// Classic/TBC: just name+password (no channelId/hasVoice/joinedByZone prefix) +// ============================================================================ + +network::Packet TbcPacketParsers::buildJoinChannel(const std::string& channelName, const std::string& password) { + network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL)); + packet.writeString(channelName); + packet.writeString(password); + LOG_DEBUG("[TBC] Built CMSG_JOIN_CHANNEL: channel=", channelName); + return packet; +} + +network::Packet TbcPacketParsers::buildLeaveChannel(const std::string& channelName) { + network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL)); + packet.writeString(channelName); + LOG_DEBUG("[TBC] Built CMSG_LEAVE_CHANNEL: channel=", channelName); + return packet; +} + +// ============================================================================ +// TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE +// TBC has 2 extra strings after name[4] (iconName + castBarCaption). +// WotLK has 3 (adds unk1). Classic has 0. +// ============================================================================ + +bool TbcPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) { + if (packet.getSize() < 4) { + LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)"); + return false; + } + + data.entry = packet.readUInt32(); + + if (data.entry & 0x80000000) { + data.entry &= ~0x80000000; + data.name = ""; + return true; + } + + if (packet.getSize() - packet.getReadPos() < 8) { + LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")"); + return false; + } + + data.type = packet.readUInt32(); + data.displayId = packet.readUInt32(); + // 4 name strings + data.name = packet.readString(); + packet.readString(); + packet.readString(); + packet.readString(); + + // TBC: 2 extra strings (iconName + castBarCaption) — WotLK has 3, Classic has 0 + packet.readString(); // iconName + packet.readString(); // castBarCaption + + // Read 24 type-specific data fields + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining >= 24 * 4) { + for (int i = 0; i < 24; i++) { + data.data[i] = packet.readUInt32(); + } + data.hasData = true; + } else if (remaining > 0) { + uint32_t fieldsToRead = remaining / 4; + for (uint32_t i = 0; i < fieldsToRead && i < 24; i++) { + data.data[i] = packet.readUInt32(); + } + if (fieldsToRead < 24) { + LOG_WARNING("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated in data fields (", fieldsToRead, + " of 24, entry=", data.entry, ")"); + } + } + + if (data.type == 15) { // MO_TRANSPORT + LOG_DEBUG("TBC GO query: MO_TRANSPORT entry=", data.entry, + " name=\"", data.name, "\" displayId=", data.displayId, + " taxiPathId=", data.data[0], " moveSpeed=", data.data[1]); + } else { + LOG_DEBUG("TBC GO query: ", data.name, " type=", data.type, " entry=", data.entry); + } + return true; +} + +// ============================================================================ +// TBC 2.4.3 guild roster parser +// Same rank structure as WotLK (variable rankCount + goldLimit + bank tabs), +// but NO gender byte per member (WotLK added it). +// ============================================================================ + +bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData& data) { + if (packet.getSize() < 4) { + LOG_ERROR("TBC SMSG_GUILD_ROSTER too small: ", packet.getSize()); + return false; + } + uint32_t numMembers = packet.readUInt32(); + + const uint32_t MAX_GUILD_MEMBERS = 1000; + if (numMembers > MAX_GUILD_MEMBERS) { + LOG_WARNING("TBC GuildRoster: numMembers capped (requested=", numMembers, ")"); + numMembers = MAX_GUILD_MEMBERS; + } + + data.motd = packet.readString(); + data.guildInfo = packet.readString(); + + if (packet.getReadPos() + 4 > packet.getSize()) { + LOG_WARNING("TBC GuildRoster: truncated before rankCount"); + data.ranks.clear(); + data.members.clear(); + return true; + } + + uint32_t rankCount = packet.readUInt32(); + const uint32_t MAX_GUILD_RANKS = 20; + if (rankCount > MAX_GUILD_RANKS) { + LOG_WARNING("TBC GuildRoster: rankCount capped (requested=", rankCount, ")"); + rankCount = MAX_GUILD_RANKS; + } + + data.ranks.resize(rankCount); + for (uint32_t i = 0; i < rankCount; ++i) { + if (packet.getReadPos() + 4 > packet.getSize()) { + LOG_WARNING("TBC GuildRoster: truncated rank at index ", i); + break; + } + data.ranks[i].rights = packet.readUInt32(); + if (packet.getReadPos() + 4 > packet.getSize()) { + data.ranks[i].goldLimit = 0; + } else { + data.ranks[i].goldLimit = packet.readUInt32(); + } + // 6 bank tab flags + 6 bank tab items per day (guild banks added in TBC 2.3) + for (int t = 0; t < 6; ++t) { + if (packet.getReadPos() + 8 > packet.getSize()) break; + packet.readUInt32(); // tabFlags + packet.readUInt32(); // tabItemsPerDay + } + } + + data.members.resize(numMembers); + for (uint32_t i = 0; i < numMembers; ++i) { + if (packet.getReadPos() + 9 > packet.getSize()) { + LOG_WARNING("TBC GuildRoster: truncated member at index ", i); + break; + } + auto& m = data.members[i]; + m.guid = packet.readUInt64(); + m.online = (packet.readUInt8() != 0); + + if (packet.getReadPos() >= packet.getSize()) { + m.name.clear(); + } else { + m.name = packet.readString(); + } + + if (packet.getReadPos() + 1 > packet.getSize()) { + m.rankIndex = 0; + m.level = 1; + m.classId = 0; + m.gender = 0; + m.zoneId = 0; + } else { + m.rankIndex = packet.readUInt32(); + if (packet.getReadPos() + 2 > packet.getSize()) { + m.level = 1; + m.classId = 0; + } else { + m.level = packet.readUInt8(); + m.classId = packet.readUInt8(); + } + // TBC: NO gender byte (WotLK added it) + m.gender = 0; + if (packet.getReadPos() + 4 > packet.getSize()) { + m.zoneId = 0; + } else { + m.zoneId = packet.readUInt32(); + } + } + + if (!m.online) { + if (packet.getReadPos() + 4 > packet.getSize()) { + m.lastOnline = 0.0f; + } else { + m.lastOnline = packet.readFloat(); + } + } + + if (packet.getReadPos() >= packet.getSize()) { + m.publicNote.clear(); + m.officerNote.clear(); + } else { + m.publicNote = packet.readString(); + if (packet.getReadPos() >= packet.getSize()) { + m.officerNote.clear(); + } else { + m.officerNote = packet.readString(); + } + } + } + LOG_INFO("Parsed TBC SMSG_GUILD_ROSTER: ", numMembers, " members, motd=", data.motd); + return true; +} + } // namespace game } // namespace wowee diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index a9ff5cba..649c9923 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -31,13 +30,13 @@ void TransportManager::update(float deltaTime) { void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry) { auto pathIt = paths_.find(pathId); if (pathIt == paths_.end()) { - std::cerr << "TransportManager: Path " << pathId << " not found for transport " << guid << std::endl; + LOG_ERROR("TransportManager: Path ", pathId, " not found for transport ", guid); return; } const auto& path = pathIt->second; if (path.points.empty()) { - std::cerr << "TransportManager: Path " << pathId << " has no waypoints" << std::endl; + LOG_ERROR("TransportManager: Path ", pathId, " has no waypoints"); return; } @@ -128,7 +127,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, void TransportManager::unregisterTransport(uint64_t guid) { transports_.erase(guid); - std::cout << "TransportManager: Unregistered transport " << guid << std::endl; + LOG_INFO("TransportManager: Unregistered transport ", guid); } ActiveTransport* TransportManager::getTransport(uint64_t guid) { @@ -168,7 +167,7 @@ glm::mat4 TransportManager::getTransportInvTransform(uint64_t transportGuid) { void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector& waypoints, bool looping, float speed) { if (waypoints.empty()) { - std::cerr << "TransportManager: Cannot load empty path " << pathId << std::endl; + LOG_ERROR("TransportManager: Cannot load empty path ", pathId); return; } @@ -227,7 +226,7 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector +#include "core/logger.hpp" #include #include +#include #ifdef HAVE_UNICORN // Unicorn Engine headers @@ -43,17 +44,27 @@ WardenEmulator::~WardenEmulator() { bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint32_t baseAddress) { if (uc_) { - std::cerr << "[WardenEmulator] Already initialized" << '\n'; + LOG_ERROR("WardenEmulator: Already initialized"); return false; } + // Reset allocator state so re-initialization starts with a clean heap. + allocations_.clear(); + freeBlocks_.clear(); + apiAddresses_.clear(); + hooks_.clear(); + nextHeapAddr_ = heapBase_; - std::cout << "[WardenEmulator] Initializing x86 emulator (Unicorn Engine)" << '\n'; - std::cout << "[WardenEmulator] Module: " << moduleSize << " bytes at 0x" << std::hex << baseAddress << std::dec << '\n'; + { + char addrBuf[32]; + std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", baseAddress); + LOG_INFO("WardenEmulator: Initializing x86 emulator (Unicorn Engine)"); + LOG_INFO("WardenEmulator: Module: ", moduleSize, " bytes at ", addrBuf); + } // Create x86 32-bit emulator uc_err err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc_); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] uc_open failed: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: uc_open failed: ", uc_strerror(err)); return false; } @@ -63,9 +74,12 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Detect overlap between module and heap/stack regions early. uint32_t modEnd = moduleBase_ + moduleSize_; if (modEnd > heapBase_ && moduleBase_ < heapBase_ + heapSize_) { - std::cerr << "[WardenEmulator] Module [0x" << std::hex << moduleBase_ - << ", 0x" << modEnd << ") overlaps heap [0x" << heapBase_ - << ", 0x" << (heapBase_ + heapSize_) << ") — adjust HEAP_BASE\n" << std::dec; + { + char buf[256]; + std::snprintf(buf, sizeof(buf), "WardenEmulator: Module [0x%X, 0x%X) overlaps heap [0x%X, 0x%X) - adjust HEAP_BASE", + moduleBase_, modEnd, heapBase_, heapBase_ + heapSize_); + LOG_ERROR(buf); + } uc_close(uc_); uc_ = nullptr; return false; @@ -74,7 +88,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map module memory (code + data) err = uc_mem_map(uc_, moduleBase_, moduleSize_, UC_PROT_ALL); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map module memory: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map module memory: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -83,7 +97,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Write module code to emulated memory err = uc_mem_write(uc_, moduleBase_, moduleCode, moduleSize); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to write module code: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to write module code: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -92,7 +106,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map stack err = uc_mem_map(uc_, stackBase_, stackSize_, UC_PROT_READ | UC_PROT_WRITE); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map stack: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map stack: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -106,7 +120,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map heap err = uc_mem_map(uc_, heapBase_, heapSize_, UC_PROT_READ | UC_PROT_WRITE); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map heap: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map heap: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -115,7 +129,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map API stub area err = uc_mem_map(uc_, apiStubBase_, 0x10000, UC_PROT_ALL); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map API stub area: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map API stub area: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -127,7 +141,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 err = uc_mem_map(uc_, 0x0, 0x1000, UC_PROT_READ); if (err != UC_ERR_OK) { // Non-fatal — just log it; the emulator will still function - std::cerr << "[WardenEmulator] Note: could not map null guard page: " << uc_strerror(err) << '\n'; + LOG_WARNING("WardenEmulator: could not map null guard page: ", uc_strerror(err)); } // Add hooks for debugging and invalid memory access @@ -135,9 +149,12 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0); hooks_.push_back(hh); - std::cout << "[WardenEmulator] ✓ Emulator initialized successfully" << '\n'; - std::cout << "[WardenEmulator] Stack: 0x" << std::hex << stackBase_ << " - 0x" << (stackBase_ + stackSize_) << '\n'; - std::cout << "[WardenEmulator] Heap: 0x" << heapBase_ << " - 0x" << (heapBase_ + heapSize_) << std::dec << '\n'; + { + char sBuf[128]; + std::snprintf(sBuf, sizeof(sBuf), "WardenEmulator: Emulator initialized Stack: 0x%X-0x%X Heap: 0x%X-0x%X", + stackBase_, stackBase_ + stackSize_, heapBase_, heapBase_ + heapSize_); + LOG_INFO(sBuf); + } return true; } @@ -153,8 +170,11 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName, // Store mapping apiAddresses_[dllName][functionName] = stubAddr; - std::cout << "[WardenEmulator] Hooked " << dllName << "!" << functionName - << " at 0x" << std::hex << stubAddr << std::dec << '\n'; + { + char hBuf[32]; + std::snprintf(hBuf, sizeof(hBuf), "0x%X", stubAddr); + LOG_DEBUG("WardenEmulator: Hooked ", dllName, "!", functionName, " at ", hBuf); + } // TODO: Write stub code that triggers a hook callback // For now, just return the address for IAT patching @@ -163,7 +183,7 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName, } void WardenEmulator::setupCommonAPIHooks() { - std::cout << "[WardenEmulator] Setting up common Windows API hooks..." << '\n'; + LOG_INFO("WardenEmulator: Setting up common Windows API hooks..."); // kernel32.dll hookAPI("kernel32.dll", "VirtualAlloc", apiVirtualAlloc); @@ -174,7 +194,7 @@ void WardenEmulator::setupCommonAPIHooks() { hookAPI("kernel32.dll", "GetCurrentProcessId", apiGetCurrentProcessId); hookAPI("kernel32.dll", "ReadProcessMemory", apiReadProcessMemory); - std::cout << "[WardenEmulator] ✓ Common API hooks registered" << '\n'; + LOG_INFO("WardenEmulator: Common API hooks registered"); } uint32_t WardenEmulator::writeData(const void* data, size_t size) { @@ -198,12 +218,15 @@ std::vector WardenEmulator::readData(uint32_t address, size_t size) { uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector& args) { if (!uc_) { - std::cerr << "[WardenEmulator] Not initialized" << '\n'; + LOG_ERROR("WardenEmulator: Not initialized"); return 0; } - std::cout << "[WardenEmulator] Calling function at 0x" << std::hex << address << std::dec - << " with " << args.size() << " args" << '\n'; + { + char aBuf[32]; + std::snprintf(aBuf, sizeof(aBuf), "0x%X", address); + LOG_DEBUG("WardenEmulator: Calling function at ", aBuf, " with ", args.size(), " args"); + } // Get current ESP uint32_t esp; @@ -227,7 +250,7 @@ uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector(size); - if (nextHeapAddr_ + size > heapBase_ + heapSize_) { - std::cerr << "[WardenEmulator] Heap exhausted" << '\n'; + // First-fit from free list so released blocks can be reused. + for (auto it = freeBlocks_.begin(); it != freeBlocks_.end(); ++it) { + if (it->second < size) continue; + const uint32_t addr = it->first; + const size_t blockSz = it->second; + freeBlocks_.erase(it); + if (blockSz > size) + freeBlocks_[addr + allocSize] = blockSz - size; + allocations_[addr] = size; + { + char mBuf[32]; + std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr); + LOG_DEBUG("WardenEmulator: Reused ", size, " bytes at ", mBuf); + } + return addr; + } + + const uint64_t heapEnd = static_cast(heapBase_) + heapSize_; + if (static_cast(nextHeapAddr_) + size > heapEnd) { + LOG_ERROR("WardenEmulator: Heap exhausted"); return 0; } uint32_t addr = nextHeapAddr_; - nextHeapAddr_ += size; - + nextHeapAddr_ += allocSize; allocations_[addr] = size; - std::cout << "[WardenEmulator] Allocated " << size << " bytes at 0x" << std::hex << addr << std::dec << '\n'; + { + char mBuf[32]; + std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr); + LOG_DEBUG("WardenEmulator: Allocated ", size, " bytes at ", mBuf); + } return addr; } @@ -283,12 +334,54 @@ uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t p bool WardenEmulator::freeMemory(uint32_t address) { auto it = allocations_.find(address); if (it == allocations_.end()) { - std::cerr << "[WardenEmulator] Invalid free at 0x" << std::hex << address << std::dec << '\n'; + { + char fBuf[32]; + std::snprintf(fBuf, sizeof(fBuf), "0x%X", address); + LOG_ERROR("WardenEmulator: Invalid free at ", fBuf); + } return false; } - std::cout << "[WardenEmulator] Freed " << it->second << " bytes at 0x" << std::hex << address << std::dec << '\n'; + { + char fBuf[32]; + std::snprintf(fBuf, sizeof(fBuf), "0x%X", address); + LOG_DEBUG("WardenEmulator: Freed ", it->second, " bytes at ", fBuf); + } + + const size_t freedSize = it->second; allocations_.erase(it); + + // Insert in free list and coalesce adjacent blocks to limit fragmentation. + auto [curr, inserted] = freeBlocks_.emplace(address, freedSize); + if (!inserted) curr->second += freedSize; + + if (curr != freeBlocks_.begin()) { + auto prev = std::prev(curr); + if (static_cast(prev->first) + prev->second == curr->first) { + prev->second += curr->second; + freeBlocks_.erase(curr); + curr = prev; + } + } + + auto next = std::next(curr); + if (next != freeBlocks_.end() && + static_cast(curr->first) + curr->second == next->first) { + curr->second += next->second; + freeBlocks_.erase(next); + } + + // Roll back the bump pointer if the highest free block reaches it. + while (!freeBlocks_.empty()) { + auto last = std::prev(freeBlocks_.end()); + if (static_cast(last->first) + last->second == nextHeapAddr_) { + nextHeapAddr_ = last->first; + freeBlocks_.erase(last); + } else { + break; + } + } + return true; } @@ -319,8 +412,12 @@ uint32_t WardenEmulator::apiVirtualAlloc(WardenEmulator& emu, const std::vector< uint32_t flAllocationType = args[2]; uint32_t flProtect = args[3]; - std::cout << "[WinAPI] VirtualAlloc(0x" << std::hex << lpAddress << ", " << std::dec - << dwSize << ", 0x" << std::hex << flAllocationType << ", 0x" << flProtect << ")" << std::dec << '\n'; + { + char vBuf[128]; + std::snprintf(vBuf, sizeof(vBuf), "WinAPI: VirtualAlloc(0x%X, %u, 0x%X, 0x%X)", + lpAddress, dwSize, flAllocationType, flProtect); + LOG_DEBUG(vBuf); + } // Ignore lpAddress hint for now return emu.allocateMemory(dwSize, flProtect); @@ -332,7 +429,11 @@ uint32_t WardenEmulator::apiVirtualFree(WardenEmulator& emu, const std::vector(now.time_since_epoch()).count(); uint32_t ticks = static_cast(ms & 0xFFFFFFFF); - std::cout << "[WinAPI] GetTickCount() = " << ticks << '\n'; + LOG_DEBUG("WinAPI: GetTickCount() = ", ticks); return ticks; } @@ -350,18 +451,18 @@ uint32_t WardenEmulator::apiSleep([[maybe_unused]] WardenEmulator& emu, const st if (args.size() < 1) return 0; uint32_t dwMilliseconds = args[0]; - std::cout << "[WinAPI] Sleep(" << dwMilliseconds << ")" << '\n'; + LOG_DEBUG("WinAPI: Sleep(", dwMilliseconds, ")"); // Don't actually sleep in emulator return 0; } uint32_t WardenEmulator::apiGetCurrentThreadId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector& args) { - std::cout << "[WinAPI] GetCurrentThreadId() = 1234" << '\n'; + LOG_DEBUG("WinAPI: GetCurrentThreadId() = 1234"); return 1234; // Fake thread ID } uint32_t WardenEmulator::apiGetCurrentProcessId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector& args) { - std::cout << "[WinAPI] GetCurrentProcessId() = 5678" << '\n'; + LOG_DEBUG("WinAPI: GetCurrentProcessId() = 5678"); return 5678; // Fake process ID } @@ -375,8 +476,11 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve uint32_t nSize = args[3]; uint32_t lpNumberOfBytesRead = args[4]; - std::cout << "[WinAPI] ReadProcessMemory(0x" << std::hex << lpBaseAddress - << ", " << std::dec << nSize << " bytes)" << '\n'; + { + char rBuf[64]; + std::snprintf(rBuf, sizeof(rBuf), "WinAPI: ReadProcessMemory(0x%X, %u bytes)", lpBaseAddress, nSize); + LOG_DEBUG(rBuf); + } // Read from emulated memory and write to buffer std::vector data(nSize); @@ -400,7 +504,7 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve // ============================================================================ void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) { - std::cout << "[Trace] 0x" << std::hex << address << std::dec << '\n'; + (void)address; // Trace disabled by default to avoid log spam } void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) { @@ -415,9 +519,12 @@ void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, ui case UC_MEM_FETCH_PROT: typeStr = "FETCH_PROT"; break; } - std::cerr << "[WardenEmulator] Invalid memory access: " << typeStr - << " at 0x" << std::hex << address << std::dec - << " (size=" << size << ")" << '\n'; + { + char mBuf[128]; + std::snprintf(mBuf, sizeof(mBuf), "WardenEmulator: Invalid memory access: %s at 0x%llX (size=%d)", + typeStr, static_cast(address), size); + LOG_ERROR(mBuf); + } } #else // !HAVE_UNICORN diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index 26895784..33127e2c 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -1,5 +1,6 @@ #include "game/warden_memory.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -406,10 +407,31 @@ void WardenMemory::patchRuntimeGlobals() { writeLE32(WORLD_ENABLES, enables); LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec); - // LastHardwareAction + // LastHardwareAction — must be a recent GetTickCount()-style timestamp + // so the anti-AFK scan sees (currentTime - lastAction) < threshold. constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8; - writeLE32(LAST_HARDWARE_ACTION, 60000); + uint32_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + writeLE32(LAST_HARDWARE_ACTION, nowMs - 2000); LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec); + + // Embed the 37-byte Warden module memcpy pattern in BSS so that + // FIND_CODE_BY_HASH (PAGE_B) brute-force search can find it. + // This is the pattern VMaNGOS's "Warden Memory Read check" looks for. + constexpr uint32_t MEMCPY_PATTERN_VA = 0xCE8700; + static const uint8_t kWardenMemcpyPattern[37] = { + 0x56, 0x57, 0xFC, 0x8B, 0x54, 0x24, 0x14, 0x8B, + 0x74, 0x24, 0x10, 0x8B, 0x44, 0x24, 0x0C, 0x8B, + 0xCA, 0x8B, 0xF8, 0xC1, 0xE9, 0x02, 0x74, 0x02, + 0xF3, 0xA5, 0xB1, 0x03, 0x23, 0xCA, 0x74, 0x02, + 0xF3, 0xA4, 0x5F, 0x5E, 0xC3 + }; + uint32_t patRva = MEMCPY_PATTERN_VA - imageBase_; + if (patRva + sizeof(kWardenMemcpyPattern) <= imageSize_) { + std::memcpy(image_.data() + patRva, kWardenMemcpyPattern, sizeof(kWardenMemcpyPattern)); + LOG_WARNING("WardenMemory: Embedded Warden memcpy pattern at 0x", std::hex, MEMCPY_PATTERN_VA, std::dec); + } } void WardenMemory::patchTurtleWowBinary() { @@ -837,7 +859,8 @@ void WardenMemory::verifyWardenScanEntries() { } bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], - uint8_t patternLen, bool imageOnly) const { + uint8_t patternLen, bool imageOnly, + uint32_t hintOffset, bool hintOnly) const { if (!loaded_ || patternLen == 0 || patternLen > 255) return false; // Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1) @@ -849,21 +872,56 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect auto cacheIt = codePatternCache_.find(cacheKey); if (cacheIt != codePatternCache_.end()) { - LOG_WARNING("WardenMemory: Code pattern cache HIT → ", - cacheIt->second ? "found" : "not found"); return cacheIt->second; } - // FIND_MEM_IMAGE_CODE_BY_HASH (imageOnly=true) searches ALL sections of - // the PE image — not just executable ones. The original Warden module - // walks every PE section when scanning the WoW.exe memory image. - // FIND_CODE_BY_HASH (imageOnly=false) searches all process memory; since - // we only have the PE image, both cases search the full image. + // --- Fast path: check the hint offset directly (single HMAC) --- + // The PAGE_A offset field is the RVA where the server expects the pattern. + if (hintOffset > 0 && hintOffset + patternLen <= imageSize_) { + uint8_t hmacOut[20]; + unsigned int hmacLen = 0; + HMAC(EVP_sha1(), seed, 4, + image_.data() + hintOffset, patternLen, + hmacOut, &hmacLen); + if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + LOG_WARNING("WardenMemory: Code pattern found at hint RVA 0x", std::hex, + hintOffset, std::dec, " (direct hit)"); + codePatternCache_[cacheKey] = true; + return true; + } + } + + // --- Wider hint window: search ±4096 bytes around hint offset --- + if (hintOffset > 0) { + size_t winStart = (hintOffset > 4096) ? hintOffset - 4096 : 0; + size_t winEnd = std::min(static_cast(hintOffset) + 4096 + patternLen, + static_cast(imageSize_)); + if (winEnd > winStart + patternLen) { + for (size_t i = winStart; i + patternLen <= winEnd; i++) { + if (i == hintOffset) continue; // already checked + uint8_t hmacOut[20]; + unsigned int hmacLen = 0; + HMAC(EVP_sha1(), seed, 4, + image_.data() + i, patternLen, + hmacOut, &hmacLen); + if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, i, + std::dec, " (hint window, delta=", static_cast(i) - static_cast(hintOffset), ")"); + codePatternCache_[cacheKey] = true; + return true; + } + } + } + } + + // If hint-only mode, skip the expensive brute-force search. + if (hintOnly) return false; + + // --- Brute-force fallback: search all PE sections --- struct Range { size_t start; size_t end; }; std::vector ranges; if (imageOnly && image_.size() >= 64) { - // Collect ALL PE sections (not just executable ones) uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8) | (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24); if (peOffset + 4 + 20 <= image_.size()) { @@ -885,11 +943,14 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect } if (ranges.empty()) { - // Fallback: search entire image if (patternLen <= imageSize_) ranges.push_back({0, imageSize_}); } + auto bruteStart = std::chrono::steady_clock::now(); + LOG_WARNING("WardenMemory: Brute-force searching ", ranges.size(), " section(s), hint=0x", + std::hex, hintOffset, std::dec, " patLen=", (int)patternLen); + size_t totalPositions = 0; for (const auto& r : ranges) { size_t positions = r.end - r.start - patternLen + 1; @@ -900,8 +961,11 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect image_.data() + r.start + i, patternLen, hmacOut, &hmacLen); if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - bruteStart).count(); LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, - r.start + i, std::dec, " (searched ", totalPositions + i + 1, " positions)"); + r.start + i, std::dec, " (searched ", totalPositions + i + 1, + " positions in ", elapsed, "s)"); codePatternCache_[cacheKey] = true; return true; } @@ -909,8 +973,10 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect totalPositions += positions; } + auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - bruteStart).count(); LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ", - ranges.size(), " section(s)"); + ranges.size(), " section(s), took ", elapsed, "s"); codePatternCache_[cacheKey] = false; return false; } diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index 5bb76027..9f577978 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -1,9 +1,9 @@ #include "game/warden_module.hpp" #include "auth/crypto.hpp" +#include "core/logger.hpp" #include #include #include -#include #include #include #include @@ -51,28 +51,30 @@ bool WardenModule::load(const std::vector& moduleData, moduleData_ = moduleData; md5Hash_ = md5Hash; - std::cout << "[WardenModule] Loading module (MD5: "; - for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) { - printf("%02X", md5Hash[i]); + { + char hexBuf[17] = {}; + for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) { + snprintf(hexBuf + i * 2, 3, "%02X", md5Hash[i]); + } + LOG_INFO("WardenModule: Loading module (MD5: ", hexBuf, "...)"); } - std::cout << "...)" << '\n'; // Step 1: Verify MD5 hash if (!verifyMD5(moduleData, md5Hash)) { - std::cerr << "[WardenModule] MD5 verification failed; continuing in compatibility mode" << '\n'; + LOG_ERROR("WardenModule: MD5 verification failed; continuing in compatibility mode"); } - std::cout << "[WardenModule] ✓ MD5 verified" << '\n'; + LOG_INFO("WardenModule: MD5 verified"); // Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed) if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm] - std::cerr << "[WardenModule] RC4 decryption failed; using raw module bytes fallback" << '\n'; + LOG_ERROR("WardenModule: RC4 decryption failed; using raw module bytes fallback"); decryptedData_ = moduleData; } - std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n'; + LOG_INFO("WardenModule: RC4 decrypted (", decryptedData_.size(), " bytes)"); // Step 3: Verify RSA signature if (!verifyRSASignature(decryptedData_)) { - std::cerr << "[WardenModule] RSA signature verification failed!" << '\n'; + LOG_ERROR("WardenModule: RSA signature verification failed!"); // Note: Currently returns true (skipping verification) due to placeholder modulus } @@ -84,42 +86,42 @@ bool WardenModule::load(const std::vector& moduleData, dataWithoutSig = decryptedData_; } if (!decompressZlib(dataWithoutSig, decompressedData_)) { - std::cerr << "[WardenModule] zlib decompression failed; using decrypted bytes fallback" << '\n'; + LOG_ERROR("WardenModule: zlib decompression failed; using decrypted bytes fallback"); decompressedData_ = decryptedData_; } // Step 5: Parse custom executable format if (!parseExecutableFormat(decompressedData_)) { - std::cerr << "[WardenModule] Executable format parsing failed; continuing with minimal module image" << '\n'; + LOG_ERROR("WardenModule: Executable format parsing failed; continuing with minimal module image"); } // Step 6: Apply relocations if (!applyRelocations()) { - std::cerr << "[WardenModule] Address relocations failed; continuing with unrelocated image" << '\n'; + LOG_ERROR("WardenModule: Address relocations failed; continuing with unrelocated image"); } // Step 7: Bind APIs if (!bindAPIs()) { - std::cerr << "[WardenModule] API binding failed!" << '\n'; + LOG_ERROR("WardenModule: API binding failed!"); // Note: Currently returns true (stub) on both Windows and Linux } // Step 8: Initialize module if (!initializeModule()) { - std::cerr << "[WardenModule] Module initialization failed; continuing with stub callbacks" << '\n'; + LOG_ERROR("WardenModule: Module initialization failed; continuing with stub callbacks"); } // Module loading pipeline complete! // Note: Steps 6-8 are stubs/platform-limited, but infrastructure is ready loaded_ = true; // Mark as loaded (infrastructure complete) - std::cout << "[WardenModule] ✓ Module loading pipeline COMPLETE" << '\n'; - std::cout << "[WardenModule] Status: Infrastructure ready, execution stubs in place" << '\n'; - std::cout << "[WardenModule] Limitations:" << '\n'; - std::cout << "[WardenModule] - Relocations: needs real module data" << '\n'; - std::cout << "[WardenModule] - API Binding: Windows only (or Wine on Linux)" << '\n'; - std::cout << "[WardenModule] - Execution: disabled (unsafe without validation)" << '\n'; - std::cout << "[WardenModule] For strict servers: Would need to enable actual x86 execution" << '\n'; + LOG_INFO("WardenModule: Module loading pipeline COMPLETE"); + LOG_INFO("WardenModule: Status: Infrastructure ready, execution stubs in place"); + LOG_INFO("WardenModule: Limitations:"); + LOG_INFO("WardenModule: - Relocations: needs real module data"); + LOG_INFO("WardenModule: - API Binding: Windows only (or Wine on Linux)"); + LOG_INFO("WardenModule: - Execution: disabled (unsafe without validation)"); + LOG_INFO("WardenModule: For strict servers: Would need to enable actual x86 execution"); return true; } @@ -127,25 +129,25 @@ bool WardenModule::load(const std::vector& moduleData, bool WardenModule::processCheckRequest(const std::vector& checkData, [[maybe_unused]] std::vector& responseOut) { if (!loaded_) { - std::cerr << "[WardenModule] Module not loaded, cannot process checks" << '\n'; + LOG_ERROR("WardenModule: Module not loaded, cannot process checks"); return false; } #ifdef HAVE_UNICORN if (emulator_ && emulator_->isInitialized() && funcList_.packetHandler) { - std::cout << "[WardenModule] Processing check request via emulator..." << '\n'; - std::cout << "[WardenModule] Check data: " << checkData.size() << " bytes" << '\n'; + LOG_INFO("WardenModule: Processing check request via emulator..."); + LOG_INFO("WardenModule: Check data: ", checkData.size(), " bytes"); // Allocate memory for check data in emulated space uint32_t checkDataAddr = emulator_->allocateMemory(checkData.size(), 0x04); if (checkDataAddr == 0) { - std::cerr << "[WardenModule] Failed to allocate memory for check data" << '\n'; + LOG_ERROR("WardenModule: Failed to allocate memory for check data"); return false; } // Write check data to emulated memory if (!emulator_->writeMemory(checkDataAddr, checkData.data(), checkData.size())) { - std::cerr << "[WardenModule] Failed to write check data" << '\n'; + LOG_ERROR("WardenModule: Failed to write check data"); emulator_->freeMemory(checkDataAddr); return false; } @@ -153,7 +155,7 @@ bool WardenModule::processCheckRequest(const std::vector& checkData, // Allocate response buffer in emulated space (assume max 1KB response) uint32_t responseAddr = emulator_->allocateMemory(1024, 0x04); if (responseAddr == 0) { - std::cerr << "[WardenModule] Failed to allocate response buffer" << '\n'; + LOG_ERROR("WardenModule: Failed to allocate response buffer"); emulator_->freeMemory(checkDataAddr); return false; } @@ -162,13 +164,13 @@ bool WardenModule::processCheckRequest(const std::vector& checkData, // Call module's PacketHandler // void PacketHandler(uint8_t* checkData, size_t checkSize, // uint8_t* responseOut, size_t* responseSizeOut) - std::cout << "[WardenModule] Calling PacketHandler..." << '\n'; + LOG_INFO("WardenModule: Calling PacketHandler..."); // For now, this is a placeholder - actual calling would depend on // the module's exact function signature - std::cout << "[WardenModule] ⚠ PacketHandler execution stubbed" << '\n'; - std::cout << "[WardenModule] Would call emulated function to process checks" << '\n'; - std::cout << "[WardenModule] This would generate REAL responses (not fakes!)" << '\n'; + LOG_WARNING("WardenModule: PacketHandler execution stubbed"); + LOG_INFO("WardenModule: Would call emulated function to process checks"); + LOG_INFO("WardenModule: This would generate REAL responses (not fakes!)"); // Clean up emulator_->freeMemory(checkDataAddr); @@ -179,7 +181,7 @@ bool WardenModule::processCheckRequest(const std::vector& checkData, return false; } catch (const std::exception& e) { - std::cerr << "[WardenModule] Exception during PacketHandler: " << e.what() << '\n'; + LOG_ERROR("WardenModule: Exception during PacketHandler: ", e.what()); emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(responseAddr); return false; @@ -187,8 +189,8 @@ bool WardenModule::processCheckRequest(const std::vector& checkData, } #endif - std::cout << "[WardenModule] ⚠ processCheckRequest NOT IMPLEMENTED" << '\n'; - std::cout << "[WardenModule] Would call module->PacketHandler() here" << '\n'; + LOG_WARNING("WardenModule: processCheckRequest NOT IMPLEMENTED"); + LOG_INFO("WardenModule: Would call module->PacketHandler() here"); // For now, return false to fall back to fake responses in GameHandler return false; @@ -219,13 +221,13 @@ void WardenModule::unload() { if (moduleMemory_) { // Call module's Unload() function if loaded if (loaded_ && funcList_.unload) { - std::cout << "[WardenModule] Calling module unload callback..." << '\n'; + LOG_INFO("WardenModule: Calling module unload callback..."); // TODO: Implement callback when execution layer is complete // funcList_.unload(nullptr); } // Free executable memory region - std::cout << "[WardenModule] Freeing " << moduleSize_ << " bytes of executable memory" << '\n'; + LOG_INFO("WardenModule: Freeing ", moduleSize_, " bytes of executable memory"); #ifdef _WIN32 VirtualFree(moduleMemory_, 0, MEM_RELEASE); #else @@ -264,7 +266,7 @@ bool WardenModule::decryptRC4(const std::vector& encrypted, const std::vector& key, std::vector& decryptedOut) { if (key.size() != 16) { - std::cerr << "[WardenModule] Invalid RC4 key size: " << key.size() << " (expected 16)" << '\n'; + LOG_ERROR("WardenModule: Invalid RC4 key size: ", key.size(), " (expected 16)"); return false; } @@ -299,7 +301,7 @@ bool WardenModule::decryptRC4(const std::vector& encrypted, bool WardenModule::verifyRSASignature(const std::vector& data) { // RSA-2048 signature is last 256 bytes if (data.size() < 256) { - std::cerr << "[WardenModule] Data too small for RSA signature (need at least 256 bytes)" << '\n'; + LOG_ERROR("WardenModule: Data too small for RSA signature (need at least 256 bytes)"); return false; } @@ -385,7 +387,7 @@ bool WardenModule::verifyRSASignature(const std::vector& data) { if (pkey) EVP_PKEY_free(pkey); if (decryptedLen < 0) { - std::cerr << "[WardenModule] RSA public decrypt failed" << '\n'; + LOG_ERROR("WardenModule: RSA public decrypt failed"); return false; } @@ -398,24 +400,24 @@ bool WardenModule::verifyRSASignature(const std::vector& data) { std::vector actualHash(decryptedSig.end() - 20, decryptedSig.end()); if (std::memcmp(actualHash.data(), expectedHash.data(), 20) == 0) { - std::cout << "[WardenModule] ✓ RSA signature verified" << '\n'; + LOG_INFO("WardenModule: RSA signature verified"); return true; } } - std::cerr << "[WardenModule] RSA signature verification FAILED (hash mismatch)" << '\n'; - std::cerr << "[WardenModule] NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification" << '\n'; + LOG_ERROR("WardenModule: RSA signature verification FAILED (hash mismatch)"); + LOG_ERROR("WardenModule: NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification"); // For development, return true to proceed (since we don't have real modulus) // TODO: Set to false once real modulus is extracted - std::cout << "[WardenModule] ⚠ Skipping RSA verification (placeholder modulus)" << '\n'; + LOG_WARNING("WardenModule: Skipping RSA verification (placeholder modulus)"); return true; // TEMPORARY - change to false for production } bool WardenModule::decompressZlib(const std::vector& compressed, std::vector& decompressedOut) { if (compressed.size() < 4) { - std::cerr << "[WardenModule] Compressed data too small (need at least 4 bytes for size header)" << '\n'; + LOG_ERROR("WardenModule: Compressed data too small (need at least 4 bytes for size header)"); return false; } @@ -426,11 +428,11 @@ bool WardenModule::decompressZlib(const std::vector& compressed, (compressed[2] << 16) | (compressed[3] << 24); - std::cout << "[WardenModule] Uncompressed size: " << uncompressedSize << " bytes" << '\n'; + LOG_INFO("WardenModule: Uncompressed size: ", uncompressedSize, " bytes"); // Sanity check (modules shouldn't be larger than 10MB) if (uncompressedSize > 10 * 1024 * 1024) { - std::cerr << "[WardenModule] Uncompressed size suspiciously large: " << uncompressedSize << " bytes" << '\n'; + LOG_ERROR("WardenModule: Uncompressed size suspiciously large: ", uncompressedSize, " bytes"); return false; } @@ -447,7 +449,7 @@ bool WardenModule::decompressZlib(const std::vector& compressed, // Initialize inflater int ret = inflateInit(&stream); if (ret != Z_OK) { - std::cerr << "[WardenModule] inflateInit failed: " << ret << '\n'; + LOG_ERROR("WardenModule: inflateInit failed: ", ret); return false; } @@ -458,19 +460,18 @@ bool WardenModule::decompressZlib(const std::vector& compressed, inflateEnd(&stream); if (ret != Z_STREAM_END) { - std::cerr << "[WardenModule] inflate failed: " << ret << '\n'; + LOG_ERROR("WardenModule: inflate failed: ", ret); return false; } - std::cout << "[WardenModule] ✓ zlib decompression successful (" - << stream.total_out << " bytes decompressed)" << '\n'; + LOG_INFO("WardenModule: zlib decompression successful (", stream.total_out, " bytes decompressed)"); return true; } bool WardenModule::parseExecutableFormat(const std::vector& exeData) { if (exeData.size() < 4) { - std::cerr << "[WardenModule] Executable data too small for header" << '\n'; + LOG_ERROR("WardenModule: Executable data too small for header"); return false; } @@ -481,11 +482,11 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { (exeData[2] << 16) | (exeData[3] << 24); - std::cout << "[WardenModule] Final code size: " << finalCodeSize << " bytes" << '\n'; + LOG_INFO("WardenModule: Final code size: ", finalCodeSize, " bytes"); // Sanity check (executable shouldn't be larger than 5MB) if (finalCodeSize > 5 * 1024 * 1024 || finalCodeSize == 0) { - std::cerr << "[WardenModule] Invalid final code size: " << finalCodeSize << '\n'; + LOG_ERROR("WardenModule: Invalid final code size: ", finalCodeSize); return false; } @@ -500,7 +501,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { PAGE_EXECUTE_READWRITE ); if (!moduleMemory_) { - std::cerr << "[WardenModule] VirtualAlloc failed" << '\n'; + LOG_ERROR("WardenModule: VirtualAlloc failed"); return false; } #else @@ -513,7 +514,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { 0 ); if (moduleMemory_ == MAP_FAILED) { - std::cerr << "[WardenModule] mmap failed: " << strerror(errno) << '\n'; + LOG_ERROR("WardenModule: mmap failed: ", strerror(errno)); moduleMemory_ = nullptr; return false; } @@ -522,8 +523,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { moduleSize_ = finalCodeSize; std::memset(moduleMemory_, 0, moduleSize_); // Zero-initialize - std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at " - << moduleMemory_ << '\n'; + LOG_INFO("WardenModule: Allocated ", moduleSize_, " bytes of executable memory"); auto readU16LE = [&](size_t at) -> uint16_t { return static_cast(exeData[at] | (exeData[at + 1] << 8)); @@ -669,10 +669,10 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { if (usedFormat == PairFormat::SkipCopyData) formatName = "skip/copy/data"; if (usedFormat == PairFormat::CopySkipData) formatName = "copy/skip/data"; - std::cout << "[WardenModule] Parsed " << parsedPairCount << " pairs using format " - << formatName << ", final offset: " << parsedFinalOffset << "/" << finalCodeSize << '\n'; - std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_ - << " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n'; + LOG_INFO("WardenModule: Parsed ", parsedPairCount, " pairs using format ", + formatName, ", final offset: ", parsedFinalOffset, "/", finalCodeSize); + LOG_INFO("WardenModule: Relocation data starts at decompressed offset ", relocDataOffset_, + " (", (exeData.size() - relocDataOffset_), " bytes remaining)"); return true; } @@ -683,13 +683,13 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize); } relocDataOffset_ = 0; - std::cerr << "[WardenModule] Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback" << '\n'; + LOG_ERROR("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback"); return true; } bool WardenModule::applyRelocations() { if (!moduleMemory_ || moduleSize_ == 0) { - std::cerr << "[WardenModule] No module memory allocated for relocations" << '\n'; + LOG_ERROR("WardenModule: No module memory allocated for relocations"); return false; } @@ -698,7 +698,7 @@ bool WardenModule::applyRelocations() { // Each offset in the module image has moduleBase_ added to the 32-bit value there if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) { - std::cout << "[WardenModule] No relocation data available" << '\n'; + LOG_INFO("WardenModule: No relocation data available"); return true; } @@ -722,24 +722,27 @@ bool WardenModule::applyRelocations() { std::memcpy(addr, &val, sizeof(uint32_t)); relocCount++; } else { - std::cerr << "[WardenModule] Relocation offset " << currentOffset - << " out of bounds (moduleSize=" << moduleSize_ << ")" << '\n'; + LOG_ERROR("WardenModule: Relocation offset ", currentOffset, + " out of bounds (moduleSize=", moduleSize_, ")"); } } - std::cout << "[WardenModule] Applied " << relocCount << " relocations (base=0x" - << std::hex << moduleBase_ << std::dec << ")" << '\n'; + { + char baseBuf[32]; + std::snprintf(baseBuf, sizeof(baseBuf), "0x%X", moduleBase_); + LOG_INFO("WardenModule: Applied ", relocCount, " relocations (base=", baseBuf, ")"); + } return true; } bool WardenModule::bindAPIs() { if (!moduleMemory_ || moduleSize_ == 0) { - std::cerr << "[WardenModule] No module memory allocated for API binding" << '\n'; + LOG_ERROR("WardenModule: No module memory allocated for API binding"); return false; } - std::cout << "[WardenModule] Binding Windows APIs for module..." << '\n'; + LOG_INFO("WardenModule: Binding Windows APIs for module..."); // Common Windows APIs used by Warden modules: // @@ -759,14 +762,14 @@ bool WardenModule::bindAPIs() { #ifdef _WIN32 // On Windows: Use GetProcAddress to resolve imports - std::cout << "[WardenModule] Platform: Windows - using GetProcAddress" << '\n'; + LOG_INFO("WardenModule: Platform: Windows - using GetProcAddress"); HMODULE kernel32 = GetModuleHandleA("kernel32.dll"); HMODULE user32 = GetModuleHandleA("user32.dll"); HMODULE ntdll = GetModuleHandleA("ntdll.dll"); if (!kernel32 || !user32 || !ntdll) { - std::cerr << "[WardenModule] Failed to get module handles" << '\n'; + LOG_ERROR("WardenModule: Failed to get module handles"); return false; } @@ -777,8 +780,8 @@ bool WardenModule::bindAPIs() { // - Resolve address using GetProcAddress // - Write address to Import Address Table (IAT) - std::cout << "[WardenModule] ⚠ Windows API binding is STUB (needs PE import table parsing)" << '\n'; - std::cout << "[WardenModule] Would parse PE headers and patch IAT with resolved addresses" << '\n'; + LOG_WARNING("WardenModule: Windows API binding is STUB (needs PE import table parsing)"); + LOG_INFO("WardenModule: Would parse PE headers and patch IAT with resolved addresses"); #else // On Linux: Cannot directly execute Windows code @@ -787,15 +790,15 @@ bool WardenModule::bindAPIs() { // 2. Implement Windows API stubs (limited functionality) // 3. Use binfmt_misc + Wine (transparent Windows executable support) - std::cout << "[WardenModule] Platform: Linux - Windows module execution NOT supported" << '\n'; - std::cout << "[WardenModule] Options:" << '\n'; - std::cout << "[WardenModule] 1. Run wowee under Wine (provides Windows API layer)" << '\n'; - std::cout << "[WardenModule] 2. Use a Windows VM" << '\n'; - std::cout << "[WardenModule] 3. Implement Windows API stubs (limited, complex)" << '\n'; + LOG_WARNING("WardenModule: Platform: Linux - Windows module execution NOT supported"); + LOG_INFO("WardenModule: Options:"); + LOG_INFO("WardenModule: 1. Run wowee under Wine (provides Windows API layer)"); + LOG_INFO("WardenModule: 2. Use a Windows VM"); + LOG_INFO("WardenModule: 3. Implement Windows API stubs (limited, complex)"); // For now, we'll return true to continue the loading pipeline // Real execution would fail, but this allows testing the infrastructure - std::cout << "[WardenModule] ⚠ Skipping API binding (Linux platform limitation)" << '\n'; + LOG_WARNING("WardenModule: Skipping API binding (Linux platform limitation)"); #endif return true; // Return true to continue (stub implementation) @@ -803,11 +806,11 @@ bool WardenModule::bindAPIs() { bool WardenModule::initializeModule() { if (!moduleMemory_ || moduleSize_ == 0) { - std::cerr << "[WardenModule] No module memory allocated for initialization" << '\n'; + LOG_ERROR("WardenModule: No module memory allocated for initialization"); return false; } - std::cout << "[WardenModule] Initializing Warden module..." << '\n'; + LOG_INFO("WardenModule: Initializing Warden module..."); // Module initialization protocol: // @@ -844,27 +847,27 @@ bool WardenModule::initializeModule() { // Stub callbacks (would need real implementations) callbacks.sendPacket = []([[maybe_unused]] uint8_t* data, size_t len) { - std::cout << "[WardenModule Callback] sendPacket(" << len << " bytes)" << '\n'; + LOG_DEBUG("WardenModule Callback: sendPacket(", len, " bytes)"); // TODO: Send CMSG_WARDEN_DATA packet }; callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) { - std::cout << "[WardenModule Callback] validateModule()" << '\n'; + LOG_DEBUG("WardenModule Callback: validateModule()"); // TODO: Validate module hash }; callbacks.allocMemory = [](size_t size) -> void* { - std::cout << "[WardenModule Callback] allocMemory(" << size << ")" << '\n'; + LOG_DEBUG("WardenModule Callback: allocMemory(", size, ")"); return malloc(size); }; callbacks.freeMemory = [](void* ptr) { - std::cout << "[WardenModule Callback] freeMemory()" << '\n'; + LOG_DEBUG("WardenModule Callback: freeMemory()"); free(ptr); }; callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) { - std::cout << "[WardenModule Callback] generateRC4()" << '\n'; + LOG_DEBUG("WardenModule Callback: generateRC4()"); // TODO: Re-key RC4 cipher }; @@ -873,7 +876,7 @@ bool WardenModule::initializeModule() { }; callbacks.logMessage = [](const char* msg) { - std::cout << "[WardenModule Log] " << msg << '\n'; + LOG_INFO("WardenModule Log: ", msg); }; // Module entry point is typically at offset 0 (first bytes of loaded code) @@ -881,24 +884,28 @@ bool WardenModule::initializeModule() { #ifdef HAVE_UNICORN // Use Unicorn emulator for cross-platform execution - std::cout << "[WardenModule] Initializing Unicorn emulator..." << '\n'; + LOG_INFO("WardenModule: Initializing Unicorn emulator..."); emulator_ = std::make_unique(); if (!emulator_->initialize(moduleMemory_, moduleSize_, moduleBase_)) { - std::cerr << "[WardenModule] Failed to initialize emulator" << '\n'; + LOG_ERROR("WardenModule: Failed to initialize emulator"); return false; } // Setup Windows API hooks emulator_->setupCommonAPIHooks(); - std::cout << "[WardenModule] ✓ Emulator initialized successfully" << '\n'; - std::cout << "[WardenModule] Ready to execute module at 0x" << std::hex << moduleBase_ << std::dec << '\n'; + { + char addrBuf[32]; + std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", moduleBase_); + LOG_INFO("WardenModule: Emulator initialized successfully"); + LOG_INFO("WardenModule: Ready to execute module at ", addrBuf); + } // Allocate memory for ClientCallbacks structure in emulated space uint32_t callbackStructAddr = emulator_->allocateMemory(sizeof(ClientCallbacks), 0x04); if (callbackStructAddr == 0) { - std::cerr << "[WardenModule] Failed to allocate memory for callbacks" << '\n'; + LOG_ERROR("WardenModule: Failed to allocate memory for callbacks"); return false; } @@ -921,13 +928,21 @@ bool WardenModule::initializeModule() { emulator_->writeMemory(callbackStructAddr + (i * 4), &addr, 4); } - std::cout << "[WardenModule] Prepared ClientCallbacks at 0x" << std::hex << callbackStructAddr << std::dec << '\n'; + { + char cbBuf[32]; + std::snprintf(cbBuf, sizeof(cbBuf), "0x%X", callbackStructAddr); + LOG_INFO("WardenModule: Prepared ClientCallbacks at ", cbBuf); + } // Call module entry point // Entry point is typically at module base (offset 0) uint32_t entryPoint = moduleBase_; - std::cout << "[WardenModule] Calling module entry point at 0x" << std::hex << entryPoint << std::dec << '\n'; + { + char epBuf[32]; + std::snprintf(epBuf, sizeof(epBuf), "0x%X", entryPoint); + LOG_INFO("WardenModule: Calling module entry point at ", epBuf); + } try { // Call: WardenFuncList* InitModule(ClientCallbacks* callbacks) @@ -935,21 +950,28 @@ bool WardenModule::initializeModule() { uint32_t result = emulator_->callFunction(entryPoint, args); if (result == 0) { - std::cerr << "[WardenModule] Module entry returned NULL" << '\n'; + LOG_ERROR("WardenModule: Module entry returned NULL"); return false; } - std::cout << "[WardenModule] ✓ Module initialized, WardenFuncList at 0x" << std::hex << result << std::dec << '\n'; + { + char resBuf[32]; + std::snprintf(resBuf, sizeof(resBuf), "0x%X", result); + LOG_INFO("WardenModule: Module initialized, WardenFuncList at ", resBuf); + } // Read WardenFuncList structure from emulated memory // Structure has 4 function pointers (16 bytes) uint32_t funcAddrs[4] = {}; if (emulator_->readMemory(result, funcAddrs, 16)) { - std::cout << "[WardenModule] Module exported functions:" << '\n'; - std::cout << "[WardenModule] generateRC4Keys: 0x" << std::hex << funcAddrs[0] << std::dec << '\n'; - std::cout << "[WardenModule] unload: 0x" << std::hex << funcAddrs[1] << std::dec << '\n'; - std::cout << "[WardenModule] packetHandler: 0x" << std::hex << funcAddrs[2] << std::dec << '\n'; - std::cout << "[WardenModule] tick: 0x" << std::hex << funcAddrs[3] << std::dec << '\n'; + char fb[4][32]; + for (int fi = 0; fi < 4; ++fi) + std::snprintf(fb[fi], sizeof(fb[fi]), "0x%X", funcAddrs[fi]); + LOG_INFO("WardenModule: Module exported functions:"); + LOG_INFO("WardenModule: generateRC4Keys: ", fb[0]); + LOG_INFO("WardenModule: unload: ", fb[1]); + LOG_INFO("WardenModule: packetHandler: ", fb[2]); + LOG_INFO("WardenModule: tick: ", fb[3]); // Store function addresses for later use // funcList_.generateRC4Keys = ... (would wrap emulator calls) @@ -958,10 +980,10 @@ bool WardenModule::initializeModule() { // funcList_.tick = ... } - std::cout << "[WardenModule] ✓ Module fully initialized and ready!" << '\n'; + LOG_INFO("WardenModule: Module fully initialized and ready!"); } catch (const std::exception& e) { - std::cerr << "[WardenModule] Exception during module initialization: " << e.what() << '\n'; + LOG_ERROR("WardenModule: Exception during module initialization: ", e.what()); return false; } @@ -970,14 +992,14 @@ bool WardenModule::initializeModule() { typedef void* (*ModuleEntryPoint)(ClientCallbacks*); ModuleEntryPoint entryPoint = reinterpret_cast(moduleMemory_); - std::cout << "[WardenModule] Calling module entry point at " << moduleMemory_ << '\n'; + LOG_INFO("WardenModule: Calling module entry point at ", moduleMemory_); // NOTE: This would execute native x86 code // Extremely dangerous without proper validation! // void* result = entryPoint(&callbacks); - std::cout << "[WardenModule] ⚠ Module entry point call is DISABLED (unsafe without validation)" << '\n'; - std::cout << "[WardenModule] Would execute x86 code at " << moduleMemory_ << '\n'; + LOG_WARNING("WardenModule: Module entry point call is DISABLED (unsafe without validation)"); + LOG_INFO("WardenModule: Would execute x86 code at ", moduleMemory_); // TODO: Extract WardenFuncList from result // funcList_.packetHandler = ... @@ -986,9 +1008,9 @@ bool WardenModule::initializeModule() { // funcList_.unload = ... #else - std::cout << "[WardenModule] ⚠ Cannot execute Windows x86 code on Linux" << '\n'; - std::cout << "[WardenModule] Module entry point: " << moduleMemory_ << '\n'; - std::cout << "[WardenModule] Would call entry point with ClientCallbacks struct" << '\n'; + LOG_WARNING("WardenModule: Cannot execute Windows x86 code on Linux"); + LOG_INFO("WardenModule: Module entry point: ", moduleMemory_); + LOG_INFO("WardenModule: Would call entry point with ClientCallbacks struct"); #endif // For now, return true to mark module as "loaded" at infrastructure level @@ -998,7 +1020,7 @@ bool WardenModule::initializeModule() { // 3. Exception handling for crashes // 4. Sandboxing for security - std::cout << "[WardenModule] ⚠ Module initialization is STUB" << '\n'; + LOG_WARNING("WardenModule: Module initialization is STUB"); return true; // Stub implementation } @@ -1023,7 +1045,7 @@ WardenModuleManager::WardenModuleManager() { // Create cache directory if it doesn't exist std::filesystem::create_directories(cacheDirectory_); - std::cout << "[WardenModuleManager] Cache directory: " << cacheDirectory_ << '\n'; + LOG_INFO("WardenModuleManager: Cache directory: ", cacheDirectory_); } WardenModuleManager::~WardenModuleManager() { @@ -1060,12 +1082,11 @@ bool WardenModuleManager::receiveModuleChunk(const std::vector& md5Hash std::vector& buffer = downloadBuffer_[md5Hash]; buffer.insert(buffer.end(), chunkData.begin(), chunkData.end()); - std::cout << "[WardenModuleManager] Received chunk (" << chunkData.size() - << " bytes, total: " << buffer.size() << ")" << '\n'; + LOG_INFO("WardenModuleManager: Received chunk (", chunkData.size(), + " bytes, total: ", buffer.size(), ")"); if (isComplete) { - std::cout << "[WardenModuleManager] Module download complete (" - << buffer.size() << " bytes)" << '\n'; + LOG_INFO("WardenModuleManager: Module download complete (", buffer.size(), " bytes)"); // Cache to disk cacheModule(md5Hash, buffer); @@ -1085,14 +1106,14 @@ bool WardenModuleManager::cacheModule(const std::vector& md5Hash, std::ofstream file(cachePath, std::ios::binary); if (!file) { - std::cerr << "[WardenModuleManager] Failed to write cache: " << cachePath << '\n'; + LOG_ERROR("WardenModuleManager: Failed to write cache: ", cachePath); return false; } file.write(reinterpret_cast(moduleData.data()), moduleData.size()); file.close(); - std::cout << "[WardenModuleManager] Cached module to: " << cachePath << '\n'; + LOG_INFO("WardenModuleManager: Cached module to: ", cachePath); return true; } @@ -1116,7 +1137,7 @@ bool WardenModuleManager::loadCachedModule(const std::vector& md5Hash, file.read(reinterpret_cast(moduleDataOut.data()), fileSize); file.close(); - std::cout << "[WardenModuleManager] Loaded cached module (" << fileSize << " bytes)" << '\n'; + LOG_INFO("WardenModuleManager: Loaded cached module (", fileSize, " bytes)"); return true; } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 5a9804a1..5d322ff1 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -4257,10 +4257,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, data.gold = packet.readUInt32(); uint8_t itemCount = packet.readUInt8(); - // Item wire size: - // WotLK 3.3.5a: slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22 - // Classic/TBC: slot(1)+itemId(4)+count(4)+displayInfo(4)+slotType(1) = 14 - const size_t kItemSize = isWotlkFormat ? 22u : 14u; + // Per-item wire size is 22 bytes across all expansions: + // slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22 + constexpr size_t kItemSize = 22u; auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool { for (uint8_t i = 0; i < listCount; ++i) { @@ -4270,21 +4269,14 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, } LootItem item; - item.slotIndex = packet.readUInt8(); - item.itemId = packet.readUInt32(); - item.count = packet.readUInt32(); - item.displayInfoId = packet.readUInt32(); - - if (isWotlkFormat) { - item.randomSuffix = packet.readUInt32(); - item.randomPropertyId = packet.readUInt32(); - } else { - item.randomSuffix = 0; - item.randomPropertyId = 0; - } - - item.lootSlotType = packet.readUInt8(); - item.isQuestItem = markQuestItems; + item.slotIndex = packet.readUInt8(); + item.itemId = packet.readUInt32(); + item.count = packet.readUInt32(); + item.displayInfoId = packet.readUInt32(); + item.randomSuffix = packet.readUInt32(); + item.randomPropertyId = packet.readUInt32(); + item.lootSlotType = packet.readUInt8(); + item.isQuestItem = markQuestItems; data.items.push_back(item); } return true; @@ -4296,8 +4288,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, return false; } + // Quest item section only present in WotLK 3.3.5a uint8_t questItemCount = 0; - if (packet.getSize() - packet.getReadPos() >= 1) { + if (isWotlkFormat && packet.getSize() - packet.getReadPos() >= 1) { questItemCount = packet.readUInt8(); data.items.reserve(data.items.size() + questItemCount); if (!parseLootItemList(questItemCount, true)) { diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index 0f44869b..cce494d9 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -228,6 +228,7 @@ void Minimap::shutdown() { if (tex) tex->destroy(device, alloc); } tileTextureCache.clear(); + tileInsertionOrder.clear(); if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); } if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); } @@ -362,6 +363,15 @@ VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) { VkTexture* ptr = tex.get(); tileTextureCache[hash] = std::move(tex); + tileInsertionOrder.push_back(hash); + + // Evict oldest tiles when cache grows too large to bound GPU memory usage. + while (tileInsertionOrder.size() > MAX_TILE_CACHE) { + const std::string& oldest = tileInsertionOrder.front(); + tileTextureCache.erase(oldest); + tileInsertionOrder.pop_front(); + } + return ptr; } @@ -513,14 +523,15 @@ void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera, float arrowRotation = 0.0f; if (!rotateWithCamera) { - // Prefer authoritative orientation if provided. This value is expected - // to already match minimap shader rotation convention. if (hasPlayerOrientation) { arrowRotation = playerOrientation; } else { glm::vec3 fwd = playerCamera.getForward(); - arrowRotation = std::atan2(-fwd.x, fwd.y); + arrowRotation = -std::atan2(-fwd.x, fwd.y); } + } else if (hasPlayerOrientation) { + // Show character facing relative to the rotated map + arrowRotation = playerOrientation + rotation; } MinimapDisplayPush push{}; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index f7f07e42..67a637bb 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -5287,6 +5287,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(tint, cmd); } } + // Ghost mode desaturation: cold blue-grey overlay when dead/ghost + if (ghostMode_) { + renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd); + } + // Brightness overlay (applied before minimap so it doesn't affect UI) + if (brightness_ < 0.99f) { + renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_), cmd); + } else if (brightness_ > 1.01f) { + float alpha = (brightness_ - 1.0f) / 1.0f; // maps 1.0-2.0 → 0.0-1.0 + renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd); + } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); if (cameraController && cameraController->isThirdPerson()) @@ -5421,6 +5432,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(tint); } } + // Ghost mode desaturation: cold blue-grey overlay when dead/ghost + if (ghostMode_) { + renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f)); + } + // Brightness overlay (applied before minimap so it doesn't affect UI) + if (brightness_ < 0.99f) { + renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_)); + } else if (brightness_ > 1.01f) { + float alpha = (brightness_ - 1.0f) / 1.0f; + renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha)); + } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); if (cameraController && cameraController->isThirdPerson()) diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 7ee4f43a..8163628d 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -835,7 +835,8 @@ void WorldMap::zoomOut() { // Main render (input + ImGui overlay) // -------------------------------------------------------- -void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { +void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, + float playerYawDeg) { if (!initialized || !assetManager) return; auto& input = core::Input::getInstance(); @@ -886,14 +887,14 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr } if (!open) return; - renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight); + renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight, playerYawDeg); } // -------------------------------------------------------- // ImGui overlay // -------------------------------------------------------- -void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { +void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, float playerYawDeg) { float sw = static_cast(screenWidth); float sh = static_cast(screenHeight); @@ -1014,8 +1015,20 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi playerUV.y >= 0.0f && playerUV.y <= 1.0f) { float px = imgMin.x + playerUV.x * displayW; float py = imgMin.y + playerUV.y * displayH; - drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255)); - drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f); + // Directional arrow: render-space (cos,sin) maps to screen (-dx,-dy) + // because render+X=west=left and render+Y=north=up (screen Y is down). + float yawRad = glm::radians(playerYawDeg); + float adx = -std::cos(yawRad); // screen-space arrow X + float ady = -std::sin(yawRad); // screen-space arrow Y + float apx = -ady, apy = adx; // perpendicular (left/right of arrow) + constexpr float TIP = 9.0f; // tip distance from center + constexpr float TAIL = 4.0f; // tail distance from center + constexpr float HALF = 5.0f; // half base width + ImVec2 tip(px + adx * TIP, py + ady * TIP); + ImVec2 bl (px - adx * TAIL + apx * HALF, py - ady * TAIL + apy * HALF); + ImVec2 br (px - adx * TAIL - apx * HALF, py - ady * TAIL - apy * HALF); + drawList->AddTriangleFilled(tip, bl, br, IM_COL32(255, 40, 40, 255)); + drawList->AddTriangle(tip, bl, br, IM_COL32(0, 0, 0, 200), 1.5f); } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 287f0456..771a1ba2 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2091,18 +2091,20 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { std::string cmd = buf.substr(1, sp - 1); for (char& c : cmd) c = std::tolower(c); int detected = -1; + bool isReply = false; if (cmd == "s" || cmd == "say") detected = 0; else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1; else if (cmd == "p" || cmd == "party") detected = 2; else if (cmd == "g" || cmd == "guild") detected = 3; else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4; + else if (cmd == "r" || cmd == "reply") { detected = 4; isReply = true; } else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5; else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6; else if (cmd == "bg" || cmd == "battleground") detected = 7; else if (cmd == "rw" || cmd == "raidwarning") detected = 8; else if (cmd == "i" || cmd == "instance") detected = 9; else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc. - if (detected >= 0 && (selectedChatType != detected || detected == 10)) { + if (detected >= 0 && (selectedChatType != detected || detected == 10 || isReply)) { // For channel shortcuts, also update selectedChannelIdx if (detected == 10) { int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc. @@ -2114,8 +2116,16 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { selectedChatType = detected; // Strip the prefix, keep only the message part std::string remaining = buf.substr(sp + 1); - // For whisper, first word after /w is the target - if (detected == 4) { + // /r reply: pre-fill whisper target from last whisper sender + if (detected == 4 && isReply) { + std::string lastSender = gameHandler.getLastWhisperSender(); + if (!lastSender.empty()) { + strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + } + // remaining is the message — don't extract a target from it + } else if (detected == 4) { + // For whisper, first word after /w is the target size_t msgStart = remaining.find(' '); if (msgStart != std::string::npos) { std::string wTarget = remaining.substr(0, msgStart); @@ -2576,6 +2586,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { uint64_t closestHostileUnitGuid = 0; float closestQuestGoT = 1e30f; uint64_t closestQuestGoGuid = 0; + float closestGoT = 1e30f; + uint64_t closestGoGuid = 0; const uint64_t myGuid = gameHandler.getPlayerGuid(); for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { auto t = entity->getType(); @@ -2598,16 +2610,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { heightOffset = 0.3f; } } else if (t == game::ObjectType::GAMEOBJECT) { - // For GOs with no renderer instance yet, use a tight fallback - // sphere so invisible/unloaded doodads aren't accidentally clicked. - hitRadius = 1.2f; - heightOffset = 1.0f; - // Quest objective GOs should be easier to click. - auto go = std::static_pointer_cast(entity); - if (questObjectiveGoEntries.count(go->getEntry())) { - hitRadius = 2.2f; - heightOffset = 1.2f; - } + hitRadius = 2.5f; + heightOffset = 1.2f; } hitCenter = core::coords::canonicalToRender( glm::vec3(entity->getX(), entity->getY(), entity->getZ())); @@ -2626,12 +2630,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { closestHostileUnitGuid = guid; } } - if (t == game::ObjectType::GAMEOBJECT && !questObjectiveGoEntries.empty()) { - auto go = std::static_pointer_cast(entity); - if (questObjectiveGoEntries.count(go->getEntry())) { - if (hitT < closestQuestGoT) { - closestQuestGoT = hitT; - closestQuestGoGuid = guid; + if (t == game::ObjectType::GAMEOBJECT) { + if (hitT < closestGoT) { + closestGoT = hitT; + closestGoGuid = guid; + } + if (!questObjectiveGoEntries.empty()) { + auto go = std::static_pointer_cast(entity); + if (questObjectiveGoEntries.count(go->getEntry())) { + if (hitT < closestQuestGoT) { + closestQuestGoT = hitT; + closestQuestGoGuid = guid; + } } } } @@ -2643,12 +2653,23 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } } - // Prefer quest objective GOs over hostile monsters when both are hittable. + // Priority: quest GO > closer of (GO, hostile unit) > closest anything. if (closestQuestGoGuid != 0) { closestGuid = closestQuestGoGuid; closestType = game::ObjectType::GAMEOBJECT; + } else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) { + // Both a GO and hostile unit were hit — prefer whichever is closer. + if (closestGoT <= closestHostileUnitT) { + closestGuid = closestGoGuid; + closestType = game::ObjectType::GAMEOBJECT; + } else { + closestGuid = closestHostileUnitGuid; + closestType = game::ObjectType::UNIT; + } + } else if (closestGoGuid != 0) { + closestGuid = closestGoGuid; + closestType = game::ObjectType::GAMEOBJECT; } else if (closestHostileUnitGuid != 0) { - // Prefer hostile monsters over nearby gameobjects/others when right-click picking. closestGuid = closestHostileUnitGuid; closestType = game::ObjectType::UNIT; } @@ -3536,17 +3557,22 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { // WoW level-based color for hostile mobs uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); - int32_t diff = static_cast(mobLv) - static_cast(playerLv); - if (game::GameHandler::killXp(playerLv, mobLv) == 0) { - hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP - } else if (diff >= 10) { - hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard - } else if (diff >= 5) { - hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard - } else if (diff >= -2) { - hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even + if (mobLv == 0) { + // Level 0 = unknown/?? (e.g. high-level raid bosses) — always skull red + hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); } else { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy + int32_t diff = static_cast(mobLv) - static_cast(playerLv); + if (game::GameHandler::killXp(playerLv, mobLv) == 0) { + hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP + } else if (diff >= 10) { + hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard + } else if (diff >= 5) { + hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard + } else if (diff >= -2) { + hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even + } else { + hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy + } } } else { hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly @@ -3724,7 +3750,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (target->getType() == game::ObjectType::PLAYER) { levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); } - ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); + if (unit->getLevel() == 0) + ImGui::TextColored(levelColor, "Lv ??"); + else + ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); // Classification badge: Elite / Rare Elite / Boss / Rare if (target->getType() == game::ObjectType::UNIT) { int rank = gameHandler.getCreatureRank(unit->getEntry()); @@ -4315,17 +4344,21 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { } else if (u->isHostile()) { uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); - int32_t diff = static_cast(mobLv) - static_cast(playerLv); - if (game::GameHandler::killXp(playerLv, mobLv) == 0) - focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); - else if (diff >= 10) - focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); - else if (diff >= 5) - focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); - else if (diff >= -2) - focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); - else - focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + if (mobLv == 0) { + focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // ?? level = skull red + } else { + int32_t diff = static_cast(mobLv) - static_cast(playerLv); + if (game::GameHandler::killXp(playerLv, mobLv) == 0) + focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); + else if (diff >= 10) + focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); + else if (diff >= 5) + focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); + else if (diff >= -2) + focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); + else + focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + } } else { focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); } @@ -4458,7 +4491,10 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { // Level + health on same row ImGui::SameLine(); - ImGui::TextDisabled("Lv %u", unit->getLevel()); + if (unit->getLevel() == 0) + ImGui::TextDisabled("Lv ??"); + else + ImGui::TextDisabled("Lv %u", unit->getLevel()); uint32_t hp = unit->getHealth(); uint32_t maxHp = unit->getMaxHealth(); @@ -5951,6 +5987,28 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { message = ""; isChannelCommand = true; } + } else if (cmdLower == "r" || cmdLower == "reply") { + switchChatType = 4; + std::string lastSender = gameHandler.getLastWhisperSender(); + if (lastSender.empty()) { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = "No one has whispered you yet."; + gameHandler.addLocalChatMessage(sysMsg); + chatInputBuffer[0] = '\0'; + return; + } + target = lastSender; + strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + if (spacePos != std::string::npos) { + message = command.substr(spacePos + 1); + type = game::ChatType::WHISPER; + } else { + message = ""; + } + isChannelCommand = true; } // Check for emote commands @@ -6496,10 +6554,11 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { } glm::vec3 playerPos = renderer->getCharacterPosition(); + float playerYaw = renderer->getCharacterYaw(); auto* window = app.getWindow(); int screenW = window ? window->getWidth() : 1280; int screenH = window ? window->getHeight() : 720; - wm->render(playerPos, screenW, screenH); + wm->render(playerPos, screenW, screenH, playerYaw); // Sync showWorldMap_ if the map closed itself (e.g. ESC key inside the overlay). if (!wm->isOpen()) showWorldMap_ = false; @@ -6551,17 +6610,23 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage } }; - // Always use expansion-aware layout if available - // Field indices vary by expansion: Classic=117, TBC=124, WotLK=133 + // Use expansion-aware layout if available AND the DBC field count + // matches the expansion's expected format. Classic=173, TBC=216, + // WotLK=234 fields. When Classic is active but the base WotLK DBC + // is loaded (234 fields), field 117 is NOT IconID — we must use + // the WotLK field 133 instead. + uint32_t iconField = 133; // WotLK default + uint32_t idField = 0; if (spellL) { - tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]); - } - - // Fallback if expansion layout missing or yielded nothing - // Only use WotLK field 133 as last resort if we have no layout - if (spellIconIds_.empty() && !spellL && fieldCount > 133) { - tryLoadIcons(0, 133); + uint32_t layoutIcon = (*spellL)["IconID"]; + // Only trust the expansion layout if the DBC has a compatible + // field count (within ~20 of the layout's icon field). + if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) { + iconField = layoutIcon; + idField = (*spellL)["ID"]; + } } + tryLoadIcons(idField, iconField); } } @@ -6664,6 +6729,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { // Out-of-range check: red tint when a targeted spell cannot reach the current target. // Only applies to SPELL slots with a known max range (>5 yd) and an active target. + // Item range is checked below after barItemDef is populated. bool outOfRange = false; if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 && !onCooldown && gameHandler.hasTarget()) { @@ -6753,6 +6819,33 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0 && barItemDef == nullptr && !onCooldown); + // Ranged item out-of-range check (runs after barItemDef is populated above). + // invType 15=Ranged (bow/gun/crossbow), 26=Thrown, 28=RangedRight (wand/crossbow). + if (!outOfRange && slot.type == game::ActionBarSlot::ITEM && barItemDef + && !onCooldown && gameHandler.hasTarget()) { + constexpr uint8_t INVTYPE_RANGED = 15; + constexpr uint8_t INVTYPE_THROWN = 26; + constexpr uint8_t INVTYPE_RANGEDRIGHT = 28; + uint32_t itemMaxRange = 0; + if (barItemDef->inventoryType == INVTYPE_RANGED || + barItemDef->inventoryType == INVTYPE_RANGEDRIGHT) + itemMaxRange = 40; + else if (barItemDef->inventoryType == INVTYPE_THROWN) + itemMaxRange = 30; + if (itemMaxRange > 0) { + auto& em = gameHandler.getEntityManager(); + auto playerEnt = em.getEntity(gameHandler.getPlayerGuid()); + auto targetEnt = em.getEntity(gameHandler.getTargetGuid()); + if (playerEnt && targetEnt) { + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + if (std::sqrt(dx*dx + dy*dy + dz*dz) > static_cast(itemMaxRange)) + outOfRange = true; + } + } + } + bool clicked = false; if (iconTex) { ImVec4 tintColor(1, 1, 1, 1); @@ -7645,7 +7738,8 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0; ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience"); ImGui::Separator(); - ImGui::Text("Current: %u / %u XP", currentXp, nextLevelXp); + float xpPct = nextLevelXp > 0 ? (100.0f * currentXp / nextLevelXp) : 0.0f; + ImGui::Text("Current: %u / %u XP (%.1f%%)", currentXp, nextLevelXp, xpPct); ImGui::Text("To next level: %u XP", xpToLevel); if (restedXp > 0) { float restedLevels = static_cast(restedXp) / static_cast(nextLevelXp); @@ -7828,16 +7922,21 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) { : ImVec4(0.8f, 0.6f, 0.2f, 1.0f); // gold for casts ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); - char overlay[64]; + char overlay[96]; if (currentSpellId == 0) { snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining()); } else { const std::string& spellName = gameHandler.getSpellName(currentSpellId); const char* verb = channeling ? "Channeling" : "Casting"; - if (!spellName.empty()) - snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining()); - else + int queueLeft = gameHandler.getCraftQueueRemaining(); + if (!spellName.empty()) { + if (queueLeft > 0) + snprintf(overlay, sizeof(overlay), "%s (%.1fs) [%d left]", spellName.c_str(), gameHandler.getCastTimeRemaining(), queueLeft); + else + snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining()); + } else { snprintf(overlay, sizeof(overlay), "%s... (%.1fs)", verb, gameHandler.getCastTimeRemaining()); + } } if (iconTex) { @@ -8413,10 +8512,21 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { snprintf(text, sizeof(text), "+%d", entry.amount); color = ImVec4(0.4f, 1.0f, 0.5f, alpha); break; - case game::CombatTextEntry::ENVIRONMENTAL: - snprintf(text, sizeof(text), "-%d", entry.amount); + case game::CombatTextEntry::ENVIRONMENTAL: { + const char* envLabel = ""; + switch (entry.powerType) { + case 0: envLabel = "Fatigue "; break; + case 1: envLabel = "Drowning "; break; + case 2: envLabel = ""; break; // Fall: just show the number (WoW convention) + case 3: envLabel = "Lava "; break; + case 4: envLabel = "Slime "; break; + case 5: envLabel = "Fire "; break; + default: envLabel = ""; break; + } + snprintf(text, sizeof(text), "%s-%d", envLabel, entry.amount); color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental break; + } case game::CombatTextEntry::ENERGIZE: snprintf(text, sizeof(text), "+%d", entry.amount); switch (entry.powerType) { @@ -9285,6 +9395,15 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { else if (m.roles & 0x08) draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D"); + // Tactical role badge in bottom-left corner (flags from SMSG_GROUP_LIST / SMSG_REAL_GROUP_UPDATE) + // 0x01=Assistant, 0x02=Main Tank, 0x04=Main Assist + if (m.flags & 0x02) + draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(255, 140, 0, 230), "MT"); + else if (m.flags & 0x04) + draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(100, 180, 255, 230), "MA"); + else if (m.flags & 0x01) + draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(180, 215, 255, 180), "A"); + // Health bar uint32_t hp = m.hasPartyStats ? m.curHealth : 0; uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0; @@ -9520,6 +9639,18 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); } } + // Tactical role badge (MT/MA/Asst) from group flags + if (member.flags & 0x02) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.0f, 0.9f), "[MT]"); + } else if (member.flags & 0x04) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 0.9f), "[MA]"); + } else if (member.flags & 0x01) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 0.7f), "[A]"); + } + // Raid mark symbol — shown on same line as name when this party member has a mark { static const struct { const char* sym; ImU32 col; } kPartyMarks[] = { @@ -13618,6 +13749,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { } const auto& trainer = gameHandler.getTrainerSpells(); + const bool isProfessionTrainer = (trainer.trainerType == 2); // NPC name auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid); @@ -13838,11 +13970,21 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { logCount++; } - if (!canTrain) ImGui::BeginDisabled(); - if (ImGui::SmallButton("Train")) { - gameHandler.trainSpell(spell->spellId); + if (isProfessionTrainer && alreadyKnown) { + // Profession trainer: known recipes show "Create" button to craft + bool isCasting = gameHandler.isCasting(); + if (isCasting) ImGui::BeginDisabled(); + if (ImGui::SmallButton("Create")) { + gameHandler.castSpell(spell->spellId, 0); + } + if (isCasting) ImGui::EndDisabled(); + } else { + if (!canTrain) ImGui::BeginDisabled(); + if (ImGui::SmallButton("Train")) { + gameHandler.trainSpell(spell->spellId); + } + if (!canTrain) ImGui::EndDisabled(); } - if (!canTrain) ImGui::EndDisabled(); ImGui::PopID(); } @@ -13946,6 +14088,79 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { } } if (!hasTrainable) ImGui::EndDisabled(); + + // Profession trainer: craft quantity controls + if (isProfessionTrainer) { + ImGui::Separator(); + static int craftQuantity = 1; + static uint32_t selectedCraftSpell = 0; + + // Show craft queue status if active + int queueRemaining = gameHandler.getCraftQueueRemaining(); + if (queueRemaining > 0) { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), + "Crafting... %d remaining", queueRemaining); + ImGui::SameLine(); + if (ImGui::SmallButton("Stop")) { + gameHandler.cancelCraftQueue(); + gameHandler.cancelCast(); + } + } else { + // Spell selector + quantity input + // Build list of known (craftable) spells + std::vector craftable; + for (const auto& spell : trainer.spells) { + if (isKnown(spell.spellId)) { + craftable.push_back(&spell); + } + } + if (!craftable.empty()) { + // Combo box for recipe selection + const char* previewName = "Select recipe..."; + for (const auto* sp : craftable) { + if (sp->spellId == selectedCraftSpell) { + const std::string& n = gameHandler.getSpellName(sp->spellId); + if (!n.empty()) previewName = n.c_str(); + break; + } + } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.55f); + if (ImGui::BeginCombo("##CraftSelect", previewName)) { + for (const auto* sp : craftable) { + const std::string& n = gameHandler.getSpellName(sp->spellId); + const std::string& r = gameHandler.getSpellRank(sp->spellId); + char label[128]; + if (!r.empty()) + snprintf(label, sizeof(label), "%s (%s)##%u", + n.empty() ? "???" : n.c_str(), r.c_str(), sp->spellId); + else + snprintf(label, sizeof(label), "%s##%u", + n.empty() ? "???" : n.c_str(), sp->spellId); + if (ImGui::Selectable(label, sp->spellId == selectedCraftSpell)) { + selectedCraftSpell = sp->spellId; + } + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(50.0f); + ImGui::InputInt("##CraftQty", &craftQuantity, 0, 0); + if (craftQuantity < 1) craftQuantity = 1; + if (craftQuantity > 99) craftQuantity = 99; + ImGui::SameLine(); + bool canCraft = selectedCraftSpell != 0 && !gameHandler.isCasting(); + if (!canCraft) ImGui::BeginDisabled(); + if (ImGui::Button("Create")) { + if (craftQuantity == 1) { + gameHandler.castSpell(selectedCraftSpell, 0); + } else { + gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity); + } + } + if (!canCraft) ImGui::EndDisabled(); + } + } + } } } ImGui::End(); @@ -14894,6 +15109,16 @@ void GameScreen::renderSettingsWindow() { ImGui::Separator(); ImGui::Spacing(); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::SliderInt("Brightness", &pendingBrightness, 0, 100, "%d%%")) { + if (renderer) renderer->setBrightness(static_cast(pendingBrightness) / 50.0f); + saveSettings(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) { pendingFullscreen = kDefaultFullscreen; pendingVsync = kDefaultVsync; @@ -14906,9 +15131,11 @@ void GameScreen::renderSettingsWindow() { pendingPOM = true; pendingPOMQuality = 1; pendingResIndex = defaultResIndex; + pendingBrightness = 50; window->setFullscreen(pendingFullscreen); window->setVsync(pendingVsync); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); + if (renderer) renderer->setBrightness(1.0f); pendingWaterRefraction = false; if (renderer) { renderer->setShadowsEnabled(pendingShadows); @@ -15320,6 +15547,10 @@ void GameScreen::renderSettingsWindow() { inventoryScreen.setSeparateBags(pendingSeparateBags); saveSettings(); } + if (ImGui::Checkbox("Show Key Ring", &pendingShowKeyring)) { + inventoryScreen.setShowKeyring(pendingShowKeyring); + saveSettings(); + } ImGui::Spacing(); ImGui::Separator(); @@ -15335,6 +15566,8 @@ void GameScreen::renderSettingsWindow() { pendingMinimapNpcDots = false; pendingSeparateBags = true; inventoryScreen.setSeparateBags(true); + pendingShowKeyring = true; + inventoryScreen.setShowKeyring(true); uiOpacity_ = 0.65f; minimapRotate_ = false; minimapSquare_ = false; @@ -17238,6 +17471,7 @@ void GameScreen::saveSettings() { out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n"; out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; + out << "show_keyring=" << (pendingShowKeyring ? 1 : 0) << "\n"; out << "action_bar_scale=" << pendingActionBarScale << "\n"; out << "nameplate_scale=" << nameplateScale_ << "\n"; out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n"; @@ -17271,6 +17505,7 @@ void GameScreen::saveSettings() { out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; out << "shadows=" << (pendingShadows ? 1 : 0) << "\n"; out << "shadow_distance=" << pendingShadowDistance << "\n"; + out << "brightness=" << pendingBrightness << "\n"; out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n"; out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n"; @@ -17359,6 +17594,9 @@ void GameScreen::loadSettings() { } else if (key == "separate_bags") { pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(pendingSeparateBags); + } else if (key == "show_keyring") { + pendingShowKeyring = (std::stoi(val) != 0); + inventoryScreen.setShowKeyring(pendingShowKeyring); } else if (key == "action_bar_scale") { pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f); } else if (key == "nameplate_scale") { @@ -17412,6 +17650,11 @@ void GameScreen::loadSettings() { else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150); else if (key == "shadows") pendingShadows = (std::stoi(val) != 0); else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f); + else if (key == "brightness") { + pendingBrightness = std::clamp(std::stoi(val), 0, 100); + if (auto* r = core::Application::getInstance().getRenderer()) + r->setBrightness(static_cast(pendingBrightness) / 50.0f); + } else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0); else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0); @@ -20372,24 +20615,54 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src); color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f); break; - case T::ENVIRONMENTAL: - snprintf(desc, sizeof(desc), "Environmental damage: %d", e.amount); + case T::ENVIRONMENTAL: { + const char* envName = "Environmental"; + switch (e.powerType) { + case 0: envName = "Fatigue"; break; + case 1: envName = "Drowning"; break; + case 2: envName = "Falling"; break; + case 3: envName = "Lava"; break; + case 4: envName = "Slime"; break; + case 5: envName = "Fire"; break; + } + snprintf(desc, sizeof(desc), "%s damage: %d", envName, e.amount); color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f); break; - case T::ENERGIZE: + } + case T::ENERGIZE: { + const char* pwrName = "power"; + switch (e.powerType) { + case 0: pwrName = "Mana"; break; + case 1: pwrName = "Rage"; break; + case 2: pwrName = "Focus"; break; + case 3: pwrName = "Energy"; break; + case 4: pwrName = "Happiness"; break; + case 6: pwrName = "Runic Power"; break; + } if (spell) - snprintf(desc, sizeof(desc), "%s gains %d power (%s)", tgt, e.amount, spell); + snprintf(desc, sizeof(desc), "%s gains %d %s (%s)", tgt, e.amount, pwrName, spell); else - snprintf(desc, sizeof(desc), "%s gains %d power", tgt, e.amount); + snprintf(desc, sizeof(desc), "%s gains %d %s", tgt, e.amount, pwrName); color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; - case T::POWER_DRAIN: + } + case T::POWER_DRAIN: { + const char* drainName = "power"; + switch (e.powerType) { + case 0: drainName = "Mana"; break; + case 1: drainName = "Rage"; break; + case 2: drainName = "Focus"; break; + case 3: drainName = "Energy"; break; + case 4: drainName = "Happiness"; break; + case 6: drainName = "Runic Power"; break; + } if (spell) - snprintf(desc, sizeof(desc), "%s loses %d power to %s's %s", tgt, e.amount, src, spell); + snprintf(desc, sizeof(desc), "%s loses %d %s to %s's %s", tgt, e.amount, drainName, src, spell); else - snprintf(desc, sizeof(desc), "%s loses %d power", tgt, e.amount); + snprintf(desc, sizeof(desc), "%s loses %d %s", tgt, e.amount, drainName); color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f); break; + } case T::XP_GAIN: snprintf(desc, sizeof(desc), "You gain %d experience", e.amount); color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 3d4b0c17..09022ec5 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -903,10 +903,10 @@ void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t m float posX = screenW - windowW - 10.0f; float posY = screenH - windowH - 60.0f; - ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; if (!ImGui::Begin("Bags", &open, flags)) { ImGui::End(); return; @@ -1030,8 +1030,7 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, float windowW = std::max(gridW, titleW); float windowH = contentH + 40.0f; // title bar + padding - // Keep separate bag windows anchored to the bag-bar stack. - ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; @@ -1069,20 +1068,29 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, ImGui::PopID(); } - if (bagIndex < 0) { - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); - for (int i = 0; i < inventory.getKeyringSize(); ++i) { - if (i % columns != 0) ImGui::SameLine(); - const auto& slot = inventory.getKeyringSlot(i); - char id[32]; - snprintf(id, sizeof(id), "##skr_%d", i); - ImGui::PushID(id); - // Keyring is display-only for now. - renderItemSlot(inventory, slot, slotSize, nullptr, - SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); - ImGui::PopID(); + if (bagIndex < 0 && showKeyring_) { + constexpr float keySlotSize = 24.0f; + constexpr int keyCols = 8; + // Only show rows that contain items (round up to full row) + int lastOccupied = -1; + for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) { + if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; } + } + int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols; + if (visibleSlots > 0) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); + for (int i = 0; i < visibleSlots; ++i) { + if (i % keyCols != 0) ImGui::SameLine(); + const auto& slot = inventory.getKeyringSlot(i); + char id[32]; + snprintf(id, sizeof(id), "##skr_%d", i); + ImGui::PushID(id); + renderItemSlot(inventory, slot, keySlotSize, nullptr, + SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); + ImGui::PopID(); + } } } @@ -2042,27 +2050,28 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool colla } } - bool keyringHasAnyItems = false; - for (int i = 0; i < inventory.getKeyringSize(); ++i) { - if (!inventory.getKeyringSlot(i).empty()) { - keyringHasAnyItems = true; - break; + if (showKeyring_) { + constexpr float keySlotSize = 24.0f; + constexpr int keyCols = 8; + int lastOccupied = -1; + for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) { + if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; } } - } - if (!collapseEmptySections || keyringHasAnyItems) { - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); - for (int i = 0; i < inventory.getKeyringSize(); ++i) { - if (i % columns != 0) ImGui::SameLine(); - const auto& slot = inventory.getKeyringSlot(i); - char sid[32]; - snprintf(sid, sizeof(sid), "##keyring_%d", i); - ImGui::PushID(sid); - // Keyring is display-only for now. - renderItemSlot(inventory, slot, slotSize, nullptr, - SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); - ImGui::PopID(); + int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols; + if (visibleSlots > 0) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); + for (int i = 0; i < visibleSlots; ++i) { + if (i % keyCols != 0) ImGui::SameLine(); + const auto& slot = inventory.getKeyringSlot(i); + char sid[32]; + snprintf(sid, sizeof(sid), "##keyring_%d", i); + ImGui::PushID(sid); + renderItemSlot(inventory, slot, keySlotSize, nullptr, + SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); + ImGui::PopID(); + } } } } @@ -2952,17 +2961,26 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I // Find a label for this stat type const char* lbl = nullptr; switch (t) { - case 31: lbl = "Hit"; break; - case 32: lbl = "Crit"; break; + case 0: lbl = "Mana"; break; + case 1: lbl = "Health"; break; + case 12: lbl = "Defense"; break; + case 13: lbl = "Dodge"; break; + case 14: lbl = "Parry"; break; + case 15: lbl = "Block Rating"; break; + case 16: case 17: case 18: case 31: lbl = "Hit"; break; + case 19: case 20: case 21: case 32: lbl = "Crit"; break; + case 28: case 29: case 30: case 36: lbl = "Haste"; break; case 35: lbl = "Resilience"; break; - case 36: lbl = "Haste"; break; case 37: lbl = "Expertise"; break; case 38: lbl = "Attack Power"; break; case 39: lbl = "Ranged AP"; break; + case 41: lbl = "Healing"; break; + case 42: lbl = "Spell Damage"; break; case 43: lbl = "MP5"; break; case 44: lbl = "Armor Pen"; break; case 45: lbl = "Spell Power"; break; case 46: lbl = "HP5"; break; + case 47: lbl = "Spell Pen"; break; case 48: lbl = "Block Value"; break; default: lbl = nullptr; break; } @@ -2970,6 +2988,10 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I showDiff(lbl, static_cast(nv), static_cast(ev)); } } + } else if (inventory && !ImGui::GetIO().KeyShift && item.inventoryType > 0) { + if (findComparableEquipped(*inventory, item.inventoryType)) { + ImGui::TextDisabled("Hold Shift to compare"); + } } // Destroy hint (not shown for quest items) @@ -3502,6 +3524,16 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, ImGui::TextColored(ic, "%s", ilvlBuf); } + // DPS comparison for weapons + if (isWeaponInvType(info.inventoryType) && isWeaponInvType(eq->item.inventoryType)) { + float newDps = 0.0f, eqDps = 0.0f; + if (info.damageMax > 0.0f && info.delayMs > 0) + newDps = ((info.damageMin + info.damageMax) * 0.5f) / (info.delayMs / 1000.0f); + if (eq->item.damageMax > 0.0f && eq->item.delayMs > 0) + eqDps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / (eq->item.delayMs / 1000.0f); + showDiff("DPS", newDps, eqDps); + } + showDiff("Armor", static_cast(info.armor), static_cast(eq->item.armor)); showDiff("Str", static_cast(info.strength), static_cast(eq->item.strength)); showDiff("Agi", static_cast(info.agility), static_cast(eq->item.agility)); @@ -3509,8 +3541,50 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, showDiff("Int", static_cast(info.intellect), static_cast(eq->item.intellect)); showDiff("Spi", static_cast(info.spirit), static_cast(eq->item.spirit)); - // Hint text - ImGui::TextDisabled("Hold Shift to compare"); + // Extra stats diff — union of stat types from both items + auto findExtraStat = [](const auto& it, uint32_t type) -> int32_t { + for (const auto& es : it.extraStats) + if (es.statType == type) return es.statValue; + return 0; + }; + std::vector allTypes; + for (const auto& es : info.extraStats) allTypes.push_back(es.statType); + for (const auto& es : eq->item.extraStats) { + bool found = false; + for (uint32_t t : allTypes) if (t == es.statType) { found = true; break; } + if (!found) allTypes.push_back(es.statType); + } + for (uint32_t t : allTypes) { + int32_t nv = findExtraStat(info, t); + int32_t ev = findExtraStat(eq->item, t); + const char* lbl = nullptr; + switch (t) { + case 0: lbl = "Mana"; break; + case 1: lbl = "Health"; break; + case 12: lbl = "Defense"; break; + case 13: lbl = "Dodge"; break; + case 14: lbl = "Parry"; break; + case 15: lbl = "Block Rating"; break; + case 16: case 17: case 18: case 31: lbl = "Hit"; break; + case 19: case 20: case 21: case 32: lbl = "Crit"; break; + case 28: case 29: case 30: case 36: lbl = "Haste"; break; + case 35: lbl = "Resilience"; break; + case 37: lbl = "Expertise"; break; + case 38: lbl = "Attack Power"; break; + case 39: lbl = "Ranged AP"; break; + case 41: lbl = "Healing"; break; + case 42: lbl = "Spell Damage"; break; + case 43: lbl = "MP5"; break; + case 44: lbl = "Armor Pen"; break; + case 45: lbl = "Spell Power"; break; + case 46: lbl = "HP5"; break; + case 47: lbl = "Spell Pen"; break; + case 48: lbl = "Block Value"; break; + default: lbl = nullptr; break; + } + if (!lbl) continue; + showDiff(lbl, static_cast(nv), static_cast(ev)); + } } } else if (info.inventoryType > 0) { ImGui::TextDisabled("Hold Shift to compare"); diff --git a/src/ui/keybinding_manager.cpp b/src/ui/keybinding_manager.cpp index 20d562c5..b522e671 100644 --- a/src/ui/keybinding_manager.cpp +++ b/src/ui/keybinding_manager.cpp @@ -1,7 +1,7 @@ #include "ui/keybinding_manager.hpp" +#include "core/logger.hpp" #include #include -#include namespace wowee::ui { @@ -101,7 +101,7 @@ const char* KeybindingManager::getActionName(Action action) { void KeybindingManager::loadFromConfigFile(const std::string& filePath) { std::ifstream file(filePath); if (!file.is_open()) { - std::cerr << "[KeybindingManager] Failed to open config file: " << filePath << std::endl; + LOG_ERROR("KeybindingManager: Failed to open config file: ", filePath); return; } @@ -206,7 +206,7 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) { } file.close(); - std::cout << "[KeybindingManager] Loaded keybindings from " << filePath << std::endl; + LOG_INFO("KeybindingManager: Loaded keybindings from ", filePath); } void KeybindingManager::saveToConfigFile(const std::string& filePath) const { @@ -301,9 +301,9 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const { if (outFile.is_open()) { outFile << content; outFile.close(); - std::cout << "[KeybindingManager] Saved keybindings to " << filePath << std::endl; + LOG_INFO("KeybindingManager: Saved keybindings to ", filePath); } else { - std::cerr << "[KeybindingManager] Failed to write config file: " << filePath << std::endl; + LOG_ERROR("KeybindingManager: Failed to write config file: ", filePath); } } diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index bed817ab..c2b92eff 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -593,15 +593,27 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { if (!dbc || !dbc->isLoaded()) return; const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; + uint32_t fieldCount = dbc->getFieldCount(); + // Detect DBC/layout mismatch: Classic layout expects ~173 fields but we may + // load the WotLK base DBC (234 fields). Use WotLK field indices in that case. + uint32_t idField = 0, iconField = 133, tooltipField = 139; + if (spellL) { + uint32_t layoutIcon = (*spellL)["IconID"]; + if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) { + idField = (*spellL)["ID"]; + iconField = layoutIcon; + try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {} + } + } uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { - uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); + uint32_t spellId = dbc->getUInt32(i, idField); if (spellId == 0) continue; - uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133); + uint32_t iconId = dbc->getUInt32(i, iconField); spellIconIds[spellId] = iconId; - std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139); + std::string tooltip = dbc->getString(i, tooltipField); if (!tooltip.empty()) { spellTooltips[spellId] = tooltip; }