From 871da339422f651fe0504f51ecc9f9dbc5aa4fff Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 19 Feb 2026 03:31:49 -0800 Subject: [PATCH] Clean README mentions and finalize current gameplay/UI fixes --- README.md | 8 +-- include/game/game_handler.hpp | 6 ++ src/core/application.cpp | 10 ++++ src/game/game_handler.cpp | 89 +++++++++++++++++++++++----- src/rendering/character_renderer.cpp | 14 ++++- src/ui/game_screen.cpp | 51 +++++----------- 6 files changed, 121 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 182a89d6..14182302 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ A native C++ World of Warcraft client with a custom OpenGL renderer. [![Watch the video](https://img.youtube.com/vi/J4NXegzqWSQ/maxresdefault.jpg)](https://youtu.be/J4NXegzqWSQ) -Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**, including Turtle WoW (1.17). All three expansions are broadly functional with roughly even support. +Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three expansions are broadly functional with roughly even support. > **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction. ## Status & Direction (2026-02-18) -- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all broadly supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). Turtle WoW (1.17) is also supported. All three expansions are roughly on par — no single one is significantly more complete than the others. -- **Tested against**: AzerothCore, TrinityCore, Turtle WoW, and ChromieCraft. +- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all broadly supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par — no single one is significantly more complete than the others. +- **Tested against**: AzerothCore, TrinityCore, and ChromieCraft. - **Current focus**: protocol correctness across server variants, visual accuracy (M2/WMO edge cases, equipment textures), and multi-expansion coverage. - **Warden**: Full module execution via Unicorn Engine CPU emulation. Decrypts (RC4→RSA→zlib), parses and relocates the PE module, executes via x86 emulation with Windows API interception. Module cache at `~/.local/share/wowee/warden_cache/`. @@ -113,7 +113,7 @@ Data/ Notes: - `StormLib` is required to build/run the extractor (`asset_extract`), but the main client does not require StormLib at runtime. -- `extract_assets.sh` supports `classic`, `turtle`, `tbc`, `wotlk` targets. +- `extract_assets.sh` supports `classic`, `tbc`, `wotlk` targets. #### 2) Point wowee at the extracted data diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 652a38b1..9231ab69 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -419,6 +419,9 @@ public: void cancelAura(uint32_t spellId); const std::unordered_set& getKnownSpells() const { return knownSpells; } bool isCasting() const { return casting; } + bool isGameObjectInteractionCasting() const { + return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0; + } uint32_t getCurrentCastSpellId() const { return currentCastSpellId; } float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastTimeRemaining() const { return castTimeRemaining; } @@ -1372,6 +1375,7 @@ private: bool casting = false; uint32_t currentCastSpellId = 0; float castTimeRemaining = 0.0f; + uint64_t pendingGameObjectInteractGuid_ = 0; // Talents (dual-spec support) uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1) @@ -1431,6 +1435,8 @@ private: bool gossipWindowOpen = false; GossipMessageData currentGossip; + void performGameObjectInteractionNow(uint64_t guid); + // Quest details bool questDetailsOpen = false; QuestDetailsData currentQuestDetails; diff --git a/src/core/application.cpp b/src/core/application.cpp index ad36e2d0..718a6259 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -5084,6 +5084,16 @@ void Application::updateQuestMarkers() { // Get NPC entity position auto entity = gameHandler->getEntityManager().getEntity(guid); if (!entity) continue; + if (entity->getType() == game::ObjectType::UNIT) { + auto unit = std::static_pointer_cast(entity); + std::string name = unit->getName(); + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c){ return static_cast(std::tolower(c)); }); + if (name.find("spirit healer") != std::string::npos || + name.find("spirit guide") != std::string::npos) { + continue; // Spirit healers/guides use their own white visual cue. + } + } glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); glm::vec3 renderPos = coords::canonicalToRender(canonical); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4b97e5f0..8aa48146 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -567,9 +567,22 @@ void GameHandler::update(float deltaTime) { } // Update cast timer (Phase 3) + if (pendingGameObjectInteractGuid_ != 0 && + (autoAttacking || !hostileAttackers_.empty())) { + pendingGameObjectInteractGuid_ = 0; + casting = false; + currentCastSpellId = 0; + castTimeRemaining = 0.0f; + addSystemChatMessage("Interrupted."); + } if (casting && castTimeRemaining > 0.0f) { castTimeRemaining -= deltaTime; if (castTimeRemaining <= 0.0f) { + if (pendingGameObjectInteractGuid_ != 0) { + uint64_t interactGuid = pendingGameObjectInteractGuid_; + pendingGameObjectInteractGuid_ = 0; + performGameObjectInteractionNow(interactGuid); + } casting = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; @@ -2636,6 +2649,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { autoAttackTarget = 0; casting = false; currentCastSpellId = 0; + pendingGameObjectInteractGuid_ = 0; castTimeRemaining = 0.0f; castTimeTotal = 0.0f; playerDead_ = false; @@ -6029,13 +6043,16 @@ void GameHandler::stopCasting() { return; // Not casting anything } - // Send cancel cast packet with current spell ID - auto packet = CancelCastPacket::build(currentCastSpellId); - socket->send(packet); + // Send cancel cast packet only for real spell casts. + if (pendingGameObjectInteractGuid_ == 0 && currentCastSpellId != 0) { + auto packet = CancelCastPacket::build(currentCastSpellId); + socket->send(packet); + } // Reset casting state casting = false; currentCastSpellId = 0; + pendingGameObjectInteractGuid_ = 0; castTimeRemaining = 0.0f; castTimeTotal = 0.0f; @@ -7872,10 +7889,14 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { void GameHandler::cancelCast() { if (!casting) return; - if (state == WorldState::IN_WORLD && socket) { + // GameObject interaction cast is client-side timing only. + if (pendingGameObjectInteractGuid_ == 0 && + state == WorldState::IN_WORLD && socket && + currentCastSpellId != 0) { auto packet = CancelCastPacket::build(currentCastSpellId); socket->send(packet); } + pendingGameObjectInteractGuid_ = 0; casting = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; @@ -8545,6 +8566,21 @@ void GameHandler::interactWithNpc(uint64_t guid) { } void GameHandler::interactWithGameObject(uint64_t guid) { + if (guid == 0) return; + if (state != WorldState::IN_WORLD || !socket) return; + if (casting && currentCastSpellId != 0) return; // don't overlap spell cast bar + if (autoAttacking) { + stopAutoAttack(); + } + pendingGameObjectInteractGuid_ = guid; + casting = true; + currentCastSpellId = 0; + castTimeTotal = 1.5f; + castTimeRemaining = castTimeTotal; +} + +void GameHandler::performGameObjectInteractionNow(uint64_t guid) { + if (guid == 0) return; if (state != WorldState::IN_WORLD || !socket) return; bool turtleMode = isActiveExpansion("turtle"); @@ -8564,10 +8600,6 @@ void GameHandler::interactWithGameObject(uint64_t guid) { if (autoAttacking) { stopAutoAttack(); } - if (targetGuid != guid) { - setTarget(guid); - } - auto entity = entityManager.getEntity(guid); auto packet = GameObjectUsePacket::build(guid); @@ -9258,6 +9290,16 @@ void GameHandler::handleGossipMessage(network::Packet& packet) { bool hasAvailableQuest = false; bool hasRewardQuest = false; bool hasIncompleteQuest = false; + auto questIconIsCompletable = [](uint32_t icon) { + return icon == 5 || icon == 6 || icon == 10; + }; + auto questIconIsIncomplete = [](uint32_t icon) { + return icon == 3 || icon == 4; + }; + auto questIconIsAvailable = [](uint32_t icon) { + return icon == 2 || icon == 7 || icon == 8; + }; + for (const auto& questItem : currentGossip.quests) { // WotLK gossip questIcon is an integer enum, NOT a bitmask: // 2 = yellow ! (available, not yet accepted) @@ -9265,9 +9307,9 @@ void GameHandler::handleGossipMessage(network::Packet& packet) { // 5 = gold ? (complete, ready to turn in) // Bit-masking these values is wrong: 4 & 0x04 = true, treating incomplete // quests as completable and causing the server to reject the turn-in request. - bool isCompletable = (questItem.questIcon == 5); // Gold ? = can turn in - bool isIncomplete = (questItem.questIcon == 4); // Gray ? = in progress - bool isAvailable = (questItem.questIcon == 2); // Yellow ! = available + bool isCompletable = questIconIsCompletable(questItem.questIcon); + bool isIncomplete = questIconIsIncomplete(questItem.questIcon); + bool isAvailable = questIconIsAvailable(questItem.questIcon); hasAvailableQuest |= isAvailable; hasRewardQuest |= isCompletable; @@ -9290,7 +9332,9 @@ void GameHandler::handleGossipMessage(network::Packet& packet) { if (hasRewardQuest) derivedStatus = QuestGiverStatus::REWARD; else if (hasAvailableQuest) derivedStatus = QuestGiverStatus::AVAILABLE; else if (hasIncompleteQuest) derivedStatus = QuestGiverStatus::INCOMPLETE; - npcQuestStatus_[currentGossip.npcGuid] = derivedStatus; + if (derivedStatus != QuestGiverStatus::NONE) { + npcQuestStatus_[currentGossip.npcGuid] = derivedStatus; + } } // Play NPC greeting voice @@ -9371,10 +9415,20 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { bool hasAvailableQuest = false; bool hasRewardQuest = false; bool hasIncompleteQuest = false; + auto questIconIsCompletable = [](uint32_t icon) { + return icon == 5 || icon == 6 || icon == 10; + }; + auto questIconIsIncomplete = [](uint32_t icon) { + return icon == 3 || icon == 4; + }; + auto questIconIsAvailable = [](uint32_t icon) { + return icon == 2 || icon == 7 || icon == 8; + }; + for (const auto& questItem : currentGossip.quests) { - bool isCompletable = (questItem.questIcon == 5 || questItem.questIcon == 10); - bool isIncomplete = (questItem.questIcon == 3 || questItem.questIcon == 4); - bool isAvailable = (questItem.questIcon == 2 || questItem.questIcon == 7 || questItem.questIcon == 8); + bool isCompletable = questIconIsCompletable(questItem.questIcon); + bool isIncomplete = questIconIsIncomplete(questItem.questIcon); + bool isAvailable = questIconIsAvailable(questItem.questIcon); hasAvailableQuest |= isAvailable; hasRewardQuest |= isCompletable; hasIncompleteQuest |= isIncomplete; @@ -9384,7 +9438,9 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { if (hasRewardQuest) derivedStatus = QuestGiverStatus::REWARD; else if (hasAvailableQuest) derivedStatus = QuestGiverStatus::AVAILABLE; else if (hasIncompleteQuest) derivedStatus = QuestGiverStatus::INCOMPLETE; - npcQuestStatus_[currentGossip.npcGuid] = derivedStatus; + if (derivedStatus != QuestGiverStatus::NONE) { + npcQuestStatus_[currentGossip.npcGuid] = derivedStatus; + } } LOG_INFO("Questgiver quest list: npc=0x", std::hex, currentGossip.npcGuid, std::dec, @@ -9990,6 +10046,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) { stopAutoAttack(); casting = false; currentCastSpellId = 0; + pendingGameObjectInteractGuid_ = 0; castTimeRemaining = 0.0f; // Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index cdb22ed8..61b68526 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1649,7 +1649,19 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons characterShader->setUniform("uUnlit", unlit ? 1 : 0); float emissiveBoost = 1.0f; glm::vec3 emissiveTint(1.0f, 1.0f, 1.0f); - if (unlit) { + // Keep custom warm/flicker treatment narrowly scoped to kobold candle flames. + bool koboldCandleFlame = false; + if (colorKeyBlack) { + std::string modelKey = gpuModel.data.name; + std::transform(modelKey.begin(), modelKey.end(), modelKey.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + koboldCandleFlame = + (modelKey.find("kobold") != std::string::npos) && + ((modelKey.find("candle") != std::string::npos) || + (modelKey.find("torch") != std::string::npos) || + (modelKey.find("mine") != std::string::npos)); + } + if (unlit && koboldCandleFlame) { using clock = std::chrono::steady_clock; float t = std::chrono::duration(clock::now().time_since_epoch()).count(); float phase = static_cast(batch.submeshId) * 0.31f; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 312c0180..205220a3 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4826,48 +4826,27 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) { } } - // Render item with icon + // Render item with icon + visible selectable label ImGui::PushID(static_cast(i)); - if (ImGui::Selectable("##reward", selected, 0, ImVec2(0, 40))) { + std::string label; + if (info && info->valid && !info->name.empty()) { + label = info->name; + } else { + label = "Item " + std::to_string(item.itemId); + } + if (item.count > 1) { + label += " x" + std::to_string(item.count); + } + if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 24))) { selectedChoice = static_cast(i); } - - // Draw icon and text over the selectable - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetItemRectSize().x + 4); - + if (ImGui::IsItemHovered() && iconTex) { + ImGui::SetTooltip("Reward option"); + } if (iconTex) { - ImGui::Image((void*)(intptr_t)iconTex, ImVec2(36, 36)); ImGui::SameLine(); + ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18)); } - - ImGui::BeginGroup(); - if (info && info->valid) { - ImGui::TextColored(qualityColor, "%s", info->name.c_str()); - if (item.count > 1) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "x%u", item.count); - } - // Show stats - if (info->armor > 0 || info->stamina > 0 || info->strength > 0 || - info->agility > 0 || info->intellect > 0 || info->spirit > 0) { - std::string stats; - if (info->armor > 0) stats += std::to_string(info->armor) + " Armor "; - if (info->stamina > 0) stats += "+" + std::to_string(info->stamina) + " Sta "; - if (info->strength > 0) stats += "+" + std::to_string(info->strength) + " Str "; - if (info->agility > 0) stats += "+" + std::to_string(info->agility) + " Agi "; - if (info->intellect > 0) stats += "+" + std::to_string(info->intellect) + " Int "; - if (info->spirit > 0) stats += "+" + std::to_string(info->spirit) + " Spi "; - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "%s", stats.c_str()); - } - } else { - ImGui::TextColored(qualityColor, "Item %u", item.itemId); - if (item.count > 0) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "x%u", item.count); - } - } - ImGui::EndGroup(); ImGui::PopID(); } }