diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 2d0c1b2e..46fdabcb 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -148,6 +148,15 @@ public: return GossipMessageParser::parse(packet, data); } + // --- Quest Giver Status --- + + /** Read quest giver status from packet. + * WotLK: uint8, vanilla/classic: uint32 with different enum values. + * Returns the status value normalized to WotLK enum values. */ + virtual uint8_t readQuestGiverStatus(network::Packet& packet) { + return packet.readUInt8(); + } + // --- Destroy Object --- /** Parse SMSG_DESTROY_OBJECT */ @@ -294,13 +303,31 @@ public: network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override; network::Packet buildItemQuery(uint32_t entry, uint64_t guid) override; bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override; + uint8_t readQuestGiverStatus(network::Packet& packet) override; +}; + +/** + * Turtle WoW (build 7234) packet parsers. + * + * Turtle WoW is a heavily modified vanilla server that sends TBC-style + * movement blocks (moveFlags2, transport timestamps, 8 speeds including flight) + * while keeping all other Classic packet formats. + * + * Inherits all Classic overrides (charEnum, chat, gossip, mail, items, etc.) + * but delegates movement block parsing to TBC format. + */ +class TurtlePacketParsers : public ClassicPacketParsers { +public: + uint8_t movementFlags2Size() const override { return 0; } + bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override; }; /** * Factory function to create the right parser set for an expansion. */ inline std::unique_ptr createPacketParsers(const std::string& expansionId) { - if (expansionId == "classic" || expansionId == "turtle") return std::make_unique(); + if (expansionId == "classic") return std::make_unique(); + if (expansionId == "turtle") return std::make_unique(); if (expansionId == "tbc") return std::make_unique(); return std::make_unique(); } diff --git a/include/game/zone_manager.hpp b/include/game/zone_manager.hpp index 06f43baf..0b02820e 100644 --- a/include/game/zone_manager.hpp +++ b/include/game/zone_manager.hpp @@ -20,13 +20,14 @@ public: uint32_t getZoneId(int tileX, int tileY) const; const ZoneInfo* getZoneInfo(uint32_t zoneId) const; - std::string getRandomMusic(uint32_t zoneId) const; + std::string getRandomMusic(uint32_t zoneId); std::vector getAllMusicPaths() const; private: // tile key = tileX * 100 + tileY std::unordered_map tileToZone; std::unordered_map zones; + std::string lastPlayedMusic_; }; } // namespace game diff --git a/src/audio/music_manager.cpp b/src/audio/music_manager.cpp index 27f45ec1..93f55132 100644 --- a/src/audio/music_manager.cpp +++ b/src/audio/music_manager.cpp @@ -158,7 +158,7 @@ void MusicManager::crossfadeTo(const std::string& mpqPath, float fadeMs) { fadeDuration = fadeMs / 1000.0f; AudioEngine::instance().stopMusic(); } else { - playMusic(mpqPath); + playMusic(mpqPath, false); } } @@ -173,7 +173,7 @@ void MusicManager::crossfadeToFile(const std::string& filePath, float fadeMs) { fadeDuration = fadeMs / 1000.0f; AudioEngine::instance().stopMusic(); } else { - playFilePath(filePath); + playFilePath(filePath, false); } } @@ -190,9 +190,9 @@ void MusicManager::update(float deltaTime) { // Start new track after brief pause crossfading = false; if (pendingIsFile) { - playFilePath(pendingTrack); + playFilePath(pendingTrack, false); } else { - playMusic(pendingTrack); + playMusic(pendingTrack, false); } pendingTrack.clear(); pendingIsFile = false; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6bb7f0ca..53144e65 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1213,23 +1213,21 @@ void GameHandler::handlePacket(network::Packet& packet) { handleGameObjectQueryResponse(packet); break; case Opcode::SMSG_QUESTGIVER_STATUS: { - // uint64 npcGuid + uint8 status if (packet.getSize() - packet.getReadPos() >= 9) { uint64_t npcGuid = packet.readUInt64(); - uint8_t status = packet.readUInt8(); + uint8_t status = packetParsers_->readQuestGiverStatus(packet); npcQuestStatus_[npcGuid] = static_cast(status); LOG_DEBUG("SMSG_QUESTGIVER_STATUS: guid=0x", std::hex, npcGuid, std::dec, " status=", (int)status); } break; } case Opcode::SMSG_QUESTGIVER_STATUS_MULTIPLE: { - // uint32 count, then count * (uint64 guid + uint8 status) if (packet.getSize() - packet.getReadPos() >= 4) { uint32_t count = packet.readUInt32(); for (uint32_t i = 0; i < count; ++i) { if (packet.getSize() - packet.getReadPos() < 9) break; uint64_t npcGuid = packet.readUInt64(); - uint8_t status = packet.readUInt8(); + uint8_t status = packetParsers_->readQuestGiverStatus(packet); npcQuestStatus_[npcGuid] = static_cast(status); } LOG_DEBUG("SMSG_QUESTGIVER_STATUS_MULTIPLE: ", count, " entries"); @@ -5416,8 +5414,13 @@ void GameHandler::addLocalChatMessage(const MessageChatData& msg) { void GameHandler::queryPlayerName(uint64_t guid) { if (playerNameCache.count(guid) || pendingNameQueries.count(guid)) return; - if (state != WorldState::IN_WORLD || !socket) return; + if (state != WorldState::IN_WORLD || !socket) { + LOG_INFO("queryPlayerName: skipped guid=0x", std::hex, guid, std::dec, + " state=", worldStateName(state), " socket=", (socket ? "yes" : "no")); + return; + } + LOG_INFO("queryPlayerName: sending CMSG_NAME_QUERY for guid=0x", std::hex, guid, std::dec); pendingNameQueries.insert(guid); auto packet = NameQueryPacket::build(guid); socket->send(packet); @@ -5460,6 +5463,10 @@ void GameHandler::handleNameQueryResponse(network::Packet& packet) { pendingNameQueries.erase(data.guid); + LOG_INFO("Name query response: guid=0x", std::hex, data.guid, std::dec, + " found=", (int)data.found, " name='", data.name, "'", + " race=", (int)data.race, " class=", (int)data.classId); + if (data.isValid()) { playerNameCache[data.guid] = data.name; // Update entity name diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 8ac5865a..8b75982f 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -316,8 +316,12 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& data) { data.castCount = 0; data.spellId = packet.readUInt32(); - data.result = packet.readUInt8(); - LOG_INFO("[Classic] Cast failed: spell=", data.spellId, " result=", (int)data.result); + uint8_t vanillaResult = packet.readUInt8(); + // Vanilla enum starts at 0=AFFECTING_COMBAT (no SUCCESS entry). + // WotLK enum starts at 0=SUCCESS, 1=AFFECTING_COMBAT. + // Shift +1 to align with WotLK result strings. + data.result = vanillaResult + 1; + LOG_DEBUG("[Classic] Cast failed: spell=", data.spellId, " vanillaResult=", (int)vanillaResult); return true; } @@ -945,5 +949,210 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ return true; } +// ============================================================================ +// Turtle WoW (build 7234) parseMovementBlock +// +// Turtle WoW is a heavily modified vanilla (1.12.1) server. Through hex dump +// analysis the wire format is nearly identical to Classic with one key addition: +// +// LIVING section: +// moveFlags u32 (NO moveFlags2 — confirmed by position alignment) +// time u32 +// position 4×float +// transport guarded by moveFlags & 0x02000000 (Classic flag) +// packed GUID + 4 floats + u32 timestamp (TBC-style addition) +// pitch guarded by SWIMMING (0x200000) +// fallTime u32 +// jump data guarded by JUMPING (0x2000) +// splineElev guarded by 0x04000000 +// speeds 6 floats (walk/run/runBack/swim/swimBack/turnRate) +// spline guarded by 0x00400000 (Classic flag) OR 0x08000000 (TBC flag) +// +// Tail (same as Classic): +// LOWGUID → 1×u32 +// HIGHGUID → 1×u32 +// +// The ONLY confirmed difference from pure Classic is: +// Transport data includes a u32 timestamp after the 4 transport floats +// (Classic omits this; TBC/WotLK include it). Without this, entities on +// transports cause a 4-byte desync that cascades to later blocks. +// ============================================================================ +namespace TurtleMoveFlags { + constexpr uint32_t ONTRANSPORT = 0x02000000; // Classic transport flag + constexpr uint32_t JUMPING = 0x00002000; + constexpr uint32_t SWIMMING = 0x00200000; + constexpr uint32_t SPLINE_ELEVATION = 0x04000000; + constexpr uint32_t SPLINE_CLASSIC = 0x00400000; // Classic spline enabled + constexpr uint32_t SPLINE_TBC = 0x08000000; // TBC spline enabled +} + +bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { + uint8_t updateFlags = packet.readUInt8(); + block.updateFlags = static_cast(updateFlags); + + LOG_DEBUG(" [Turtle] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec); + + const uint8_t UPDATEFLAG_LIVING = 0x20; + const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; + const uint8_t UPDATEFLAG_HAS_TARGET = 0x04; + const uint8_t UPDATEFLAG_TRANSPORT = 0x02; + const uint8_t UPDATEFLAG_LOWGUID = 0x08; + const uint8_t UPDATEFLAG_HIGHGUID = 0x10; + + if (updateFlags & UPDATEFLAG_LIVING) { + size_t livingStart = packet.getReadPos(); + + uint32_t moveFlags = packet.readUInt32(); + // Turtle: NO moveFlags2 (confirmed by hex dump — positions are only correct without it) + /*uint32_t time =*/ packet.readUInt32(); + + // Position + block.x = packet.readFloat(); + block.y = packet.readFloat(); + block.z = packet.readFloat(); + block.orientation = packet.readFloat(); + block.hasMovement = true; + + LOG_DEBUG(" [Turtle] LIVING: (", block.x, ", ", block.y, ", ", block.z, + "), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec); + + // Transport — Classic flag position 0x02000000 + if (moveFlags & TurtleMoveFlags::ONTRANSPORT) { + block.onTransport = true; + block.transportGuid = UpdateObjectParser::readPackedGuid(packet); + block.transportX = packet.readFloat(); + block.transportY = packet.readFloat(); + block.transportZ = packet.readFloat(); + block.transportO = packet.readFloat(); + /*uint32_t transportTime =*/ packet.readUInt32(); // Turtle adds TBC-style timestamp + } + + // Pitch (swimming only, Classic-style) + if (moveFlags & TurtleMoveFlags::SWIMMING) { + /*float pitch =*/ packet.readFloat(); + } + + // Fall time (always present) + /*uint32_t fallTime =*/ packet.readUInt32(); + + // Jump data + if (moveFlags & TurtleMoveFlags::JUMPING) { + /*float jumpVelocity =*/ packet.readFloat(); + /*float jumpSinAngle =*/ packet.readFloat(); + /*float jumpCosAngle =*/ packet.readFloat(); + /*float jumpXYSpeed =*/ packet.readFloat(); + } + + // Spline elevation + if (moveFlags & TurtleMoveFlags::SPLINE_ELEVATION) { + /*float splineElevation =*/ packet.readFloat(); + } + + // Turtle: 6 speeds (same as Classic — no flight speeds) + float walkSpeed = packet.readFloat(); + float runSpeed = packet.readFloat(); + float runBackSpeed = packet.readFloat(); + float swimSpeed = packet.readFloat(); + float swimBackSpeed = packet.readFloat(); + float turnRate = packet.readFloat(); + + block.runSpeed = runSpeed; + + LOG_DEBUG(" [Turtle] Speeds: walk=", walkSpeed, " run=", runSpeed, + " runBack=", runBackSpeed, " swim=", swimSpeed, + " swimBack=", swimBackSpeed, " turn=", turnRate); + + // Spline data — check both Classic (0x00400000) and TBC (0x08000000) flag positions + bool hasSpline = (moveFlags & TurtleMoveFlags::SPLINE_CLASSIC) || + (moveFlags & TurtleMoveFlags::SPLINE_TBC); + if (hasSpline) { + uint32_t splineFlags = packet.readUInt32(); + LOG_DEBUG(" [Turtle] Spline: flags=0x", std::hex, splineFlags, std::dec); + + if (splineFlags & 0x00010000) { + packet.readFloat(); packet.readFloat(); packet.readFloat(); + } else if (splineFlags & 0x00020000) { + packet.readUInt64(); + } else if (splineFlags & 0x00040000) { + packet.readFloat(); + } + + /*uint32_t timePassed =*/ packet.readUInt32(); + /*uint32_t duration =*/ packet.readUInt32(); + /*uint32_t splineId =*/ packet.readUInt32(); + + uint32_t pointCount = packet.readUInt32(); + if (pointCount > 256) { + LOG_WARNING(" [Turtle] Spline pointCount=", pointCount, " exceeds max, capping"); + pointCount = 0; + } + for (uint32_t i = 0; i < pointCount; i++) { + packet.readFloat(); packet.readFloat(); packet.readFloat(); + } + + // End point + packet.readFloat(); packet.readFloat(); packet.readFloat(); + } + + LOG_DEBUG(" [Turtle] LIVING block consumed ", packet.getReadPos() - livingStart, + " bytes, readPos now=", packet.getReadPos()); + } + else if (updateFlags & UPDATEFLAG_HAS_POSITION) { + block.x = packet.readFloat(); + block.y = packet.readFloat(); + block.z = packet.readFloat(); + block.orientation = packet.readFloat(); + block.hasMovement = true; + + LOG_DEBUG(" [Turtle] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); + } + + // Target GUID + if (updateFlags & UPDATEFLAG_HAS_TARGET) { + /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + } + + // Transport time + if (updateFlags & UPDATEFLAG_TRANSPORT) { + /*uint32_t transportTime =*/ packet.readUInt32(); + } + + // Low GUID — Classic-style: 1×u32 (NOT TBC's 2×u32) + if (updateFlags & UPDATEFLAG_LOWGUID) { + /*uint32_t lowGuid =*/ packet.readUInt32(); + } + + // High GUID — 1×u32 + if (updateFlags & UPDATEFLAG_HIGHGUID) { + /*uint32_t highGuid =*/ packet.readUInt32(); + } + + return true; +} + +// ============================================================================ +// Classic/Vanilla quest giver status +// +// Vanilla sends status as uint32 with different enum values: +// 0=NONE, 1=UNAVAILABLE, 2=CHAT, 3=INCOMPLETE, 4=REWARD_REP, 5=AVAILABLE +// WotLK uses uint8 with: +// 0=NONE, 1=UNAVAILABLE, 5=INCOMPLETE, 6=REWARD_REP, 7=AVAILABLE_LOW, 8=AVAILABLE, 10=REWARD +// +// Read uint32, translate to WotLK enum values. +// ============================================================================ +uint8_t ClassicPacketParsers::readQuestGiverStatus(network::Packet& packet) { + uint32_t vanillaStatus = packet.readUInt32(); + switch (vanillaStatus) { + 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 8; // AVAILABLE → WotLK AVAILABLE + case 6: return 10; // REWARD → WotLK REWARD + default: return 0; + } +} + } // namespace game } // namespace wowee diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 8916048b..4c31d73d 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -440,8 +440,9 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa } if (!ok) { - LOG_ERROR("Failed to parse update block ", i + 1); - return false; + LOG_WARNING("Failed to parse update block ", i + 1, " of ", data.blockCount, + " — keeping ", data.blocks.size(), " parsed blocks"); + break; } data.blocks.push_back(block); } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 3d29cc25..c5bb518b 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2802,16 +2802,26 @@ network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t qu network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST)); packet.writeUInt64(npcGuid); packet.writeUInt32(questId); - packet.writeUInt32(0); // unused return packet; } bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) { - if (packet.getSize() < 28) return false; + if (packet.getSize() < 20) return false; data.npcGuid = packet.readUInt64(); + + // WotLK has informUnit(u64) before questId; Vanilla/TBC do not. + // Detect: try WotLK first (read informUnit + questId), then check if title + // string looks valid. If not, rewind and try vanilla (questId directly). + size_t preInform = packet.getReadPos(); /*informUnit*/ packet.readUInt64(); data.questId = packet.readUInt32(); data.title = packet.readString(); + if (data.title.empty() || data.questId > 100000) { + // Likely vanilla format — rewind past informUnit + packet.setReadPos(preInform); + data.questId = packet.readUInt32(); + data.title = packet.readString(); + } data.details = packet.readString(); data.objectives = packet.readString(); diff --git a/src/game/zone_manager.cpp b/src/game/zone_manager.cpp index acb59329..4208f10a 100644 --- a/src/game/zone_manager.cpp +++ b/src/game/zone_manager.cpp @@ -430,14 +430,26 @@ const ZoneInfo* ZoneManager::getZoneInfo(uint32_t zoneId) const { return nullptr; } -std::string ZoneManager::getRandomMusic(uint32_t zoneId) const { +std::string ZoneManager::getRandomMusic(uint32_t zoneId) { auto it = zones.find(zoneId); if (it == zones.end() || it->second.musicPaths.empty()) { return ""; } const auto& paths = it->second.musicPaths; - return paths[std::rand() % paths.size()]; + if (paths.size() == 1) { + lastPlayedMusic_ = paths[0]; + return paths[0]; + } + + // Avoid playing the same track back-to-back + std::string pick; + for (int attempts = 0; attempts < 5; ++attempts) { + pick = paths[std::rand() % paths.size()]; + if (pick != lastPlayedMusic_) break; + } + lastPlayedMusic_ = pick; + return pick; } std::vector ZoneManager::getAllMusicPaths() const { diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 6d166ce3..73c655af 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2286,6 +2286,16 @@ void Renderer::update(float deltaTime) { } musicManager->update(deltaTime); + + // When a track finishes, pick a new random track from the current zone + if (!musicManager->isPlaying() && !inTavern_ && !inBlacksmith_ && + currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) { + std::string music = zoneManager->getRandomMusic(currentZoneId); + if (!music.empty()) { + playZoneMusic(music); + musicSwitchCooldown_ = 2.0f; + } + } } // Update performance HUD diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 2375adb7..d1f362d8 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1328,6 +1328,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc(); if (!unit->isHostile() && (unit->isInteractable() || allowSpiritInteract)) { gameHandler.interactWithNpc(target->getGuid()); + } else if (unit->isHostile()) { + gameHandler.startAutoAttack(target->getGuid()); } } } else if (target->getType() == game::ObjectType::GAMEOBJECT) { @@ -2967,16 +2969,29 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) { } } - // Load Spell.dbc: field 133 = SpellIconID + // Load Spell.dbc: SpellIconID field auto spellDbc = am->loadDBC("Spell.dbc"); const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; - if (spellDbc && spellDbc->isLoaded() && spellDbc->getFieldCount() > 133) { - for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) { - uint32_t id = spellDbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); - uint32_t iconId = spellDbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133); - if (id > 0 && iconId > 0) { - spellIconIds_[id] = iconId; + if (spellDbc && spellDbc->isLoaded()) { + uint32_t fieldCount = spellDbc->getFieldCount(); + // Try expansion layout first + auto tryLoadIcons = [&](uint32_t idField, uint32_t iconField) { + spellIconIds_.clear(); + if (iconField >= fieldCount) return; + for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) { + uint32_t id = spellDbc->getUInt32(i, idField); + uint32_t iconId = spellDbc->getUInt32(i, iconField); + if (id > 0 && iconId > 0) { + spellIconIds_[id] = iconId; + } } + }; + if (spellL) { + tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]); + } + // Fallback to WotLK field 133 if expansion layout yielded nothing + if (spellIconIds_.empty() && fieldCount > 133) { + tryLoadIcons(0, 133); } } } diff --git a/src/ui/realm_screen.cpp b/src/ui/realm_screen.cpp index e0afe664..0410174c 100644 --- a/src/ui/realm_screen.cpp +++ b/src/ui/realm_screen.cpp @@ -127,8 +127,8 @@ void RealmScreen::render(auth::AuthHandler& authHandler) { ImGui::TableSetColumnIndex(1); if (realm.icon == 0) ImGui::Text("Normal"); else if (realm.icon == 1) ImGui::Text("PvP"); - else if (realm.icon == 4) ImGui::Text("RP"); - else if (realm.icon == 6) ImGui::Text("RP-PvP"); + else if (realm.icon == 6) ImGui::Text("RP"); + else if (realm.icon == 8) ImGui::Text("RP-PvP"); else ImGui::Text("Type %d", realm.icon); // Population column @@ -136,8 +136,8 @@ void RealmScreen::render(auth::AuthHandler& authHandler) { ImVec4 popColor = getPopulationColor(realm.population); ImGui::PushStyleColor(ImGuiCol_Text, popColor); if (realm.population < 0.5f) ImGui::Text("Low"); - else if (realm.population < 1.0f) ImGui::Text("Medium"); - else if (realm.population < 2.0f) ImGui::Text("High"); + else if (realm.population < 1.5f) ImGui::Text("Medium"); + else if (realm.population < 2.5f) ImGui::Text("High"); else ImGui::Text("Full"); ImGui::PopStyleColor(); @@ -238,9 +238,9 @@ const char* RealmScreen::getRealmStatus(uint8_t flags) const { ImVec4 RealmScreen::getPopulationColor(float population) const { if (population < 0.5f) { return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - Low - } else if (population < 1.0f) { + } else if (population < 1.5f) { return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow - Medium - } else if (population < 2.0f) { + } else if (population < 2.5f) { return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange - High } else { return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red - Full diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index cf60bc2e..c1a7b8cd 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -29,28 +29,46 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { return; } - // WoW 3.3.5a Spell.dbc fields (0-based): - // 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 136 = SpellName_enUS, 153 = RankText_enUS + // Try expansion-specific layout first, then fall back to WotLK hardcoded indices + // if the DBC is from WotLK MPQs but the active expansion uses different field offsets. const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; - uint32_t count = dbc->getRecordCount(); - for (uint32_t i = 0; i < count; ++i) { - uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); - if (spellId == 0) continue; - SpellInfo info; - info.spellId = spellId; - info.attributes = dbc->getUInt32(i, spellL ? (*spellL)["Attributes"] : 4); - info.iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133); - info.name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136); - info.rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153); + auto tryLoad = [&](uint32_t idField, uint32_t attrField, uint32_t iconField, + uint32_t nameField, uint32_t rankField, const char* label) { + spellData.clear(); + uint32_t count = dbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t spellId = dbc->getUInt32(i, idField); + if (spellId == 0) continue; - if (!info.name.empty()) { - spellData[spellId] = std::move(info); + SpellInfo info; + info.spellId = spellId; + info.attributes = dbc->getUInt32(i, attrField); + info.iconId = dbc->getUInt32(i, iconField); + info.name = dbc->getString(i, nameField); + info.rank = dbc->getString(i, rankField); + + if (!info.name.empty()) { + spellData[spellId] = std::move(info); + } } + LOG_INFO("Spellbook: Loaded ", spellData.size(), " spells from Spell.dbc (", label, ")"); + }; + + // Try active expansion layout + if (spellL) { + tryLoad((*spellL)["ID"], (*spellL)["Attributes"], (*spellL)["IconID"], + (*spellL)["Name"], (*spellL)["Rank"], "expansion layout"); } - dbcLoaded = true; - LOG_INFO("Spellbook: Loaded ", spellData.size(), " spells from Spell.dbc"); + // If layout failed or loaded 0 spells, try WotLK hardcoded indices + // (binary DBC may be from WotLK MPQs regardless of active expansion) + if (spellData.empty() && fieldCount >= 200) { + LOG_INFO("Spellbook: Retrying with WotLK field indices (DBC has ", fieldCount, " fields)"); + tryLoad(0, 4, 133, 136, 153, "WotLK fallback"); + } + + dbcLoaded = !spellData.empty(); } void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {