mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix Turtle WoW compatibility: NPC spawning, quests, spells, realm display, and music
- Add TurtlePacketParsers with dedicated movement block parser (Classic format + transport timestamp) - Fix quest giver status: read uint32 and translate vanilla enum values for Classic/Turtle - Fix quest accept packet: remove trailing uint32 that vanilla servers reject - Fix quest details parser: auto-detect vanilla vs WotLK format (informUnit field) - Fix spellbook and action bar icons: fallback to WotLK DBC field indices when expansion layout fails - Fix spell cast failure messages: translate vanilla SpellCastResult codes (+1 offset) - Fix realm list: correct type values (6=RP, 8=RP-PvP) and population thresholds - Fix music: disable looping for zone music, auto-advance to next random track when finished - Add music anti-repeat: avoid playing the same track back-to-back - Make TBC update block parsing resilient (keep parsed blocks on failure instead of aborting) - Add right-click attack on hostile mobs - Add name query diagnostic logging
This commit is contained in:
parent
d850fe6fc0
commit
36fc1df706
12 changed files with 358 additions and 48 deletions
|
|
@ -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<PacketParsers> createPacketParsers(const std::string& expansionId) {
|
||||
if (expansionId == "classic" || expansionId == "turtle") return std::make_unique<ClassicPacketParsers>();
|
||||
if (expansionId == "classic") return std::make_unique<ClassicPacketParsers>();
|
||||
if (expansionId == "turtle") return std::make_unique<TurtlePacketParsers>();
|
||||
if (expansionId == "tbc") return std::make_unique<TbcPacketParsers>();
|
||||
return std::make_unique<WotlkPacketParsers>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<std::string> getAllMusicPaths() const;
|
||||
|
||||
private:
|
||||
// tile key = tileX * 100 + tileY
|
||||
std::unordered_map<int, uint32_t> tileToZone;
|
||||
std::unordered_map<uint32_t, ZoneInfo> zones;
|
||||
std::string lastPlayedMusic_;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<QuestGiverStatus>(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<QuestGiverStatus>(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
|
||||
|
|
|
|||
|
|
@ -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<uint16_t>(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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<std::string> ZoneManager::getAllMusicPaths() const {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue