Clean README mentions and finalize current gameplay/UI fixes

This commit is contained in:
Kelsi 2026-02-19 03:31:49 -08:00
parent 16f8b0177e
commit 871da33942
6 changed files with 121 additions and 57 deletions

View file

@ -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

View file

@ -419,6 +419,9 @@ public:
void cancelAura(uint32_t spellId);
const std::unordered_set<uint32_t>& 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;

View file

@ -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<game::Unit>(entity);
std::string name = unit->getName();
std::transform(name.begin(), name.end(), name.begin(),
[](unsigned char c){ return static_cast<char>(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);

View file

@ -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

View file

@ -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<char>(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<float>(clock::now().time_since_epoch()).count();
float phase = static_cast<float>(batch.submeshId) * 0.31f;

View file

@ -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<int>(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<int>(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();
}
}