diff --git a/docs/status.md b/docs/status.md index fca68f19..991d813d 100644 --- a/docs/status.md +++ b/docs/status.md @@ -25,9 +25,6 @@ Implemented (working in normal use): - Talent tree UI with proper visuals and functionality - Pet tracking (SMSG_PET_SPELLS), dismiss pet button - Party: group invites, party list, out-of-range member health (SMSG_PARTY_MEMBER_STATS) -- Nameplates: NPC subtitles, guild names, elite/boss/rare borders, quest/raid indicators, cast bars, debuff dots -- Floating combat text: world-space damage/heal numbers above entities with 3D projection -- Target/focus frames: guild name, creature type, rank badges, combo points, cast bars - Map exploration: subzone-level fog-of-war reveal - Warden anti-cheat: full module execution via Unicorn Engine x86 emulation; module caching - Audio: ambient, movement, combat, spell, and UI sound systems diff --git a/include/game/entity.hpp b/include/game/entity.hpp index a608f6f5..57147902 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -135,13 +135,6 @@ public: bool isEntityMoving() const { return isMoving_; } - /// True only during the active interpolation phase (before reaching destination). - /// Unlike isEntityMoving(), this does NOT include the dead-reckoning overrun window, - /// so animations (Run/Walk) should use this to avoid "running in place" after arrival. - bool isActivelyMoving() const { - return isMoving_ && moveElapsed_ < moveDuration_; - } - // Returns the latest server-authoritative position: destination if moving, current if not. // Unlike getX/Y/Z (which only update via updateMovement), this always reflects the // last known server position regardless of distance culling. @@ -284,14 +277,18 @@ protected: /** * Player entity - * Name is inherited from Unit — do NOT redeclare it here or the - * shadowed field will diverge from Unit::name, causing nameplates - * and other Unit*-based lookups to read an empty string. */ class Player : public Unit { public: Player() { type = ObjectType::PLAYER; } explicit Player(uint64_t guid) : Unit(guid) { type = ObjectType::PLAYER; } + + // Name + const std::string& getName() const { return name; } + void setName(const std::string& n) { name = n; } + +protected: + std::string name; }; /** diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 569261b2..47997040 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -453,7 +453,6 @@ public: uint32_t avgWaitTimeSec = 0; // server-estimated average wait (STATUS_WAIT_QUEUE) uint32_t timeInQueueSec = 0; // time already spent in queue (STATUS_WAIT_QUEUE) std::chrono::steady_clock::time_point inviteReceivedTime{}; - std::string bgName; // human-readable BG/arena name }; // Available BG list (populated by SMSG_BATTLEFIELD_LIST) @@ -609,33 +608,6 @@ public: uint32_t getPetitionCost() const { return petitionCost_; } uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; } - // Petition signatures (guild charter signing flow) - struct PetitionSignature { - uint64_t playerGuid = 0; - std::string playerName; // resolved later or empty - }; - struct PetitionInfo { - uint64_t petitionGuid = 0; - uint64_t ownerGuid = 0; - std::string guildName; - uint32_t signatureCount = 0; - uint32_t signaturesRequired = 9; // guild default; arena teams differ - std::vector signatures; - bool showUI = false; - }; - const PetitionInfo& getPetitionInfo() const { return petitionInfo_; } - bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; } - void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; } - void signPetition(uint64_t petitionGuid); - void turnInPetition(uint64_t petitionGuid); - - // Guild name lookup for other players' nameplates - // Returns the guild name for a given guildId, or empty if unknown. - // Automatically queries the server for unknown guild IDs. - const std::string& lookupGuildName(uint32_t guildId); - // Returns the guildId for a player entity (from PLAYER_GUILDID update field). - uint32_t getEntityGuildId(uint64_t guid) const; - // Ready check struct ReadyCheckResult { std::string name; @@ -695,16 +667,6 @@ public: auto it = creatureInfoCache.find(entry); return (it != creatureInfoCache.end()) ? static_cast(it->second.rank) : -1; } - // Returns creature type (1=Beast,2=Dragonkin,...,7=Humanoid,...) or 0 if not cached - uint32_t getCreatureType(uint32_t entry) const { - auto it = creatureInfoCache.find(entry); - return (it != creatureInfoCache.end()) ? it->second.creatureType : 0; - } - // Returns creature family (e.g. pet family for beasts) or 0 - uint32_t getCreatureFamily(uint32_t entry) const { - auto it = creatureInfoCache.find(entry); - return (it != creatureInfoCache.end()) ? it->second.family : 0; - } // ---- Phase 2: Combat ---- void startAutoAttack(uint64_t targetGuid); @@ -969,10 +931,6 @@ public: using StandStateCallback = std::function; void setStandStateCallback(StandStateCallback cb) { standStateCallback_ = std::move(cb); } - // Appearance changed callback — fired when PLAYER_BYTES or facial features update (barber shop, etc.) - using AppearanceChangedCallback = std::function; - void setAppearanceChangedCallback(AppearanceChangedCallback cb) { appearanceChangedCallback_ = std::move(cb); } - // Ghost state callback — fired when player enters or leaves ghost (spirit) form using GhostStateCallback = std::function; void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); } @@ -1244,17 +1202,6 @@ public: uint32_t getPetUnlearnCost() const { return petUnlearnCost_; } void confirmPetUnlearn(); void cancelPetUnlearn() { petUnlearnPending_ = false; } - - // Barber shop - bool isBarberShopOpen() const { return barberShopOpen_; } - void closeBarberShop() { barberShopOpen_ = false; } - void sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair); - - // Instance difficulty (0=5N, 1=5H, 2=25N, 3=25H for WotLK) - uint32_t getInstanceDifficulty() const { return instanceDifficulty_; } - bool isInstanceHeroic() const { return instanceIsHeroic_; } - bool isInInstance() const { return inInstance_; } - /** True when ghost is within 40 yards of corpse position (same map). */ bool canReclaimCorpse() const; /** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */ @@ -1451,11 +1398,8 @@ public: uint32_t seasonGames = 0; uint32_t seasonWins = 0; uint32_t rank = 0; - std::string teamName; - uint32_t teamType = 0; // 2, 3, or 5 }; const std::vector& getArenaTeamStats() const { return arenaTeamStats_; } - void requestArenaTeamRoster(uint32_t teamId); // ---- Arena Team Roster ---- struct ArenaTeamMember { @@ -1507,7 +1451,6 @@ public: std::string itemName; uint8_t itemQuality = 0; uint32_t rollCountdownMs = 60000; // Duration of roll window in ms - uint8_t voteMask = 0xFF; // Bitmask: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant std::chrono::steady_clock::time_point rollStartedAt{}; struct PlayerRollResult { @@ -2068,7 +2011,6 @@ public: void openItemBySlot(int backpackIndex); void openItemInBag(int bagIndex, int slotIndex); void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1); - void splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count); void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot); void swapBagSlots(int srcBagIndex, int dstBagIndex); void useItemById(uint32_t itemId); @@ -2395,9 +2337,6 @@ private: void handleGuildInvite(network::Packet& packet); void handleGuildCommandResult(network::Packet& packet); void handlePetitionShowlist(network::Packet& packet); - void handlePetitionQueryResponse(network::Packet& packet); - void handlePetitionShowSignatures(network::Packet& packet); - void handlePetitionSignResults(network::Packet& packet); void handlePetSpells(network::Packet& packet); void handleTurnInPetitionResults(network::Packet& packet); @@ -2898,7 +2837,6 @@ private: // Instance difficulty uint32_t instanceDifficulty_ = 0; bool instanceIsHeroic_ = false; - bool inInstance_ = false; // Raid target markers (icon 0-7 -> guid; 0 = empty slot) std::array raidTargetGuids_ = {}; @@ -3014,22 +2952,16 @@ private: GuildInfoData guildInfoData_; GuildQueryResponseData guildQueryData_; bool hasGuildRoster_ = false; - std::unordered_map guildNameCache_; // guildId → guild name - std::unordered_set pendingGuildNameQueries_; // in-flight guild queries bool pendingGuildInvite_ = false; std::string pendingGuildInviterName_; std::string pendingGuildInviteGuildName_; bool showPetitionDialog_ = false; uint32_t petitionCost_ = 0; uint64_t petitionNpcGuid_ = 0; - PetitionInfo petitionInfo_; uint64_t activeCharacterGuid_ = 0; Race playerRace_ = Race::HUMAN; - // Barber shop - bool barberShopOpen_ = false; - // ---- Phase 5: Loot ---- bool lootWindowOpen = false; bool autoLoot_ = false; @@ -3385,7 +3317,6 @@ private: NpcAggroCallback npcAggroCallback_; NpcRespawnCallback npcRespawnCallback_; StandStateCallback standStateCallback_; - AppearanceChangedCallback appearanceChangedCallback_; GhostStateCallback ghostStateCallback_; MeleeSwingCallback meleeSwingCallback_; uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index c6fe3663..67d94c2d 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -61,9 +61,6 @@ struct CombatTextEntry { float age = 0.0f; // Seconds since creation (for fadeout) bool isPlayerSource = false; // True if player dealt this uint8_t powerType = 0; // For ENERGIZE/POWER_DRAIN: 0=mana,1=rage,2=focus,3=energy,6=runicpower - uint64_t srcGuid = 0; // Source entity (attacker/caster) - uint64_t dstGuid = 0; // Destination entity (victim/target) — used for world-space positioning - float xSeed = 0.0f; // Random horizontal offset seed (-1..1) to stagger overlapping text static constexpr float LIFETIME = 2.5f; bool isExpired() const { return age >= LIFETIME; } diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c0408743..c2aa581f 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2046,13 +2046,6 @@ public: static network::Packet build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot); }; -/** CMSG_SPLIT_ITEM packet builder */ -class SplitItemPacket { -public: - static network::Packet build(uint8_t srcBag, uint8_t srcSlot, - uint8_t dstBag, uint8_t dstSlot, uint8_t count); -}; - /** CMSG_SWAP_INV_ITEM packet builder */ class SwapInvItemPacket { public: @@ -2796,12 +2789,5 @@ public: static network::Packet build(int32_t titleBit); }; -/** CMSG_ALTER_APPEARANCE – barber shop: change hair style, color, facial hair. - * Payload: uint32 hairStyle, uint32 hairColor, uint32 facialHair. */ -class AlterAppearancePacket { -public: - static network::Packet build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair); -}; - } // namespace game } // namespace wowee diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 1f19b46e..22578309 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include namespace wowee { @@ -435,9 +434,6 @@ private: void* glowVBMapped_ = nullptr; std::unordered_map models; - // Grace period for model cleanup: track when a model first became instanceless. - // Models are only evicted after 60 seconds with no instances. - std::unordered_map modelUnusedSince_; std::vector instances; // O(1) dedup: key = (modelId, quantized x, quantized y, quantized z) → instanceId diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 588fa3af..1f33d2f4 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -154,9 +154,6 @@ public: void triggerLevelUpEffect(const glm::vec3& position); void cancelEmote(); - // Screenshot capture — copies swapchain image to PNG file - bool captureScreenshot(const std::string& outputPath); - // Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT) // useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 9fa540b3..290c45eb 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -279,9 +279,6 @@ public: /** Process one ready tile (for loading screens with per-tile progress updates) */ void processOneReadyTile(); - /** Process a bounded batch of ready tiles with async GPU upload (no sync wait) */ - void processReadyTiles(); - private: /** * Get tile coordinates from GL world position @@ -320,6 +317,10 @@ private: */ void workerLoop(); + /** + * Main thread: poll for completed tiles and upload to GPU + */ + void processReadyTiles(); void ensureGroundEffectTablesLoaded(); void generateGroundClutterPlacements(std::shared_ptr& pending, std::unordered_set& preparedModelIds); diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 4bc10707..0054bf05 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -86,8 +86,7 @@ private: bool showEntityWindow = false; bool showChatWindow = true; bool showMinimap_ = true; // M key toggles minimap - bool showNameplates_ = true; // V key toggles enemy/NPC nameplates - bool showFriendlyNameplates_ = true; // Shift+V toggles friendly player nameplates + bool showNameplates_ = true; // V key toggles nameplates float nameplateScale_ = 1.0f; // Scale multiplier for nameplate bar dimensions uint64_t nameplateCtxGuid_ = 0; // GUID of nameplate right-clicked (0 = none) ImVec2 nameplateCtxPos_{}; // Screen position of nameplate right-click @@ -366,7 +365,6 @@ private: void renderQuestOfferRewardWindow(game::GameHandler& gameHandler); void renderVendorWindow(game::GameHandler& gameHandler); void renderTrainerWindow(game::GameHandler& gameHandler); - void renderBarberShopWindow(game::GameHandler& gameHandler); void renderStableWindow(game::GameHandler& gameHandler); void renderTaxiWindow(game::GameHandler& gameHandler); void renderLogoutCountdown(game::GameHandler& gameHandler); @@ -400,7 +398,6 @@ private: void renderBattlegroundScore(game::GameHandler& gameHandler); void renderDPSMeter(game::GameHandler& gameHandler); void renderDurabilityWarning(game::GameHandler& gameHandler); - void takeScreenshot(game::GameHandler& gameHandler); /** * Inventory screen @@ -535,24 +532,6 @@ private: // Vendor search filter char vendorSearchFilter_[128] = ""; - // Vendor purchase confirmation for expensive items - bool vendorConfirmOpen_ = false; - uint64_t vendorConfirmGuid_ = 0; - uint32_t vendorConfirmItemId_ = 0; - uint32_t vendorConfirmSlot_ = 0; - uint32_t vendorConfirmQty_ = 1; - uint32_t vendorConfirmPrice_ = 0; - std::string vendorConfirmItemName_; - - // Barber shop UI state - int barberHairStyle_ = 0; - int barberHairColor_ = 0; - int barberFacialHair_ = 0; - int barberOrigHairStyle_ = 0; - int barberOrigHairColor_ = 0; - int barberOrigFacialHair_ = 0; - bool barberInitialized_ = false; - // Trainer search filter char trainerSearchFilter_[128] = ""; @@ -665,7 +644,6 @@ private: float resurrectFlashTimer_ = 0.0f; static constexpr float kResurrectFlashDuration = 3.0f; bool ghostStateCallbackSet_ = false; - bool appearanceCallbackSet_ = false; bool ghostOpacityStateKnown_ = false; bool ghostOpacityLastState_ = false; uint32_t ghostOpacityLastInstanceId_ = 0; diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index dca0e5a5..21ccdc00 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -187,14 +187,6 @@ private: uint8_t destroyCount_ = 1; std::string destroyItemName_; - // Stack split popup state - bool splitConfirmOpen_ = false; - uint8_t splitBag_ = 0xFF; - uint8_t splitSlot_ = 0; - int splitMax_ = 1; - int splitCount_ = 1; - std::string splitItemName_; - // Pending chat item link from shift-click std::string pendingChatItemLink_; diff --git a/include/ui/talent_screen.hpp b/include/ui/talent_screen.hpp index 82a674e4..72eafc2a 100644 --- a/include/ui/talent_screen.hpp +++ b/include/ui/talent_screen.hpp @@ -45,12 +45,6 @@ private: std::unordered_map spellTooltips; // spellId -> description std::unordered_map bgTextureCache_; // tabId -> bg texture - // Talent learn confirmation - bool talentConfirmOpen_ = false; - uint32_t pendingTalentId_ = 0; - uint32_t pendingTalentRank_ = 0; - std::string pendingTalentName_; - // GlyphProperties.dbc cache: glyphId -> { spellId, isMajor } struct GlyphInfo { uint32_t spellId = 0; bool isMajor = false; }; std::unordered_map glyphProperties_; // glyphId -> info diff --git a/src/core/application.cpp b/src/core/application.cpp index 28f2fad1..4ff3aae1 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1647,11 +1647,7 @@ void Application::update(float deltaTime) { // startMoveTo() in handleMonsterMove, regardless of distance-cull. // This correctly detects movement for distant creatures (> 150u) // where updateMovement() is not called and getX/Y/Z() stays stale. - // Use isActivelyMoving() (not isEntityMoving()) so the - // Run/Walk animation stops when the creature reaches its - // destination, rather than persisting through the dead- - // reckoning overrun window. - const bool entityIsMoving = entity->isActivelyMoving(); + const bool entityIsMoving = entity->isEntityMoving(); const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f); if (deadOrCorpse || largeCorrection) { charRenderer->setInstancePosition(instanceId, renderPos); @@ -1720,110 +1716,6 @@ void Application::update(float deltaTime) { } } - // --- Online player render sync (position, orientation, animation) --- - // Mirrors the creature sync loop above but without collision guard or - // weapon-attach logic. Without this, online players never transition - // back to Stand after movement stops ("run in place" bug). - auto playerSyncStart = std::chrono::steady_clock::now(); - if (renderer && gameHandler && renderer->getCharacterRenderer()) { - auto* charRenderer = renderer->getCharacterRenderer(); - glm::vec3 pPos(0.0f); - bool havePPos = false; - if (auto pe = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid())) { - pPos = glm::vec3(pe->getX(), pe->getY(), pe->getZ()); - havePPos = true; - } - const float pSyncRadiusSq = 320.0f * 320.0f; - - for (const auto& [guid, instanceId] : playerInstances_) { - auto entity = gameHandler->getEntityManager().getEntity(guid); - if (!entity || entity->getType() != game::ObjectType::PLAYER) continue; - - // Distance cull - if (havePPos) { - glm::vec3 latestCanonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); - glm::vec3 d = latestCanonical - pPos; - if (glm::dot(d, d) > pSyncRadiusSq) continue; - } - - // Position sync - glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); - glm::vec3 renderPos = core::coords::canonicalToRender(canonical); - - auto posIt = creatureRenderPosCache_.find(guid); - if (posIt == creatureRenderPosCache_.end()) { - charRenderer->setInstancePosition(instanceId, renderPos); - creatureRenderPosCache_[guid] = renderPos; - } else { - const glm::vec3 prevPos = posIt->second; - const glm::vec2 delta2(renderPos.x - prevPos.x, renderPos.y - prevPos.y); - float planarDist = glm::length(delta2); - float dz = std::abs(renderPos.z - prevPos.z); - - auto unitPtr = std::static_pointer_cast(entity); - const bool deadOrCorpse = unitPtr->getHealth() == 0; - const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f); - const bool entityIsMoving = entity->isActivelyMoving(); - const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f); - - if (deadOrCorpse || largeCorrection) { - charRenderer->setInstancePosition(instanceId, renderPos); - } else if (planarDist > 0.03f || dz > 0.08f) { - float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f); - charRenderer->moveInstanceTo(instanceId, renderPos, duration); - } - posIt->second = renderPos; - - // Drive movement animation (same logic as creatures) - const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0; - const bool isWalkingNow = creatureWalkingState_.count(guid) > 0; - const bool isFlyingNow = creatureFlyingState_.count(guid) > 0; - bool prevMoving = creatureWasMoving_[guid]; - bool prevSwimming = creatureWasSwimming_[guid]; - bool prevFlying = creatureWasFlying_[guid]; - bool prevWalking = creatureWasWalking_[guid]; - const bool stateChanged = (isMovingNow != prevMoving) || - (isSwimmingNow != prevSwimming) || - (isFlyingNow != prevFlying) || - (isWalkingNow != prevWalking && isMovingNow); - if (stateChanged) { - creatureWasMoving_[guid] = isMovingNow; - creatureWasSwimming_[guid] = isSwimmingNow; - creatureWasFlying_[guid] = isFlyingNow; - creatureWasWalking_[guid] = isWalkingNow; - uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; - bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur); - if (!gotState || curAnimId != 1 /*Death*/) { - uint32_t targetAnim; - if (isMovingNow) { - if (isFlyingNow) targetAnim = 159u; // FlyForward - else if (isSwimmingNow) targetAnim = 42u; // Swim - else if (isWalkingNow) targetAnim = 4u; // Walk - else targetAnim = 5u; // Run - } else { - if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover) - else if (isSwimmingNow) targetAnim = 41u; // SwimIdle - else targetAnim = 0u; // Stand - } - charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true); - } - } - } - - // Orientation sync - float renderYaw = entity->getOrientation() + glm::radians(90.0f); - charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw)); - } - } - { - float psMs = std::chrono::duration( - std::chrono::steady_clock::now() - playerSyncStart).count(); - if (psMs > 5.0f) { - LOG_WARNING("SLOW update stage 'player render sync': ", psMs, "ms (", - playerInstances_.size(), " players)"); - } - } - // Movement heartbeat is sent from GameHandler::update() to avoid // duplicate packets from multiple update loops. @@ -2098,7 +1990,7 @@ void Application::setupUICallbacks() { worldEntryMovementGraceTimer_ = 2.0f; taxiLandingClampTimer_ = 0.0f; lastTaxiFlight_ = false; - renderer->getTerrainManager()->processReadyTiles(); + renderer->getTerrainManager()->processAllReadyTiles(); { auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y); std::vector> nearbyTiles; @@ -2131,12 +2023,10 @@ void Application::setupUICallbacks() { renderer->getCameraController()->clearMovementInputs(); renderer->getCameraController()->suppressMovementFor(0.5f); } - // Kick off async upload for any tiles that finished background - // parsing. Use the bounded processReadyTiles() instead of - // processAllReadyTiles() to avoid multi-second main-thread stalls - // when many tiles are ready (the rest will finalize over subsequent - // frames via the normal terrain update loop). - renderer->getTerrainManager()->processReadyTiles(); + // Flush any tiles that finished background parsing during the cast + // (e.g. Hearthstone pre-loaded them) so they're GPU-uploaded before + // the first frame at the new position. + renderer->getTerrainManager()->processAllReadyTiles(); // Queue all remaining tiles within the load radius (8 tiles = 17x17) // at the new position. precacheTiles skips already-loaded/pending tiles, @@ -2999,50 +2889,29 @@ void Application::setupUICallbacks() { } }); - // NPC/player death callback (online mode) - play death animation + // NPC death callback (online mode) - play death animation gameHandler->setNpcDeathCallback([this](uint64_t guid) { deadCreatureGuids_.insert(guid); - if (!renderer || !renderer->getCharacterRenderer()) return; - uint32_t instanceId = 0; auto it = creatureInstances_.find(guid); - if (it != creatureInstances_.end()) instanceId = it->second; - else { - auto pit = playerInstances_.find(guid); - if (pit != playerInstances_.end()) instanceId = pit->second; - } - if (instanceId != 0) { - renderer->getCharacterRenderer()->playAnimation(instanceId, 1, false); // Death + if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { + renderer->getCharacterRenderer()->playAnimation(it->second, 1, false); // Death } }); - // NPC/player respawn callback (online mode) - reset to idle animation + // NPC respawn callback (online mode) - reset to idle animation gameHandler->setNpcRespawnCallback([this](uint64_t guid) { deadCreatureGuids_.erase(guid); - if (!renderer || !renderer->getCharacterRenderer()) return; - uint32_t instanceId = 0; auto it = creatureInstances_.find(guid); - if (it != creatureInstances_.end()) instanceId = it->second; - else { - auto pit = playerInstances_.find(guid); - if (pit != playerInstances_.end()) instanceId = pit->second; - } - if (instanceId != 0) { - renderer->getCharacterRenderer()->playAnimation(instanceId, 0, true); // Idle + if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { + renderer->getCharacterRenderer()->playAnimation(it->second, 0, true); // Idle } }); - // NPC/player swing callback (online mode) - play attack animation + // NPC swing callback (online mode) - play attack animation gameHandler->setNpcSwingCallback([this](uint64_t guid) { - if (!renderer || !renderer->getCharacterRenderer()) return; - uint32_t instanceId = 0; auto it = creatureInstances_.find(guid); - if (it != creatureInstances_.end()) instanceId = it->second; - else { - auto pit = playerInstances_.find(guid); - if (pit != playerInstances_.end()) instanceId = pit->second; - } - if (instanceId != 0) { - renderer->getCharacterRenderer()->playAnimation(instanceId, 16, false); // Attack + if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { + renderer->getCharacterRenderer()->playAnimation(it->second, 16, false); // Attack } }); @@ -4854,42 +4723,24 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float gameHandler->setNpcDeathCallback([cr, app](uint64_t guid) { app->deadCreatureGuids_.insert(guid); - uint32_t instanceId = 0; auto it = app->creatureInstances_.find(guid); - if (it != app->creatureInstances_.end()) instanceId = it->second; - else { - auto pit = app->playerInstances_.find(guid); - if (pit != app->playerInstances_.end()) instanceId = pit->second; - } - if (instanceId != 0 && cr) { - cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death + if (it != app->creatureInstances_.end() && cr) { + cr->playAnimation(it->second, 1, false); // animation ID 1 = Death } }); gameHandler->setNpcRespawnCallback([cr, app](uint64_t guid) { app->deadCreatureGuids_.erase(guid); - uint32_t instanceId = 0; auto it = app->creatureInstances_.find(guid); - if (it != app->creatureInstances_.end()) instanceId = it->second; - else { - auto pit = app->playerInstances_.find(guid); - if (pit != app->playerInstances_.end()) instanceId = pit->second; - } - if (instanceId != 0 && cr) { - cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle + if (it != app->creatureInstances_.end() && cr) { + cr->playAnimation(it->second, 0, true); // animation ID 0 = Idle } }); gameHandler->setNpcSwingCallback([cr, app](uint64_t guid) { - uint32_t instanceId = 0; auto it = app->creatureInstances_.find(guid); - if (it != app->creatureInstances_.end()) instanceId = it->second; - else { - auto pit = app->playerInstances_.find(guid); - if (pit != app->playerInstances_.end()) instanceId = pit->second; - } - if (instanceId != 0 && cr) { - cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1 + if (it != app->creatureInstances_.end() && cr) { + cr->playAnimation(it->second, 16, false); // animation ID 16 = Attack1 } }); } @@ -7193,7 +7044,6 @@ void Application::despawnOnlinePlayer(uint64_t guid) { playerInstances_.erase(it); onlinePlayerAppearance_.erase(guid); pendingOnlinePlayerEquipment_.erase(guid); - creatureRenderPosCache_.erase(guid); creatureSwimmingState_.erase(guid); creatureWalkingState_.erase(guid); creatureFlyingState_.erase(guid); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 16666085..3591d97a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -759,8 +759,6 @@ void GameHandler::disconnect() { activeCharacterGuid_ = 0; playerNameCache.clear(); pendingNameQueries.clear(); - guildNameCache_.clear(); - pendingGuildNameQueries_.clear(); friendGuids_.clear(); contacts_.clear(); transportAttachments_.clear(); @@ -2344,7 +2342,7 @@ void GameHandler::handlePacket(network::Packet& packet) { /*uint32_t randProp =*/ packet.readUInt32(); } uint32_t countdown = packet.readUInt32(); - uint8_t voteMask = packet.readUInt8(); + /*uint8_t voteMask =*/ packet.readUInt8(); // Trigger the roll popup for local player pendingLootRollActive_ = true; pendingLootRoll_.objectGuid = objectGuid; @@ -2358,10 +2356,9 @@ void GameHandler::handlePacket(network::Packet& packet) { pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; pendingLootRoll_.rollCountdownMs = (countdown > 0 && countdown <= 120000) ? countdown : 60000; - pendingLootRoll_.voteMask = voteMask; pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now(); LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName, - ") slot=", slot, " voteMask=0x", std::hex, (int)voteMask, std::dec); + ") slot=", slot); break; } @@ -2681,8 +2678,8 @@ void GameHandler::handlePacket(network::Packet& packet) { } case Opcode::SMSG_ENABLE_BARBER_SHOP: // Sent by server when player sits in barber chair — triggers barber shop UI + // No payload; we don't have barber shop UI yet, so just log LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); - barberShopOpen_ = true; break; case Opcode::SMSG_FEIGN_DEATH_RESISTED: addUIError("Your Feign Death was resisted."); @@ -2952,7 +2949,6 @@ void GameHandler::handlePacket(network::Packet& packet) { struct SpellMissLogEntry { uint64_t victimGuid = 0; uint8_t missInfo = 0; - uint32_t reflectSpellId = 0; // Only valid when missInfo==11 (REFLECT) }; std::vector parsedMisses; parsedMisses.reserve(storedLimit); @@ -2971,18 +2967,17 @@ void GameHandler::handlePacket(network::Packet& packet) { } const uint8_t missInfo = packet.readUInt8(); // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult - uint32_t reflectSpellId = 0; if (missInfo == 11) { if (packet.getSize() - packet.getReadPos() >= 5) { - reflectSpellId = packet.readUInt32(); - /*uint8_t reflectResult =*/ packet.readUInt8(); + /*uint32_t reflectSpellId =*/ packet.readUInt32(); + /*uint8_t reflectResult =*/ packet.readUInt8(); } else { truncated = true; break; } } if (i < storedLimit) { - parsedMisses.push_back({victimGuid, missInfo, reflectSpellId}); + parsedMisses.push_back({victimGuid, missInfo}); } } @@ -2995,15 +2990,12 @@ void GameHandler::handlePacket(network::Packet& packet) { const uint64_t victimGuid = miss.victimGuid; const uint8_t missInfo = miss.missInfo; CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo); - // For REFLECT, use the reflected spell ID so combat text shows the spell name - uint32_t combatSpellId = (ct == CombatTextEntry::REFLECT && miss.reflectSpellId != 0) - ? miss.reflectSpellId : spellId; if (casterGuid == playerGuid) { // We cast a spell and it missed the target - addCombatText(ct, 0, combatSpellId, true, 0, casterGuid, victimGuid); + addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid); } else if (victimGuid == playerGuid) { // Enemy spell missed us (we dodged/parried/blocked/resisted/etc.) - addCombatText(ct, 0, combatSpellId, false, 0, casterGuid, victimGuid); + addCombatText(ct, 0, spellId, false, 0, casterGuid, victimGuid); } } break; @@ -4258,20 +4250,17 @@ void GameHandler::handlePacket(network::Packet& packet) { uint8_t auraType = packet.readUInt8(); if (auraType == 3 || auraType == 89) { // Classic/TBC: damage(4)+school(4)+absorbed(4)+resisted(4) = 16 bytes - // WotLK 3.3.5a: damage(4)+overkill(4)+school(4)+absorbed(4)+resisted(4)+isCrit(1) = 21 bytes + // WotLK 3.3.5a: damage(4)+overkill(4)+school(4)+absorbed(4)+resisted(4) = 20 bytes const bool periodicWotlk = isActiveExpansion("wotlk"); - const size_t dotSz = periodicWotlk ? 21u : 16u; + const size_t dotSz = periodicWotlk ? 20u : 16u; if (packet.getSize() - packet.getReadPos() < dotSz) break; uint32_t dmg = packet.readUInt32(); if (periodicWotlk) /*uint32_t overkill=*/ packet.readUInt32(); /*uint32_t school=*/ packet.readUInt32(); uint32_t abs = packet.readUInt32(); uint32_t res = packet.readUInt32(); - bool dotCrit = false; - if (periodicWotlk) dotCrit = (packet.readUInt8() != 0); if (dmg > 0) - addCombatText(dotCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::PERIODIC_DAMAGE, - static_cast(dmg), + addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast(dmg), spellId, isPlayerCaster, 0, casterGuid, victimGuid); if (abs > 0) addCombatText(CombatTextEntry::ABSORB, static_cast(abs), @@ -4289,13 +4278,11 @@ void GameHandler::handlePacket(network::Packet& packet) { /*uint32_t max=*/ packet.readUInt32(); /*uint32_t over=*/ packet.readUInt32(); uint32_t hotAbs = 0; - bool hotCrit = false; if (healWotlk) { hotAbs = packet.readUInt32(); - hotCrit = (packet.readUInt8() != 0); + /*uint8_t isCrit=*/ packet.readUInt8(); } - addCombatText(hotCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::PERIODIC_HEAL, - static_cast(heal), + addCombatText(CombatTextEntry::PERIODIC_HEAL, static_cast(heal), spellId, isPlayerCaster, 0, casterGuid, victimGuid); if (hotAbs > 0) addCombatText(CombatTextEntry::ABSORB, static_cast(hotAbs), @@ -4902,7 +4889,6 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t result = packet.readUInt32(); if (result == 0) { addSystemChatMessage("Hairstyle changed."); - barberShopOpen_ = false; } else { const char* msg = (result == 1) ? "Not enough money for new hairstyle." : (result == 2) ? "You are not at a barber shop." @@ -5806,31 +5792,7 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_LFG_OFFER_CONTINUE: addSystemChatMessage("Dungeon Finder: You may continue your dungeon."); break; - case Opcode::SMSG_LFG_ROLE_CHOSEN: { - // uint64 guid + uint8 ready + uint32 roles - if (packet.getSize() - packet.getReadPos() >= 13) { - uint64_t roleGuid = packet.readUInt64(); - uint8_t ready = packet.readUInt8(); - uint32_t roles = packet.readUInt32(); - // Build a descriptive message for group chat - std::string roleName; - if (roles & 0x02) roleName += "Tank "; - if (roles & 0x04) roleName += "Healer "; - if (roles & 0x08) roleName += "DPS "; - if (roleName.empty()) roleName = "None"; - // Find player name - std::string pName = "A player"; - if (auto e = entityManager.getEntity(roleGuid)) - if (auto u = std::dynamic_pointer_cast(e)) - pName = u->getName(); - if (ready) - addSystemChatMessage(pName + " has chosen: " + roleName); - LOG_DEBUG("SMSG_LFG_ROLE_CHOSEN: guid=", roleGuid, - " ready=", (int)ready, " roles=", roles); - } - packet.setReadPos(packet.getSize()); - break; - } + case Opcode::SMSG_LFG_ROLE_CHOSEN: case Opcode::SMSG_LFG_UPDATE_SEARCH: case Opcode::SMSG_UPDATE_LFG_LIST: case Opcode::SMSG_LFG_PLAYER_INFO: @@ -7654,13 +7616,9 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_PETITION_QUERY_RESPONSE: - handlePetitionQueryResponse(packet); - break; case Opcode::SMSG_PETITION_SHOW_SIGNATURES: - handlePetitionShowSignatures(packet); - break; case Opcode::SMSG_PETITION_SIGN_RESULTS: - handlePetitionSignResults(packet); + packet.setReadPos(packet.getSize()); break; // ---- Pet system ---- @@ -7714,17 +7672,13 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_PET_CAST_FAILED: { - // WotLK: castCount(1) + spellId(4) + reason(1) - // Classic/TBC: spellId(4) + reason(1) (no castCount) - const bool hasCount = isActiveExpansion("wotlk"); - const size_t minSize = hasCount ? 6u : 5u; - if (packet.getSize() - packet.getReadPos() >= minSize) { - if (hasCount) /*uint8_t castCount =*/ packet.readUInt8(); + if (packet.getSize() - packet.getReadPos() >= 5) { + uint8_t castCount = packet.readUInt8(); uint32_t spellId = packet.readUInt32(); uint8_t reason = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; LOG_DEBUG("SMSG_PET_CAST_FAILED: spell=", spellId, - " reason=", (int)reason); + " reason=", (int)reason, " castCount=", (int)castCount); if (reason != 0) { const char* reasonStr = getSpellCastResultString(reason); const std::string& sName = getSpellName(spellId); @@ -11339,10 +11293,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem // Track player-on-transport state if (block.guid == playerGuid) { if (block.onTransport) { + setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f)); // Convert transport offset from server → canonical coordinates glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); - glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset); - setPlayerOnTransport(block.transportGuid, canonicalOffset); + playerTransportOffset_ = core::coords::serverToCanonical(serverOffset); if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) { glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); entity->setPosition(composed.x, composed.y, composed.z, oCanonical); @@ -11608,9 +11562,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem " displayId=", unit->getDisplayId(), " appearance extraction failed — model will not render"); } } - if (unitInitiallyDead && npcDeathCallback_) { - npcDeathCallback_(block.guid); - } } else if (creatureSpawnCallback_) { LOG_DEBUG("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, " displayId=", unit->getDisplayId(), " at (", @@ -11957,7 +11908,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem corpseX_, ",", corpseY_, ",", corpseZ_, ") map=", corpseMapId_); } - if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && npcDeathCallback_) { + if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) { npcDeathCallback_(block.guid); npcDeathNotified = true; } @@ -11970,7 +11921,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem LOG_INFO("Player entered ghost form"); } } - if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && npcRespawnCallback_) { + if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) { npcRespawnCallback_(block.guid); npcRespawnNotified = true; } @@ -12001,7 +11952,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem selfResAvailable_ = false; LOG_INFO("Player resurrected (dynamic flags)"); } - } else if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { + } else if (entity->getType() == ObjectType::UNIT) { bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0; bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; if (!wasDead && nowDead) { @@ -12137,12 +12088,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem " displayId=", unit->getDisplayId(), " appearance extraction failed (VALUES update) — model will not render"); } } - bool isDeadNow = (unit->getHealth() == 0) || - ((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0); - if (isDeadNow && !npcDeathNotified && npcDeathCallback_) { - npcDeathCallback_(block.guid); - npcDeathNotified = true; - } } else if (creatureSpawnCallback_) { float unitScale2 = 1.0f; { @@ -12209,7 +12154,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS); const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); - const uint16_t ufPBytesV = fieldIndex(UF::PLAYER_BYTES); const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2); const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE); const uint16_t ufStatsV[5] = { @@ -12260,38 +12204,15 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) { playerResistances_[key - ufArmor - 1] = static_cast(val); } - else if (ufPBytesV != 0xFFFF && key == ufPBytesV) { - // PLAYER_BYTES changed (barber shop, polymorph, etc.) - // Update the Character struct so inventory preview refreshes - for (auto& ch : characters) { - if (ch.guid == playerGuid) { - ch.appearanceBytes = val; - break; - } - } - if (appearanceChangedCallback_) - appearanceChangedCallback_(); - } else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) { - // Byte 0 (bits 0-7): facial hair / piercings - uint8_t facialHair = static_cast(val & 0xFF); - for (auto& ch : characters) { - if (ch.guid == playerGuid) { - ch.facialFeatures = facialHair; - break; - } - } uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); - LOG_DEBUG("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, - " bankBagSlots=", static_cast(bankBagSlots), - " facial=", static_cast(facialHair)); + LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, + " bankBagSlots=", static_cast(bankBagSlots)); inventory.setPurchasedBankBagSlots(bankBagSlots); // Byte 3 (bits 24-31): REST_STATE // 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY uint8_t restStateByte = static_cast((val >> 24) & 0xFF); isResting_ = (restStateByte != 0); - if (appearanceChangedCallback_) - appearanceChangedCallback_(); } else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) { chosenTitleBit_ = static_cast(val); @@ -12509,10 +12430,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem // Track player-on-transport state from MOVEMENT updates if (block.onTransport) { + setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f)); // Convert transport offset from server → canonical coordinates glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); - glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset); - setPlayerOnTransport(block.transportGuid, canonicalOffset); + playerTransportOffset_ = core::coords::serverToCanonical(serverOffset); if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) { glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); entity->setPosition(composed.x, composed.y, composed.z, oCanonical); @@ -12816,15 +12737,6 @@ void GameHandler::handleMessageChat(network::Packet& packet) { // Track whisper sender for /r command if (data.type == ChatType::WHISPER && !data.senderName.empty()) { lastWhisperSender_ = data.senderName; - - // Auto-reply if AFK or DND - if (afkStatus_ && !data.senderName.empty()) { - std::string reply = afkMessage_.empty() ? "Away from Keyboard" : afkMessage_; - sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); - } else if (dndStatus_ && !data.senderName.empty()) { - std::string reply = dndMessage_.empty() ? "Do Not Disturb" : dndMessage_; - sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); - } } // Trigger chat bubble for SAY/YELL messages from others @@ -15632,12 +15544,6 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint entry.age = 0.0f; entry.isPlayerSource = isPlayerSource; entry.powerType = powerType; - entry.srcGuid = srcGuid; - entry.dstGuid = dstGuid; - // Random horizontal stagger so simultaneous hits don't stack vertically - static std::mt19937 rng(std::random_device{}()); - std::uniform_real_distribution dist(-1.0f, 1.0f); - entry.xSeed = dist(rng); combatText.push_back(entry); // Persistent combat log — use explicit GUIDs if provided, else fall back to @@ -16171,21 +16077,18 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { uint32_t statusId = packet.readUInt32(); // Map BG type IDs to their names (stable across all three expansions) - // BattlemasterList.dbc IDs (3.3.5a) static const std::pair kBgNames[] = { {1, "Alterac Valley"}, {2, "Warsong Gulch"}, {3, "Arathi Basin"}, - {4, "Nagrand Arena"}, - {5, "Blade's Edge Arena"}, - {6, "All Arenas"}, - {7, "Eye of the Storm"}, - {8, "Ruins of Lordaeron"}, + {6, "Eye of the Storm"}, {9, "Strand of the Ancients"}, - {10, "Dalaran Sewers"}, - {11, "Ring of Valor"}, - {30, "Isle of Conquest"}, - {32, "Random Battleground"}, + {11, "Isle of Conquest"}, + {30, "Nagrand Arena"}, + {31, "Blade's Edge Arena"}, + {32, "Dalaran Sewers"}, + {33, "Ring of Valor"}, + {34, "Ruins of Lordaeron"}, }; std::string bgName = "Battleground"; for (const auto& kv : kBgNames) { @@ -16236,7 +16139,6 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { bgQueues_[queueSlot].bgTypeId = bgTypeId; bgQueues_[queueSlot].arenaType = arenaType; bgQueues_[queueSlot].statusId = statusId; - bgQueues_[queueSlot].bgName = bgName; if (statusId == 1) { bgQueues_[queueSlot].avgWaitTimeSec = avgWaitSec; bgQueues_[queueSlot].timeInQueueSec = timeInQueueSec; @@ -16490,7 +16392,6 @@ void GameHandler::handleInstanceDifficulty(network::Packet& packet) { } else { instanceIsHeroic_ = (instanceDifficulty_ == 1); } - inInstance_ = true; LOG_INFO("Instance difficulty: ", instanceDifficulty_, " heroic=", instanceIsHeroic_); // Announce difficulty change to the player (only when it actually changes) @@ -17032,25 +16933,7 @@ void GameHandler::handleArenaTeamQueryResponse(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 4) return; uint32_t teamId = packet.readUInt32(); std::string teamName = packet.readString(); - uint32_t teamType = 0; - if (packet.getSize() - packet.getReadPos() >= 4) - teamType = packet.readUInt32(); - LOG_INFO("Arena team query response: id=", teamId, " name=", teamName, " type=", teamType); - - // Store name and type in matching ArenaTeamStats entry - for (auto& s : arenaTeamStats_) { - if (s.teamId == teamId) { - s.teamName = teamName; - s.teamType = teamType; - return; - } - } - // No stats entry yet — create a placeholder so we can show the name - ArenaTeamStats stub; - stub.teamId = teamId; - stub.teamName = teamName; - stub.teamType = teamType; - arenaTeamStats_.push_back(std::move(stub)); + LOG_INFO("Arena team query response: id=", teamId, " name=", teamName); } void GameHandler::handleArenaTeamRoster(network::Packet& packet) { @@ -17194,29 +17077,18 @@ void GameHandler::handleArenaTeamStats(network::Packet& packet) { stats.seasonWins = packet.readUInt32(); stats.rank = packet.readUInt32(); - // Update or insert for this team (preserve name/type from query response) + // Update or insert for this team for (auto& s : arenaTeamStats_) { if (s.teamId == stats.teamId) { - stats.teamName = std::move(s.teamName); - stats.teamType = s.teamType; - s = std::move(stats); - LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", s.teamId, - " rating=", s.rating, " rank=", s.rank); + s = stats; + LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", stats.teamId, + " rating=", stats.rating, " rank=", stats.rank); return; } } - arenaTeamStats_.push_back(std::move(stats)); - LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", arenaTeamStats_.back().teamId, - " rating=", arenaTeamStats_.back().rating, - " rank=", arenaTeamStats_.back().rank); -} - -void GameHandler::requestArenaTeamRoster(uint32_t teamId) { - if (!socket) return; - network::Packet pkt(wireOpcode(Opcode::CMSG_ARENA_TEAM_ROSTER)); - pkt.writeUInt32(teamId); - socket->send(pkt); - LOG_INFO("Requesting arena team roster for teamId=", teamId); + arenaTeamStats_.push_back(stats); + LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", stats.teamId, + " rating=", stats.rating, " rank=", stats.rank); } void GameHandler::handleArenaError(network::Packet& packet) { @@ -18633,12 +18505,6 @@ void GameHandler::handleCastFailed(network::Packet& packet) { msg.language = ChatLanguage::UNIVERSAL; msg.message = errMsg; addLocalChatMessage(msg); - - // Play error sound for cast failure feedback - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playError(); - } } static audio::SpellSoundManager::MagicSchool schoolMaskToMagicSchool(uint32_t mask) { @@ -19271,13 +19137,6 @@ void GameHandler::confirmTalentWipe() { talentWipeCost_ = 0; } -void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { - if (state != WorldState::IN_WORLD || !socket) return; - auto pkt = AlterAppearancePacket::build(hairStyle, hairColor, facialHair); - socket->send(pkt); - LOG_INFO("sendAlterAppearance: hair=", hairStyle, " color=", hairColor, " facial=", facialHair); -} - // ============================================================ // Phase 4: Group/Party // ============================================================ @@ -19704,28 +19563,6 @@ void GameHandler::queryGuildInfo(uint32_t guildId) { LOG_INFO("Querying guild info: guildId=", guildId); } -static const std::string kEmptyString; - -const std::string& GameHandler::lookupGuildName(uint32_t guildId) { - if (guildId == 0) return kEmptyString; - auto it = guildNameCache_.find(guildId); - if (it != guildNameCache_.end()) return it->second; - // Query the server if we haven't already - if (pendingGuildNameQueries_.insert(guildId).second) { - queryGuildInfo(guildId); - } - return kEmptyString; -} - -uint32_t GameHandler::getEntityGuildId(uint64_t guid) const { - auto entity = entityManager.getEntity(guid); - if (!entity || entity->getType() != ObjectType::PLAYER) return 0; - // PLAYER_GUILDID = UNIT_END + 3 across all expansions - const uint16_t ufUnitEnd = fieldIndex(UF::UNIT_END); - if (ufUnitEnd == 0xFFFF) return 0; - return entity->getField(ufUnitEnd + 3); -} - void GameHandler::createGuild(const std::string& guildName) { if (state != WorldState::IN_WORLD || !socket) return; auto packet = GuildCreatePacket::build(guildName); @@ -19774,118 +19611,6 @@ void GameHandler::handlePetitionShowlist(network::Packet& packet) { LOG_INFO("Petition showlist: cost=", data.cost); } -void GameHandler::handlePetitionQueryResponse(network::Packet& packet) { - // SMSG_PETITION_QUERY_RESPONSE (3.3.5a): - // uint32 petitionEntry, uint64 petitionGuid, string guildName, - // string bodyText (empty), uint32 flags, uint32 minSignatures, - // uint32 maxSignatures, ...plus more fields we can skip - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 12) return; - - /*uint32_t entry =*/ packet.readUInt32(); - uint64_t petGuid = packet.readUInt64(); - std::string guildName = packet.readString(); - /*std::string body =*/ packet.readString(); - - // Update petition info if it matches our current petition - if (petitionInfo_.petitionGuid == petGuid) { - petitionInfo_.guildName = guildName; - } - - LOG_INFO("SMSG_PETITION_QUERY_RESPONSE: guid=", petGuid, " name=", guildName); - packet.setReadPos(packet.getSize()); // skip remaining fields -} - -void GameHandler::handlePetitionShowSignatures(network::Packet& packet) { - // SMSG_PETITION_SHOW_SIGNATURES (3.3.5a): - // uint64 itemGuid (petition item in inventory) - // uint64 ownerGuid - // uint32 petitionGuid (low part / entry) - // uint8 signatureCount - // For each signature: - // uint64 playerGuid - // uint32 unk (always 0) - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 21) return; - - petitionInfo_ = PetitionInfo{}; - petitionInfo_.petitionGuid = packet.readUInt64(); - petitionInfo_.ownerGuid = packet.readUInt64(); - /*uint32_t petEntry =*/ packet.readUInt32(); - uint8_t sigCount = packet.readUInt8(); - - petitionInfo_.signatureCount = sigCount; - petitionInfo_.signatures.reserve(sigCount); - - for (uint8_t i = 0; i < sigCount; ++i) { - if (rem() < 12) break; - PetitionSignature sig; - sig.playerGuid = packet.readUInt64(); - /*uint32_t unk =*/ packet.readUInt32(); - petitionInfo_.signatures.push_back(sig); - } - - petitionInfo_.showUI = true; - LOG_INFO("SMSG_PETITION_SHOW_SIGNATURES: petGuid=", petitionInfo_.petitionGuid, - " owner=", petitionInfo_.ownerGuid, - " sigs=", sigCount); -} - -void GameHandler::handlePetitionSignResults(network::Packet& packet) { - // SMSG_PETITION_SIGN_RESULTS (3.3.5a): - // uint64 petitionGuid, uint64 playerGuid, uint32 result - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 20) return; - - uint64_t petGuid = packet.readUInt64(); - uint64_t playerGuid = packet.readUInt64(); - uint32_t result = packet.readUInt32(); - - switch (result) { - case 0: // PETITION_SIGN_OK - addSystemChatMessage("Petition signed successfully."); - // Increment local count - if (petitionInfo_.petitionGuid == petGuid) { - petitionInfo_.signatureCount++; - PetitionSignature sig; - sig.playerGuid = playerGuid; - petitionInfo_.signatures.push_back(sig); - } - break; - case 1: // PETITION_SIGN_ALREADY_SIGNED - addSystemChatMessage("You have already signed that petition."); - break; - case 2: // PETITION_SIGN_ALREADY_IN_GUILD - addSystemChatMessage("You are already in a guild."); - break; - case 3: // PETITION_SIGN_CANT_SIGN_OWN - addSystemChatMessage("You cannot sign your own petition."); - break; - default: - addSystemChatMessage("Cannot sign petition (error " + std::to_string(result) + ")."); - break; - } - LOG_INFO("SMSG_PETITION_SIGN_RESULTS: pet=", petGuid, " player=", playerGuid, - " result=", result); -} - -void GameHandler::signPetition(uint64_t petitionGuid) { - if (!socket || state != WorldState::IN_WORLD) return; - network::Packet pkt(wireOpcode(Opcode::CMSG_PETITION_SIGN)); - pkt.writeUInt64(petitionGuid); - pkt.writeUInt8(0); // unk - socket->send(pkt); - LOG_INFO("Signing petition: ", petitionGuid); -} - -void GameHandler::turnInPetition(uint64_t petitionGuid) { - if (!socket || state != WorldState::IN_WORLD) return; - network::Packet pkt(wireOpcode(Opcode::CMSG_TURN_IN_PETITION)); - pkt.writeUInt64(petitionGuid); - socket->send(pkt); - LOG_INFO("Turning in petition: ", petitionGuid); -} - void GameHandler::handleTurnInPetitionResults(network::Packet& packet) { uint32_t result = 0; if (!TurnInPetitionResultsParser::parse(packet, result)) return; @@ -19922,30 +19647,18 @@ void GameHandler::handleGuildQueryResponse(network::Packet& packet) { GuildQueryResponseData data; if (!packetParsers_->parseGuildQueryResponse(packet, data)) return; - // Always cache the guild name for nameplate lookups - if (data.guildId != 0 && !data.guildName.empty()) { - guildNameCache_[data.guildId] = data.guildName; - pendingGuildNameQueries_.erase(data.guildId); - } - - // Check if this is the local player's guild - const Character* ch = getActiveCharacter(); - bool isLocalGuild = (ch && ch->hasGuild() && ch->guildId == data.guildId); - - if (isLocalGuild) { - const bool wasUnknown = guildName_.empty(); - guildName_ = data.guildName; - guildQueryData_ = data; - guildRankNames_.clear(); - for (uint32_t i = 0; i < 10; ++i) { - guildRankNames_.push_back(data.rankNames[i]); - } - LOG_INFO("Guild name set to: ", guildName_); - if (wasUnknown && !guildName_.empty()) - addSystemChatMessage("Guild: <" + guildName_ + ">"); - } else { - LOG_INFO("Cached guild name: id=", data.guildId, " name=", data.guildName); + const bool wasUnknown = guildName_.empty(); + guildName_ = data.guildName; + guildQueryData_ = data; + guildRankNames_.clear(); + for (uint32_t i = 0; i < 10; ++i) { + guildRankNames_.push_back(data.rankNames[i]); } + LOG_INFO("Guild name set to: ", guildName_); + // Only announce once — when we first learn our own guild name at login. + // Subsequent queries (e.g. querying other players' guilds) are silent. + if (wasUnknown && !guildName_.empty()) + addSystemChatMessage("Guild: <" + guildName_ + ">"); } void GameHandler::handleGuildEvent(network::Packet& packet) { @@ -20219,17 +19932,6 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { addSystemChatMessage("Too far away."); return; } - // Stop movement before interacting — servers may reject GO use or - // immediately cancel the resulting spell cast if the player is moving. - const uint32_t moveFlags = movementInfo.flags; - const bool isMoving = (moveFlags & 0x00000001u) || // FORWARD - (moveFlags & 0x00000002u) || // BACKWARD - (moveFlags & 0x00000004u) || // STRAFE_LEFT - (moveFlags & 0x00000008u); // STRAFE_RIGHT - if (isMoving) { - movementInfo.flags &= ~0x0000000Fu; // clear directional movement flags - sendMovement(Opcode::MSG_MOVE_STOP); - } if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { movementInfo.orientation = std::atan2(-dy, dx); sendMovement(Opcode::MSG_MOVE_SET_FACING); @@ -20237,12 +19939,6 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } - LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec, - " entry=", goEntry, " type=", goType, - " name='", goName, "' dist=", entity ? std::sqrt( - (entity->getX() - movementInfo.x) * (entity->getX() - movementInfo.x) + - (entity->getY() - movementInfo.y) * (entity->getY() - movementInfo.y) + - (entity->getZ() - movementInfo.z) * (entity->getZ() - movementInfo.z)) : -1.0f); auto packet = GameObjectUsePacket::build(guid); socket->send(packet); lastInteractedGoGuid_ = guid; @@ -21387,40 +21083,6 @@ void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { socket->send(packet); } -void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) { - if (state != WorldState::IN_WORLD || !socket) return; - if (count == 0) return; - - // Find a free slot for the split destination: try backpack first, then bags - int freeBp = inventory.findFreeBackpackSlot(); - if (freeBp >= 0) { - uint8_t dstBag = 0xFF; - uint8_t dstSlot = static_cast(23 + freeBp); - LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot, - ") count=", (int)count, " -> dst(bag=0xFF slot=", (int)dstSlot, ")"); - auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count); - socket->send(packet); - return; - } - // Try equipped bags - for (int b = 0; b < inventory.NUM_BAG_SLOTS; b++) { - int bagSize = inventory.getBagSize(b); - for (int s = 0; s < bagSize; s++) { - if (inventory.getBagSlot(b, s).empty()) { - uint8_t dstBag = static_cast(19 + b); - uint8_t dstSlot = static_cast(s); - LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot, - ") count=", (int)count, " -> dst(bag=", (int)dstBag, - " slot=", (int)dstSlot, ")"); - auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count); - socket->send(packet); - return; - } - } - } - addSystemChatMessage("Cannot split: no free inventory slots."); -} - void GameHandler::useItemBySlot(int backpackIndex) { if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return; const auto& slot = inventory.getBackpackSlot(backpackIndex); @@ -21574,8 +21236,8 @@ void GameHandler::unstuckHearth() { } void GameHandler::handleLootResponse(network::Packet& packet) { - // All expansions use 22 bytes/item (slot+itemId+count+displayInfo+randSuffix+randProp+slotType). - // WotLK adds a quest item list after the regular items. + // Classic 1.12 and TBC 2.4.3 use 14 bytes/item (no randomSuffix/randomProp fields); + // 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; @@ -22560,7 +22222,6 @@ void GameHandler::handleNewWorld(network::Packet& packet) { } currentMapId_ = mapId; - inInstance_ = false; // cleared on map change; re-set if SMSG_INSTANCE_DIFFICULTY follows if (socket) { socket->tracePacketsFor(std::chrono::seconds(12), "new_world"); } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 0d4d09e2..e0dd01f8 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -189,8 +189,11 @@ uint32_t classicWireMoveFlags(uint32_t internalFlags) { // Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate // ============================================================================ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 1) return false; + // Validate minimum packet size for updateFlags byte + if (packet.getReadPos() >= packet.getSize()) { + LOG_WARNING("[Classic] Movement block packet too small (need at least 1 byte for updateFlags)"); + return false; + } // Classic: UpdateFlags is uint8 (same as TBC) uint8_t updateFlags = packet.readUInt8(); @@ -206,9 +209,6 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; if (updateFlags & UPDATEFLAG_LIVING) { - // Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes - if (rem() < 52) return false; - // Movement flags (u32 only — NO extra flags byte in Classic) uint32_t moveFlags = packet.readUInt32(); /*uint32_t time =*/ packet.readUInt32(); @@ -225,29 +225,26 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Transport data (Classic: ONTRANSPORT=0x02000000, no timestamp) if (moveFlags & ClassicMoveFlags::ONTRANSPORT) { - if (rem() < 1) return false; block.onTransport = true; block.transportGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 16) return false; // 4 floats block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); block.transportO = packet.readFloat(); + // Classic: NO transport timestamp (TBC adds u32 timestamp) + // Classic: NO transport seat byte } // Pitch (Classic: only SWIMMING, no FLYING or ONTRANSPORT pitch) if (moveFlags & ClassicMoveFlags::SWIMMING) { - if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time (always present) - if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping (Classic: JUMPING=0x2000, same as TBC) if (moveFlags & ClassicMoveFlags::JUMPING) { - if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -256,12 +253,12 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Spline elevation if (moveFlags & ClassicMoveFlags::SPLINE_ELEVATION) { - if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Speeds (Classic: 6 values — no flight speeds, no pitchRate) - if (rem() < 24) return false; + // TBC added flying_speed + backwards_flying_speed (8 total) + // WotLK added pitchRate (9 total) /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); @@ -274,34 +271,34 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Spline data (Classic: SPLINE_ENABLED=0x00400000) if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) { - if (rem() < 4) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { // FINAL_POINT - if (rem() < 12) return false; /*float finalX =*/ packet.readFloat(); /*float finalY =*/ packet.readFloat(); /*float finalZ =*/ packet.readFloat(); } else if (splineFlags & 0x00020000) { // FINAL_TARGET - if (rem() < 8) return false; /*uint64_t finalTarget =*/ packet.readUInt64(); } else if (splineFlags & 0x00040000) { // FINAL_ANGLE - if (rem() < 4) return false; /*float finalAngle =*/ packet.readFloat(); } - // Classic spline: timePassed, duration, id, pointCount - if (rem() < 16) return false; + // Classic spline: timePassed, duration, id, nodes, finalNode (same as TBC) /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); uint32_t pointCount = packet.readUInt32(); - if (pointCount > 256) return false; - - // points + endPoint (no splineMode in Classic) - if (rem() < static_cast(pointCount) * 12 + 12) return false; + if (pointCount > 256) { + static uint32_t badClassicSplineCount = 0; + ++badClassicSplineCount; + if (badClassicSplineCount <= 5 || (badClassicSplineCount % 100) == 0) { + LOG_WARNING(" [Classic] Spline pointCount=", pointCount, + " exceeds max, capping (occurrence=", badClassicSplineCount, ")"); + } + pointCount = 0; + } for (uint32_t i = 0; i < pointCount; i++) { /*float px =*/ packet.readFloat(); /*float py =*/ packet.readFloat(); @@ -315,7 +312,6 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo } } else if (updateFlags & UPDATEFLAG_HAS_POSITION) { - if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -327,25 +323,21 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // High GUID if (updateFlags & UPDATEFLAG_HIGHGUID) { - if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } // ALL/SELF extra uint32 if (updateFlags & UPDATEFLAG_ALL) { - if (rem() < 4) return false; /*uint32_t unkAll =*/ packet.readUInt32(); } // Current melee target as packed guid if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { - if (rem() < 1) return false; /*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet); } // Transport progress / world time if (updateFlags & UPDATEFLAG_TRANSPORT) { - if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } @@ -1926,9 +1918,6 @@ namespace TurtleMoveFlags { } bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 1) return false; - uint8_t updateFlags = packet.readUInt8(); block.updateFlags = static_cast(updateFlags); @@ -1942,8 +1931,6 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; if (updateFlags & UPDATEFLAG_LIVING) { - // Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes - if (rem() < 52) return false; size_t livingStart = packet.getReadPos(); uint32_t moveFlags = packet.readUInt32(); @@ -1962,10 +1949,8 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // Transport — Classic flag position 0x02000000 if (moveFlags & TurtleMoveFlags::ONTRANSPORT) { - if (rem() < 1) return false; // PackedGuid mask byte block.onTransport = true; block.transportGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 20) return false; // 4 floats + u32 timestamp block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); @@ -1975,17 +1960,14 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // Pitch (swimming only, Classic-style) if (moveFlags & TurtleMoveFlags::SWIMMING) { - if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time (always present) - if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jump data if (moveFlags & TurtleMoveFlags::JUMPING) { - if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -1994,12 +1976,10 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // Spline elevation if (moveFlags & TurtleMoveFlags::SPLINE_ELEVATION) { - if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Turtle: 6 speeds (same as Classic — no flight speeds) - if (rem() < 24) return false; // 6 × float float walkSpeed = packet.readFloat(); float runSpeed = packet.readFloat(); float runBackSpeed = packet.readFloat(); @@ -2017,23 +1997,17 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc bool hasSpline = (moveFlags & TurtleMoveFlags::SPLINE_CLASSIC) || (moveFlags & TurtleMoveFlags::SPLINE_TBC); if (hasSpline) { - if (rem() < 4) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" [Turtle] Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { - if (rem() < 12) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } else if (splineFlags & 0x00020000) { - if (rem() < 8) return false; packet.readUInt64(); } else if (splineFlags & 0x00040000) { - if (rem() < 4) return false; packet.readFloat(); } - // timePassed + duration + splineId + pointCount = 16 bytes - if (rem() < 16) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); @@ -2044,12 +2018,10 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc ++badTurtleSplineCount; if (badTurtleSplineCount <= 5 || (badTurtleSplineCount % 100) == 0) { LOG_WARNING(" [Turtle] Spline pointCount=", pointCount, - " exceeds max (occurrence=", badTurtleSplineCount, ")"); + " exceeds max, capping (occurrence=", badTurtleSplineCount, ")"); } - return false; + pointCount = 0; } - // points + endPoint - if (rem() < static_cast(pointCount) * 12 + 12) return false; for (uint32_t i = 0; i < pointCount; i++) { packet.readFloat(); packet.readFloat(); packet.readFloat(); } @@ -2062,7 +2034,6 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc " bytes, readPos now=", packet.getReadPos()); } else if (updateFlags & UPDATEFLAG_HAS_POSITION) { - if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -2074,22 +2045,18 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // High GUID — 1×u32 if (updateFlags & UPDATEFLAG_HIGHGUID) { - if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } if (updateFlags & UPDATEFLAG_ALL) { - if (rem() < 4) return false; /*uint32_t unkAll =*/ packet.readUInt32(); } if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { - if (rem() < 1) return false; /*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet); } if (updateFlags & UPDATEFLAG_TRANSPORT) { - if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } @@ -2218,10 +2185,12 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec return this->TbcPacketParsers::parseMovementBlock(p, b); }, "tbc"); } - // NOTE: Do NOT fall back to WotLK parseMovementBlock here. - // WotLK uses uint16 updateFlags and 9 speeds vs Classic's uint8 - // and 6 speeds. A false-positive WotLK parse consumes wrong bytes, - // corrupting subsequent update fields and losing NPC data. + if (!ok) { + ok = parseMovementVariant( + [](network::Packet& p, UpdateBlock& b) { + return UpdateObjectParser::parseMovementBlock(p, b); + }, "wotlk"); + } break; case UpdateType::OUT_OF_RANGE_OBJECTS: case UpdateType::NEAR_OBJECTS: diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 9d68879f..c1397460 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -30,8 +30,11 @@ namespace TbcMoveFlags { // - Flag 0x08 (HIGH_GUID) reads 2 u32s (Classic: 1 u32) // ============================================================================ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 1) return false; + // Validate minimum packet size for updateFlags byte + if (packet.getReadPos() >= packet.getSize()) { + LOG_WARNING("[TBC] Movement block packet too small (need at least 1 byte for updateFlags)"); + return false; + } // TBC 2.4.3: UpdateFlags is uint8 (1 byte) uint8_t updateFlags = packet.readUInt8(); @@ -55,9 +58,6 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& const uint8_t UPDATEFLAG_HIGHGUID = 0x10; if (updateFlags & UPDATEFLAG_LIVING) { - // Minimum: moveFlags(4)+moveFlags2(1)+time(4)+position(16)+fallTime(4)+speeds(32) = 61 - if (rem() < 61) return false; - // Full movement block for living units uint32_t moveFlags = packet.readUInt32(); uint8_t moveFlags2 = packet.readUInt8(); // TBC: uint8, not uint16 @@ -76,33 +76,29 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Transport data if (moveFlags & TbcMoveFlags::ON_TRANSPORT) { - if (rem() < 1) return false; block.onTransport = true; block.transportGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 20) return false; // 4 floats + 1 uint32 block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); block.transportO = packet.readFloat(); /*uint32_t tTime =*/ packet.readUInt32(); + // TBC: NO transport seat byte + // TBC: NO interpolated movement check } // Pitch: SWIMMING, or else ONTRANSPORT (TBC-specific secondary pitch) if (moveFlags & TbcMoveFlags::SWIMMING) { - if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } else if (moveFlags & TbcMoveFlags::ONTRANSPORT) { - if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time (always present) - if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping (TBC: JUMPING=0x2000, WotLK: FALLING=0x1000) if (moveFlags & TbcMoveFlags::JUMPING) { - if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -111,12 +107,11 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Spline elevation (TBC: 0x02000000, WotLK: 0x04000000) if (moveFlags & TbcMoveFlags::SPLINE_ELEVATION) { - if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Speeds (TBC: 8 values — walk, run, runBack, swim, fly, flyBack, swimBack, turn) - if (rem() < 32) return false; + // WotLK adds pitchRate (9 total) /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); @@ -131,47 +126,49 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Spline data (TBC/WotLK: SPLINE_ENABLED = 0x08000000) if (moveFlags & TbcMoveFlags::SPLINE_ENABLED) { - if (rem() < 4) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" [TBC] Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { // FINAL_POINT - if (rem() < 12) return false; /*float finalX =*/ packet.readFloat(); /*float finalY =*/ packet.readFloat(); /*float finalZ =*/ packet.readFloat(); } else if (splineFlags & 0x00020000) { // FINAL_TARGET - if (rem() < 8) return false; /*uint64_t finalTarget =*/ packet.readUInt64(); } else if (splineFlags & 0x00040000) { // FINAL_ANGLE - if (rem() < 4) return false; /*float finalAngle =*/ packet.readFloat(); } - // TBC spline: timePassed, duration, id, pointCount - if (rem() < 16) return false; + // TBC spline: timePassed, duration, id, nodes, finalNode + // (no durationMod, durationModNext, verticalAccel, effectStartTime, splineMode) /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); uint32_t pointCount = packet.readUInt32(); - if (pointCount > 256) return false; - - // points + endPoint (no splineMode in TBC) - if (rem() < static_cast(pointCount) * 12 + 12) return false; + if (pointCount > 256) { + static uint32_t badTbcSplineCount = 0; + ++badTbcSplineCount; + if (badTbcSplineCount <= 5 || (badTbcSplineCount % 100) == 0) { + LOG_WARNING(" [TBC] Spline pointCount=", pointCount, + " exceeds max, capping (occurrence=", badTbcSplineCount, ")"); + } + pointCount = 0; + } for (uint32_t i = 0; i < pointCount; i++) { /*float px =*/ packet.readFloat(); /*float py =*/ packet.readFloat(); /*float pz =*/ packet.readFloat(); } + // TBC: NO splineMode byte (WotLK adds it) /*float endPointX =*/ packet.readFloat(); /*float endPointY =*/ packet.readFloat(); /*float endPointZ =*/ packet.readFloat(); } } else if (updateFlags & UPDATEFLAG_HAS_POSITION) { - if (rem() < 16) return false; + // TBC: Simple stationary position (same as WotLK STATIONARY) block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -180,29 +177,29 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& LOG_DEBUG(" [TBC] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); } + // TBC: No UPDATEFLAG_POSITION (0x0100) code path // Target GUID if (updateFlags & UPDATEFLAG_HAS_TARGET) { - if (rem() < 1) return false; /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); } // Transport time if (updateFlags & UPDATEFLAG_TRANSPORT) { - if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } - // LOWGUID (0x08) — TBC has 2 u32s, Classic has 1 u32 + // TBC: No VEHICLE flag (WotLK 0x0080) + // TBC: No ROTATION flag (WotLK 0x0200) + + // HIGH_GUID (0x08) — TBC has 2 u32s, Classic has 1 u32 if (updateFlags & UPDATEFLAG_LOWGUID) { - if (rem() < 8) return false; /*uint32_t unknown0 =*/ packet.readUInt32(); /*uint32_t unknown1 =*/ packet.readUInt32(); } - // HIGHGUID (0x10) + // ALL (0x10) if (updateFlags & UPDATEFLAG_HIGHGUID) { - if (rem() < 4) return false; /*uint32_t unknown2 =*/ packet.readUInt32(); } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index e20c2d09..e6f6d872 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -913,9 +913,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // 1. UpdateFlags (1 byte, sometimes 2) // 2. Movement data depends on update flags - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 2) return false; - // Update flags (3.3.5a uses 2 bytes for flags) uint16_t updateFlags = packet.readUInt16(); block.updateFlags = updateFlags; @@ -960,9 +957,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock const uint16_t UPDATEFLAG_HIGHGUID = 0x0010; if (updateFlags & UPDATEFLAG_LIVING) { - // Minimum: moveFlags(4)+moveFlags2(2)+time(4)+position(16)+fallTime(4)+speeds(36) = 66 - if (rem() < 66) return false; - // Full movement block for living units uint32_t moveFlags = packet.readUInt32(); uint16_t moveFlags2 = packet.readUInt16(); @@ -980,10 +974,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Transport data (if on transport) if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT - if (rem() < 1) return false; block.onTransport = true; block.transportGuid = readPackedGuid(packet); - if (rem() < 21) return false; // 4 floats + uint32 + uint8 block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); @@ -995,7 +987,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock " offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")"); if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT - if (rem() < 4) return false; /*uint32_t tTime2 =*/ packet.readUInt32(); } } @@ -1014,17 +1005,14 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock if ((moveFlags & 0x00200000) /* SWIMMING */ || (moveFlags & 0x01000000) /* FLYING */ || (moveFlags2 & 0x0010) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) { - if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time - if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING - if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -1033,12 +1021,10 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Spline elevation if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION - if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } - // Speeds (9 values in WotLK: walk/run/runBack/swim/swimBack/flight/flightBack/turn/pitch) - if (rem() < 36) return false; + // Speeds (7 speed values) /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); @@ -1072,60 +1058,46 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock /*float finalAngle =*/ packet.readFloat(); } - // Spline data layout varies by expansion: - // Classic/Vanilla: timePassed(4)+duration(4)+splineId(4)+pointCount(4)+points+mode(1)+endPoint(12) - // WotLK: timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4) - // +[ANIMATION(5)]+[PARABOLIC(8)]+pointCount(4)+points+mode(1)+endPoint(12) - // Since the parser has no expansion context, auto-detect by trying Classic first. + // Legacy UPDATE_OBJECT spline layout used by many servers: + // timePassed, duration, splineId, durationMod, durationModNext, + // [ANIMATION: animType(1)+animTime(4) if SPLINEFLAG_ANIMATION(0x00400000)], + // verticalAccel, effectStartTime, pointCount, points, splineMode, endPoint. const size_t legacyStart = packet.getReadPos(); - if (!bytesAvailable(16)) return false; // minimum: 12 common + 4 pointCount + if (!bytesAvailable(12 + 8 + 8 + 4)) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); - const size_t afterSplineId = packet.getReadPos(); - - // Helper: try to parse uncompressed spline points from current read position. - auto tryParseUncompressedSpline = [&](const char* tag) -> bool { - if (!bytesAvailable(4)) return false; - uint32_t pc = packet.readUInt32(); - if (pc > 256) return false; - size_t needed = static_cast(pc) * 12ull + 13ull; - if (!bytesAvailable(needed)) return false; - for (uint32_t i = 0; i < pc; i++) { - packet.readFloat(); packet.readFloat(); packet.readFloat(); - } - packet.readUInt8(); // splineMode - packet.readFloat(); packet.readFloat(); packet.readFloat(); // endPoint - LOG_DEBUG(" Spline pointCount=", pc, " (", tag, ")"); - return true; - }; - - // --- Try 1: Classic format (pointCount immediately after splineId) --- - bool splineParsed = tryParseUncompressedSpline("classic"); - - // --- Try 2: WotLK format (durationMod+durationModNext+conditional+pointCount) --- - if (!splineParsed) { - packet.setReadPos(afterSplineId); - bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext - if (wotlkOk) { - /*float durationMod =*/ packet.readFloat(); - /*float durationModNext =*/ packet.readFloat(); - if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION - if (!bytesAvailable(5)) { wotlkOk = false; } - else { packet.readUInt8(); packet.readUInt32(); } - } - } - if (wotlkOk && (splineFlags & 0x00000800)) { // SPLINEFLAG_PARABOLIC - if (!bytesAvailable(8)) { wotlkOk = false; } - else { packet.readFloat(); packet.readUInt32(); } - } - if (wotlkOk) { - splineParsed = tryParseUncompressedSpline("wotlk"); - } + /*float durationMod =*/ packet.readFloat(); + /*float durationModNext =*/ packet.readFloat(); + // Animation flag inserts 5 bytes (uint8 type + int32 time) before verticalAccel + if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION + if (!bytesAvailable(5)) return false; + packet.readUInt8(); // animationType + packet.readUInt32(); // animTime } + /*float verticalAccel =*/ packet.readFloat(); + /*uint32_t effectStartTime =*/ packet.readUInt32(); + uint32_t pointCount = packet.readUInt32(); - // --- Try 3: Compact layout (compressed points) as final recovery --- - if (!splineParsed) { + const size_t remainingAfterCount = packet.getSize() - packet.getReadPos(); + const bool legacyCountLooksValid = (pointCount <= 256); + const size_t legacyPointsBytes = static_cast(pointCount) * 12ull; + const bool legacyPayloadFits = (legacyPointsBytes + 13ull) <= remainingAfterCount; + + if (legacyCountLooksValid && legacyPayloadFits) { + for (uint32_t i = 0; i < pointCount; i++) { + /*float px =*/ packet.readFloat(); + /*float py =*/ packet.readFloat(); + /*float pz =*/ packet.readFloat(); + } + /*uint8_t splineMode =*/ packet.readUInt8(); + /*float endPointX =*/ packet.readFloat(); + /*float endPointY =*/ packet.readFloat(); + /*float endPointZ =*/ packet.readFloat(); + LOG_DEBUG(" Spline pointCount=", pointCount, " (legacy)"); + } else { + // Legacy pointCount looks invalid; try compact WotLK layout as recovery. + // This keeps malformed/variant packets from desyncing the whole update block. packet.setReadPos(legacyStart); const size_t afterFinalFacingPos = packet.getReadPos(); if (splineFlags & 0x00400000) { // Animation @@ -1146,7 +1118,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock static uint32_t badSplineCount = 0; ++badSplineCount; if (badSplineCount <= 5 || (badSplineCount % 100) == 0) { - LOG_WARNING(" Spline invalid (classic+wotlk+compact) at readPos=", + LOG_WARNING(" Spline pointCount=", pointCount, + " invalid (legacy+compact) at readPos=", afterFinalFacingPos, "/", packet.getSize(), ", occurrence=", badSplineCount); } @@ -1166,14 +1139,12 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock if (!bytesAvailable(compactPayloadBytes)) return false; packet.setReadPos(packet.getReadPos() + compactPayloadBytes); } - } // end compact fallback + } // end else (compact fallback) } } else if (updateFlags & UPDATEFLAG_POSITION) { // Transport position update (UPDATEFLAG_POSITION = 0x0100) - if (rem() < 1) return false; uint64_t transportGuid = readPackedGuid(packet); - if (rem() < 32) return false; // 8 floats block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -1202,7 +1173,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock } } else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) { - if (rem() < 16) return false; + // Simple stationary position (4 floats) block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -1214,38 +1185,32 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Target GUID (for units with target) if (updateFlags & UPDATEFLAG_HAS_TARGET) { - if (rem() < 1) return false; /*uint64_t targetGuid =*/ readPackedGuid(packet); } // Transport time if (updateFlags & UPDATEFLAG_TRANSPORT) { - if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } // Vehicle if (updateFlags & UPDATEFLAG_VEHICLE) { - if (rem() < 8) return false; /*uint32_t vehicleId =*/ packet.readUInt32(); /*float vehicleOrientation =*/ packet.readFloat(); } // Rotation (GameObjects) if (updateFlags & UPDATEFLAG_ROTATION) { - if (rem() < 8) return false; /*int64_t rotation =*/ packet.readUInt64(); } // Low GUID if (updateFlags & UPDATEFLAG_LOWGUID) { - if (rem() < 4) return false; /*uint32_t lowGuid =*/ packet.readUInt32(); } // High GUID if (updateFlags & UPDATEFLAG_HIGHGUID) { - if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } @@ -1255,8 +1220,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) { size_t startPos = packet.getReadPos(); - if (packet.getReadPos() >= packet.getSize()) return false; - // Read number of blocks (each block is 32 fields = 32 bits) uint8_t blockCount = packet.readUInt8(); @@ -1344,8 +1307,6 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& } bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) { - if (packet.getReadPos() >= packet.getSize()) return false; - // Read update type uint8_t updateTypeVal = packet.readUInt8(); block.updateType = static_cast(updateTypeVal); @@ -1355,7 +1316,6 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& switch (block.updateType) { case UpdateType::VALUES: { // Partial update - changed fields only - if (packet.getReadPos() >= packet.getSize()) return false; block.guid = readPackedGuid(packet); LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec); @@ -1364,7 +1324,6 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::MOVEMENT: { // Movement update - if (packet.getReadPos() + 8 > packet.getSize()) return false; block.guid = packet.readUInt64(); LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec); @@ -1374,12 +1333,10 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::CREATE_OBJECT: case UpdateType::CREATE_OBJECT2: { // Create new object with full data - if (packet.getReadPos() >= packet.getSize()) return false; block.guid = readPackedGuid(packet); LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec); // Read object type - if (packet.getReadPos() >= packet.getSize()) return false; uint8_t objectTypeVal = packet.readUInt8(); block.objectType = static_cast(objectTypeVal); LOG_DEBUG(" Object type: ", (int)objectTypeVal); @@ -3252,11 +3209,12 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { if (pointCount == 0) return true; + // Cap pointCount to prevent excessive iteration from malformed packets. constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, ")"); - return false; + " (guid=0x", std::hex, data.guid, std::dec, "), capping"); + pointCount = kMaxSplinePoints; } // Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed). @@ -3907,13 +3865,13 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.hitTargets.reserve(storedHitLimit); for (uint16_t i = 0; i < rawHitCount; ++i) { - // WotLK 3.3.5a hit targets are full uint64 GUIDs (not PackedGuid). - if (packet.getSize() - packet.getReadPos() < 8) { + // WotLK hit targets are packed GUIDs, like the caster and miss targets. + if (!hasFullPackedGuid(packet)) { LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount); truncatedTargets = true; break; } - const uint64_t targetGuid = packet.readUInt64(); + const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); if (i < storedHitLimit) { data.hitTargets.push_back(targetGuid); } @@ -3931,27 +3889,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { return false; } - const size_t missCountPos = packet.getReadPos(); const uint8_t rawMissCount = packet.readUInt8(); - if (rawMissCount > 20) { - // Likely offset error — dump context bytes for diagnostics. - const auto& raw = packet.getData(); - std::string hexCtx; - size_t dumpStart = (missCountPos >= 8) ? missCountPos - 8 : startPos; - size_t dumpEnd = std::min(missCountPos + 16, raw.size()); - for (size_t i = dumpStart; i < dumpEnd; ++i) { - char buf[4]; - std::snprintf(buf, sizeof(buf), "%02x ", raw[i]); - hexCtx += buf; - if (i == missCountPos - 1) hexCtx += "["; - if (i == missCountPos) hexCtx += "] "; - } - LOG_WARNING("Spell go: suspect missCount=", (int)rawMissCount, - " spell=", data.spellId, " hits=", (int)data.hitCount, - " castFlags=0x", std::hex, data.castFlags, std::dec, - " missCountPos=", missCountPos, " pktSize=", packet.getSize(), - " ctx=", hexCtx); - } if (rawMissCount > 128) { LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount, ") spell=", data.spellId, " hits=", (int)data.hitCount, @@ -3961,16 +3899,22 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.missTargets.reserve(storedMissLimit); for (uint16_t i = 0; i < rawMissCount; ++i) { - // WotLK 3.3.5a miss targets are full uint64 GUIDs + uint8 missType. + // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte). // REFLECT additionally appends uint8 reflectResult. - if (packet.getSize() - packet.getReadPos() < 9) { // 8 GUID + 1 missType + if (!hasFullPackedGuid(packet)) { LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount, " spell=", data.spellId, " hits=", (int)data.hitCount); truncatedTargets = true; break; } SpellGoMissEntry m; - m.targetGuid = packet.readUInt64(); + m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK + if (packet.getSize() - packet.getReadPos() < 1) { + LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount, + " spell=", data.spellId); + truncatedTargets = true; + break; + } m.missType = packet.readUInt8(); if (m.missType == 11) { // SPELL_MISS_REFLECT if (packet.getSize() - packet.getReadPos() < 1) { @@ -4358,17 +4302,6 @@ network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t s return packet; } -network::Packet SplitItemPacket::build(uint8_t srcBag, uint8_t srcSlot, - uint8_t dstBag, uint8_t dstSlot, uint8_t count) { - network::Packet packet(wireOpcode(Opcode::CMSG_SPLIT_ITEM)); - packet.writeUInt8(srcBag); - packet.writeUInt8(srcSlot); - packet.writeUInt8(dstBag); - packet.writeUInt8(dstSlot); - packet.writeUInt8(count); - return packet; -} - network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) { network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM)); packet.writeUInt8(srcSlot); @@ -5878,14 +5811,5 @@ network::Packet SetTitlePacket::build(int32_t titleBit) { return p; } -network::Packet AlterAppearancePacket::build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { - // CMSG_ALTER_APPEARANCE: uint32 hairStyle + uint32 hairColor + uint32 facialHair - network::Packet p(wireOpcode(Opcode::CMSG_ALTER_APPEARANCE)); - p.writeUInt32(hairStyle); - p.writeUInt32(hairColor); - p.writeUInt32(facialHair); - return p; -} - } // namespace game } // namespace wowee diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 53be1a25..d16fa26c 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -273,9 +273,8 @@ void CameraController::update(float deltaTime) { keyW = keyS = keyA = keyD = keyQ = keyE = nowJump = false; } - // Tilde or NumLock toggles auto-run; any forward/backward key cancels it - bool tildeDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_GRAVE) || - input.isKeyPressed(SDL_SCANCODE_NUMLOCKCLEAR)); + // Tilde toggles auto-run; any forward/backward key cancels it + bool tildeDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_GRAVE); if (tildeDown && !tildeWasDown) { autoRunning = !autoRunning; } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 40ffd4b1..390ee2c5 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1753,7 +1753,6 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, return 0; } const auto& mdlRef = modelIt->second; - modelUnusedSince_.erase(modelId); // Deduplicate: skip if same model already at nearly the same position. // Uses hash map for O(1) lookup instead of O(N) scan. @@ -1865,7 +1864,6 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& LOG_WARNING("Cannot create instance: model ", modelId, " not loaded"); return 0; } - modelUnusedSince_.erase(modelId); // Deduplicate: O(1) hash lookup { @@ -4278,28 +4276,11 @@ void M2Renderer::cleanupUnusedModels() { usedModelIds.insert(instance.modelId); } - const auto now = std::chrono::steady_clock::now(); - constexpr auto kGracePeriod = std::chrono::seconds(60); - - // Find models with no instances that have exceeded the grace period. - // Models that just lost their last instance get tracked but not evicted - // immediately — this prevents thrashing when GO models are briefly - // instance-free between despawn and respawn cycles. + // Find and remove models with no instances std::vector toRemove; for (const auto& [id, model] : models) { - if (usedModelIds.find(id) != usedModelIds.end()) { - // Model still in use — clear any pending unused timestamp - modelUnusedSince_.erase(id); - continue; - } - auto unusedIt = modelUnusedSince_.find(id); - if (unusedIt == modelUnusedSince_.end()) { - // First cycle with no instances — start the grace timer - modelUnusedSince_[id] = now; - } else if (now - unusedIt->second >= kGracePeriod) { - // Grace period expired — mark for removal + if (usedModelIds.find(id) == usedModelIds.end()) { toRemove.push_back(id); - modelUnusedSince_.erase(unusedIt); } } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 2d942c23..4da8bad7 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -67,10 +67,6 @@ #include #include #include -#include - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" #include #include #include @@ -2578,101 +2574,6 @@ void Renderer::cancelEmote() { emoteLoop = false; } -bool Renderer::captureScreenshot(const std::string& outputPath) { - if (!vkCtx) return false; - - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - VkExtent2D extent = vkCtx->getSwapchainExtent(); - const auto& images = vkCtx->getSwapchainImages(); - - if (images.empty() || currentImageIndex >= images.size()) return false; - - VkImage srcImage = images[currentImageIndex]; - uint32_t w = extent.width; - uint32_t h = extent.height; - VkDeviceSize bufSize = static_cast(w) * h * 4; - - // Stall GPU so the swapchain image is idle - vkDeviceWaitIdle(device); - - // Create staging buffer - VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - bufInfo.size = bufSize; - bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - - VmaAllocationCreateInfo allocCI{}; - allocCI.usage = VMA_MEMORY_USAGE_CPU_ONLY; - - VkBuffer stagingBuf = VK_NULL_HANDLE; - VmaAllocation stagingAlloc = VK_NULL_HANDLE; - if (vmaCreateBuffer(alloc, &bufInfo, &allocCI, &stagingBuf, &stagingAlloc, nullptr) != VK_SUCCESS) { - LOG_WARNING("Screenshot: failed to create staging buffer"); - return false; - } - - // Record copy commands - VkCommandBuffer cmd = vkCtx->beginSingleTimeCommands(); - - // Transition swapchain image: PRESENT_SRC → TRANSFER_SRC - VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - toTransfer.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; - toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - toTransfer.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - toTransfer.image = srcImage; - toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - vkCmdPipelineBarrier(cmd, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &toTransfer); - - // Copy image to buffer - VkBufferImageCopy region{}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageExtent = {w, h, 1}; - vkCmdCopyImageToBuffer(cmd, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - stagingBuf, 1, ®ion); - - // Transition back: TRANSFER_SRC → PRESENT_SRC - VkImageMemoryBarrier toPresent = toTransfer; - toPresent.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - toPresent.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; - toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - vkCmdPipelineBarrier(cmd, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - 0, 0, nullptr, 0, nullptr, 1, &toPresent); - - vkCtx->endSingleTimeCommands(cmd); - - // Map and convert BGRA → RGBA - void* mapped = nullptr; - vmaMapMemory(alloc, stagingAlloc, &mapped); - auto* pixels = static_cast(mapped); - for (uint32_t i = 0; i < w * h; ++i) { - std::swap(pixels[i * 4 + 0], pixels[i * 4 + 2]); // B ↔ R - } - - // Ensure output directory exists - std::filesystem::path outPath(outputPath); - if (outPath.has_parent_path()) - std::filesystem::create_directories(outPath.parent_path()); - - int ok = stbi_write_png(outputPath.c_str(), - static_cast(w), static_cast(h), - 4, pixels, static_cast(w * 4)); - - vmaUnmapMemory(alloc, stagingAlloc); - vmaDestroyBuffer(alloc, stagingBuf, stagingAlloc); - - if (ok) { - LOG_INFO("Screenshot saved: ", outputPath); - } else { - LOG_WARNING("Screenshot: stbi_write_png failed for ", outputPath); - } - return ok != 0; -} - void Renderer::triggerLevelUpEffect(const glm::vec3& position) { if (!levelUpEffect) return; diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 777285cf..2f4b83cc 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -206,8 +206,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { } } } - // Login screen music - if (renderer) { + // Login screen music disabled + if (false && renderer) { auto* music = renderer->getMusicManager(); if (music) { if (!loginMusicVolumeAdjusted_) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 811ef73e..f4f8cd11 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -401,14 +401,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { ghostStateCallbackSet_ = true; } - // Set up appearance-changed callback to refresh inventory preview (barber shop, etc.) - if (!appearanceCallbackSet_) { - gameHandler.setAppearanceChangedCallback([this]() { - inventoryScreenCharGuid_ = 0; // force preview re-sync on next frame - }); - appearanceCallbackSet_ = true; - } - // Set up UI error frame callback (once) if (!uiErrorCallbackSet_) { gameHandler.setUIErrorCallback([this](const std::string& msg) { @@ -741,7 +733,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderQuestOfferRewardWindow(gameHandler); renderVendorWindow(gameHandler); renderTrainerWindow(gameHandler); - renderBarberShopWindow(gameHandler); renderStableWindow(gameHandler); renderTaxiWindow(gameHandler); renderMailWindow(gameHandler); @@ -2760,6 +2751,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) { if (showSettingsWindow) { + // Close settings window if open showSettingsWindow = false; } else if (showEscapeMenu) { showEscapeMenu = false; @@ -2770,32 +2762,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { gameHandler.closeLoot(); } else if (gameHandler.isGossipWindowOpen()) { gameHandler.closeGossip(); - } else if (gameHandler.isVendorWindowOpen()) { - gameHandler.closeVendor(); - } else if (gameHandler.isBarberShopOpen()) { - gameHandler.closeBarberShop(); - } else if (gameHandler.isBankOpen()) { - gameHandler.closeBank(); - } else if (gameHandler.isTrainerWindowOpen()) { - gameHandler.closeTrainer(); - } else if (showWhoWindow_) { - showWhoWindow_ = false; - } else if (showCombatLog_) { - showCombatLog_ = false; - } else if (showSocialFrame_) { - showSocialFrame_ = false; - } else if (talentScreen.isOpen()) { - talentScreen.setOpen(false); - } else if (spellbookScreen.isOpen()) { - spellbookScreen.setOpen(false); - } else if (questLogScreen.isOpen()) { - questLogScreen.setOpen(false); - } else if (inventoryScreen.isCharacterOpen()) { - inventoryScreen.toggleCharacter(); - } else if (inventoryScreen.isOpen()) { - inventoryScreen.setOpen(false); - } else if (showWorldMap_) { - showWorldMap_ = false; } else { showEscapeMenu = true; } @@ -2816,10 +2782,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) { - if (ImGui::GetIO().KeyShift) - showFriendlyNameplates_ = !showFriendlyNameplates_; - else - showNameplates_ = !showNameplates_; + showNameplates_ = !showNameplates_; } if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) { @@ -2846,11 +2809,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { showTitlesWindow_ = !showTitlesWindow_; } - // Screenshot (PrintScreen key) - if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) { - takeScreenshot(gameHandler); - } - // Action bar keys (1-9, 0, -, =) static const SDL_Scancode actionBarKeys[] = { SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, @@ -2910,7 +2868,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } - // Cursor affordance: show hand cursor over interactable entities. + // Cursor affordance: show hand cursor over interactable game objects. if (!io.WantCaptureMouse) { auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; @@ -2921,21 +2879,17 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { float screenH = static_cast(window->getHeight()); rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH); float closestT = 1e30f; - bool hoverInteractable = false; + bool hoverInteractableGo = false; for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { - bool isGo = (entity->getType() == game::ObjectType::GAMEOBJECT); - bool isUnit = (entity->getType() == game::ObjectType::UNIT); - bool isPlayer = (entity->getType() == game::ObjectType::PLAYER); - if (!isGo && !isUnit && !isPlayer) continue; - if (guid == gameHandler.getPlayerGuid()) continue; // skip self + if (entity->getType() != game::ObjectType::GAMEOBJECT) continue; glm::vec3 hitCenter; float hitRadius = 0.0f; bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius); if (!hasBounds) { - hitRadius = isGo ? 2.5f : 1.8f; + hitRadius = 2.5f; hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); - hitCenter.z += isGo ? 1.2f : 1.0f; + hitCenter.z += 1.2f; } else { hitRadius = std::max(hitRadius * 1.1f, 0.8f); } @@ -2943,10 +2897,10 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { float hitT; if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) { closestT = hitT; - hoverInteractable = true; + hoverInteractableGo = true; } } - if (hoverInteractable) { + if (hoverInteractableGo) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } } @@ -3341,15 +3295,6 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), ""); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Do not disturb — /dnd to cancel"); } - if (auto* ren = core::Application::getInstance().getRenderer()) { - if (auto* cam = ren->getCameraController()) { - if (cam->isAutoRunning()) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "[Auto-Run]"); - if (ImGui::IsItemHovered()) ImGui::SetTooltip("Auto-running — press ` or NumLock to stop"); - } - } - } if (inCombatConfirmed && !isDead) { float combatPulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); ImGui::SameLine(); @@ -4196,39 +4141,6 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0)); ImGui::PopStyleColor(4); - // Right-click context menu on target frame - if (ImGui::BeginPopupContextItem("##TargetFrameCtx")) { - ImGui::TextDisabled("%s", name.c_str()); - ImGui::Separator(); - if (ImGui::MenuItem("Set Focus")) - gameHandler.setFocus(target->getGuid()); - if (target->getType() == game::ObjectType::PLAYER) { - ImGui::Separator(); - if (ImGui::MenuItem("Whisper")) { - selectedChatType = 4; - strncpy(whisperTargetBuffer, name.c_str(), sizeof(whisperTargetBuffer) - 1); - whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; - refocusChatInput = true; - } - if (ImGui::MenuItem("Invite to Group")) - gameHandler.inviteToGroup(name); - if (ImGui::MenuItem("Trade")) - gameHandler.initiateTrade(target->getGuid()); - if (ImGui::MenuItem("Duel")) - gameHandler.proposeDuel(target->getGuid()); - if (ImGui::MenuItem("Inspect")) { - gameHandler.inspectTarget(); - showInspectWindow_ = true; - } - ImGui::Separator(); - if (ImGui::MenuItem("Add Friend")) - gameHandler.addFriend(name); - if (ImGui::MenuItem("Ignore")) - gameHandler.addIgnore(name); - } - ImGui::EndPopup(); - } - // Group leader crown — golden ♛ when the targeted player is the party/raid leader if (gameHandler.isInGroup() && target->getType() == game::ObjectType::PLAYER) { if (gameHandler.getPartyData().leaderGuid == target->getGuid()) { @@ -4270,17 +4182,6 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { } } - // Player guild name (e.g. "") — mirrors NPC subtitle styling - if (target->getType() == game::ObjectType::PLAYER) { - uint32_t guildId = gameHandler.getEntityGuildId(target->getGuid()); - if (guildId != 0) { - const std::string& gn = gameHandler.lookupGuildName(guildId); - if (!gn.empty()) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", gn.c_str()); - } - } - } - // Right-click context menu on the target name if (ImGui::BeginPopupContextItem("##TargetNameCtx")) { const bool isPlayer = (target->getType() == game::ObjectType::PLAYER); @@ -4379,30 +4280,6 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) ImGui::SetTooltip("Rare — uncommon spawn with better loot"); } } - // Creature type label (Beast, Humanoid, Demon, etc.) - if (target->getType() == game::ObjectType::UNIT) { - uint32_t ctype = gameHandler.getCreatureType(unit->getEntry()); - const char* ctypeName = nullptr; - switch (ctype) { - case 1: ctypeName = "Beast"; break; - case 2: ctypeName = "Dragonkin"; break; - case 3: ctypeName = "Demon"; break; - case 4: ctypeName = "Elemental"; break; - case 5: ctypeName = "Giant"; break; - case 6: ctypeName = "Undead"; break; - case 7: ctypeName = "Humanoid"; break; - case 8: ctypeName = "Critter"; break; - case 9: ctypeName = "Mechanical"; break; - case 11: ctypeName = "Totem"; break; - case 12: ctypeName = "Non-combat Pet"; break; - case 13: ctypeName = "Gas Cloud"; break; - default: break; - } - if (ctypeName) { - ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 0.9f), "(%s)", ctypeName); - } - } if (confirmedCombatWithTarget) { float cPulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); ImGui::SameLine(); @@ -4453,38 +4330,6 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { } } - // Combo points — shown when the player has combo points on this target - { - uint8_t cp = gameHandler.getComboPoints(); - if (cp > 0 && gameHandler.getComboTarget() == target->getGuid()) { - const float dotSize = 12.0f; - const float dotSpacing = 4.0f; - const int maxCP = 5; - float totalW = maxCP * dotSize + (maxCP - 1) * dotSpacing; - float startX = (frameW - totalW) * 0.5f; - ImGui::SetCursorPosX(startX); - ImVec2 cursor = ImGui::GetCursorScreenPos(); - ImDrawList* dl = ImGui::GetWindowDrawList(); - for (int ci = 0; ci < maxCP; ++ci) { - float cx = cursor.x + ci * (dotSize + dotSpacing) + dotSize * 0.5f; - float cy = cursor.y + dotSize * 0.5f; - if (ci < static_cast(cp)) { - // Lit: yellow for 1-4, red glow for 5 - ImU32 col = (cp >= 5) - ? IM_COL32(255, 50, 30, 255) - : IM_COL32(255, 210, 30, 255); - dl->AddCircleFilled(ImVec2(cx, cy), dotSize * 0.45f, col); - // Subtle glow - dl->AddCircle(ImVec2(cx, cy), dotSize * 0.5f, IM_COL32(255, 255, 200, 80), 0, 1.5f); - } else { - // Unlit: dark outline - dl->AddCircle(ImVec2(cx, cy), dotSize * 0.4f, IM_COL32(80, 80, 80, 180), 0, 1.5f); - } - } - ImGui::Dummy(ImVec2(totalW, dotSize + 2.0f)); - } - } - // Target cast bar — shown when the target is casting if (gameHandler.isTargetCasting()) { float castPct = gameHandler.getTargetCastProgress(); @@ -5119,42 +4964,6 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImVec2(ImGui::CalcTextSize(focusName.c_str()).x, 0)); ImGui::PopStyleColor(4); - // Right-click context menu on focus frame - if (ImGui::BeginPopupContextItem("##FocusFrameCtx")) { - ImGui::TextDisabled("%s", focusName.c_str()); - ImGui::Separator(); - if (ImGui::MenuItem("Target")) - gameHandler.setTarget(focus->getGuid()); - if (ImGui::MenuItem("Clear Focus")) - gameHandler.clearFocus(); - if (focus->getType() == game::ObjectType::PLAYER) { - ImGui::Separator(); - if (ImGui::MenuItem("Whisper")) { - selectedChatType = 4; - strncpy(whisperTargetBuffer, focusName.c_str(), sizeof(whisperTargetBuffer) - 1); - whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; - refocusChatInput = true; - } - if (ImGui::MenuItem("Invite to Group")) - gameHandler.inviteToGroup(focusName); - if (ImGui::MenuItem("Trade")) - gameHandler.initiateTrade(focus->getGuid()); - if (ImGui::MenuItem("Duel")) - gameHandler.proposeDuel(focus->getGuid()); - if (ImGui::MenuItem("Inspect")) { - gameHandler.setTarget(focus->getGuid()); - gameHandler.inspectTarget(); - showInspectWindow_ = true; - } - ImGui::Separator(); - if (ImGui::MenuItem("Add Friend")) - gameHandler.addFriend(focusName); - if (ImGui::MenuItem("Ignore")) - gameHandler.addIgnore(focusName); - } - ImGui::EndPopup(); - } - // Group leader crown — golden ♛ when the focused player is the party/raid leader if (gameHandler.isInGroup() && focus->getType() == game::ObjectType::PLAYER) { if (gameHandler.getPartyData().leaderGuid == focus->getGuid()) { @@ -5194,42 +5003,12 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { else if (fRank == 3) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(1.0f,0.3f,0.3f,1.0f), "[Boss]"); } else if (fRank == 4) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(0.5f,0.9f,1.0f,1.0f), "[Rare]"); } - // Creature type - { - uint32_t fctype = gameHandler.getCreatureType(focusUnit->getEntry()); - const char* fctName = nullptr; - switch (fctype) { - case 1: fctName="Beast"; break; case 2: fctName="Dragonkin"; break; - case 3: fctName="Demon"; break; case 4: fctName="Elemental"; break; - case 5: fctName="Giant"; break; case 6: fctName="Undead"; break; - case 7: fctName="Humanoid"; break; case 8: fctName="Critter"; break; - case 9: fctName="Mechanical"; break; case 11: fctName="Totem"; break; - case 12: fctName="Non-combat Pet"; break; case 13: fctName="Gas Cloud"; break; - default: break; - } - if (fctName) { - ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 0.9f), "(%s)", fctName); - } - } - // Creature subtitle const std::string fSub = gameHandler.getCachedCreatureSubName(focusUnit->getEntry()); if (!fSub.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", fSub.c_str()); } - // Player guild name on focus frame - if (focus->getType() == game::ObjectType::PLAYER) { - uint32_t guildId = gameHandler.getEntityGuildId(focus->getGuid()); - if (guildId != 0) { - const std::string& gn = gameHandler.lookupGuildName(guildId); - if (!gn.empty()) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", gn.c_str()); - } - } - } - if (ImGui::BeginPopupContextItem("##FocusNameCtx")) { const bool focusIsPlayer = (focus->getType() == game::ObjectType::PLAYER); const uint64_t fGuid = focus->getGuid(); @@ -6003,33 +5782,6 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } - // /loc command — print player coordinates and zone name - if (cmdLower == "loc" || cmdLower == "coords" || cmdLower == "whereami") { - const auto& pmi = gameHandler.getMovementInfo(); - std::string zoneName; - if (auto* rend = core::Application::getInstance().getRenderer()) - zoneName = rend->getCurrentZoneName(); - char buf[256]; - snprintf(buf, sizeof(buf), "%.1f, %.1f, %.1f%s%s", - pmi.x, pmi.y, pmi.z, - zoneName.empty() ? "" : " — ", - zoneName.c_str()); - game::MessageChatData sysMsg; - sysMsg.type = game::ChatType::SYSTEM; - sysMsg.language = game::ChatLanguage::UNIVERSAL; - sysMsg.message = buf; - gameHandler.addLocalChatMessage(sysMsg); - chatInputBuffer[0] = '\0'; - return; - } - - // /screenshot command — capture current frame to PNG - if (cmdLower == "screenshot" || cmdLower == "ss") { - takeScreenshot(gameHandler); - chatInputBuffer[0] = '\0'; - return; - } - // /zone command — print current zone name if (cmdLower == "zone") { std::string zoneName; @@ -6107,9 +5859,9 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { "Items: /use /equip /equipset [name]", "Target: /target /cleartarget /focus /clearfocus", "Movement: /sit /stand /kneel /dismount", - "Misc: /played /time /zone /loc /afk [msg] /dnd [msg] /inspect", + "Misc: /played /time /zone /afk [msg] /dnd [msg] /inspect", " /helm /cloak /trade /join /leave ", - " /score /unstuck /logout /ticket /screenshot /help", + " /score /unstuck /logout /ticket /help", }; for (const char* line : kHelpLines) { game::MessageChatData helpMsg; @@ -10613,331 +10365,262 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { if (entries.empty()) return; auto* window = core::Application::getInstance().getWindow(); - if (!window) return; - const float screenW = static_cast(window->getWidth()); - const float screenH = static_cast(window->getHeight()); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - // Camera for world-space projection - auto* appRenderer = core::Application::getInstance().getRenderer(); - rendering::Camera* camera = appRenderer ? appRenderer->getCamera() : nullptr; - glm::mat4 viewProj; - if (camera) viewProj = camera->getProjectionMatrix() * camera->getViewMatrix(); + // Render combat text entries overlaid on screen + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(screenW, 400)); - ImDrawList* drawList = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - const float baseFontSize = ImGui::GetFontSize(); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; - // HUD fallback: entries without world-space anchor use classic screen-position layout. - // We still need an ImGui window for those. - const float hudIncomingX = screenW * 0.40f; - const float hudOutgoingX = screenW * 0.68f; - int hudInIdx = 0, hudOutIdx = 0; - bool needsHudWindow = false; - - for (const auto& entry : entries) { - const float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME); - const bool outgoing = entry.isPlayerSource; - - // --- Format text and color (identical logic for both world and HUD paths) --- - ImVec4 color; - char text[128]; - switch (entry.type) { - case game::CombatTextEntry::MELEE_DAMAGE: - case game::CombatTextEntry::SPELL_DAMAGE: - snprintf(text, sizeof(text), "-%d", entry.amount); - color = outgoing ? - ImVec4(1.0f, 1.0f, 0.3f, alpha) : - ImVec4(1.0f, 0.3f, 0.3f, alpha); - break; - case game::CombatTextEntry::CRIT_DAMAGE: - snprintf(text, sizeof(text), "-%d!", entry.amount); - color = outgoing ? - ImVec4(1.0f, 0.8f, 0.0f, alpha) : - ImVec4(1.0f, 0.5f, 0.0f, alpha); - break; - case game::CombatTextEntry::HEAL: - snprintf(text, sizeof(text), "+%d", entry.amount); - color = ImVec4(0.3f, 1.0f, 0.3f, alpha); - break; - case game::CombatTextEntry::CRIT_HEAL: - snprintf(text, sizeof(text), "+%d!", entry.amount); - color = ImVec4(0.3f, 1.0f, 0.3f, alpha); - break; - case game::CombatTextEntry::MISS: - snprintf(text, sizeof(text), "Miss"); - color = ImVec4(0.7f, 0.7f, 0.7f, alpha); - break; - case game::CombatTextEntry::DODGE: - snprintf(text, sizeof(text), outgoing ? "Dodge" : "You Dodge"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::PARRY: - snprintf(text, sizeof(text), outgoing ? "Parry" : "You Parry"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::BLOCK: - if (entry.amount > 0) - snprintf(text, sizeof(text), outgoing ? "Block %d" : "You Block %d", entry.amount); - else - snprintf(text, sizeof(text), outgoing ? "Block" : "You Block"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::EVADE: - snprintf(text, sizeof(text), outgoing ? "Evade" : "You Evade"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::PERIODIC_DAMAGE: - snprintf(text, sizeof(text), "-%d", entry.amount); - color = outgoing ? - ImVec4(1.0f, 0.9f, 0.3f, alpha) : - ImVec4(1.0f, 0.4f, 0.4f, alpha); - break; - case game::CombatTextEntry::PERIODIC_HEAL: - snprintf(text, sizeof(text), "+%d", entry.amount); - color = ImVec4(0.4f, 1.0f, 0.5f, alpha); - break; - case game::CombatTextEntry::ENVIRONMENTAL: { - const char* envLabel = ""; - switch (entry.powerType) { - case 0: envLabel = "Fatigue "; break; - case 1: envLabel = "Drowning "; break; - case 2: envLabel = ""; break; - 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); - break; - } - case game::CombatTextEntry::ENERGIZE: - snprintf(text, sizeof(text), "+%d", entry.amount); - switch (entry.powerType) { - case 1: color = ImVec4(1.0f, 0.2f, 0.2f, alpha); break; - case 2: color = ImVec4(1.0f, 0.6f, 0.1f, alpha); break; - case 3: color = ImVec4(1.0f, 0.9f, 0.2f, alpha); break; - case 6: color = ImVec4(0.3f, 0.9f, 0.8f, alpha); break; - default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; - } - break; - case game::CombatTextEntry::POWER_DRAIN: - snprintf(text, sizeof(text), "-%d", entry.amount); - switch (entry.powerType) { - case 1: color = ImVec4(1.0f, 0.35f, 0.35f, alpha); break; - case 2: color = ImVec4(1.0f, 0.7f, 0.2f, alpha); break; - case 3: color = ImVec4(1.0f, 0.95f, 0.35f, alpha); break; - case 6: color = ImVec4(0.45f, 0.95f, 0.85f, alpha); break; - default: color = ImVec4(0.45f, 0.75f, 1.0f, alpha); break; - } - break; - case game::CombatTextEntry::XP_GAIN: - snprintf(text, sizeof(text), "+%d XP", entry.amount); - color = ImVec4(0.7f, 0.3f, 1.0f, alpha); - break; - case game::CombatTextEntry::IMMUNE: - snprintf(text, sizeof(text), "Immune!"); - color = ImVec4(0.9f, 0.9f, 0.9f, alpha); - break; - case game::CombatTextEntry::ABSORB: - if (entry.amount > 0) - snprintf(text, sizeof(text), "Absorbed %d", entry.amount); - else - snprintf(text, sizeof(text), "Absorbed"); - color = ImVec4(0.5f, 0.8f, 1.0f, alpha); - break; - case game::CombatTextEntry::RESIST: - if (entry.amount > 0) - snprintf(text, sizeof(text), "Resisted %d", entry.amount); - else - snprintf(text, sizeof(text), "Resisted"); - color = ImVec4(0.7f, 0.7f, 0.7f, alpha); - break; - case game::CombatTextEntry::DEFLECT: - snprintf(text, sizeof(text), outgoing ? "Deflect" : "You Deflect"); - color = outgoing ? ImVec4(0.7f, 0.7f, 0.7f, alpha) - : ImVec4(0.5f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::REFLECT: { - const std::string& reflectName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; - if (!reflectName.empty()) - snprintf(text, sizeof(text), outgoing ? "Reflected: %s" : "Reflect: %s", reflectName.c_str()); - else - snprintf(text, sizeof(text), outgoing ? "Reflected" : "You Reflect"); - color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha) - : ImVec4(0.75f, 0.85f, 1.0f, alpha); - break; - } - case game::CombatTextEntry::PROC_TRIGGER: { - const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; - if (!procName.empty()) - snprintf(text, sizeof(text), "%s!", procName.c_str()); - else - snprintf(text, sizeof(text), "PROC!"); - color = ImVec4(1.0f, 0.85f, 0.0f, alpha); - break; - } - case game::CombatTextEntry::DISPEL: - if (entry.spellId != 0) { - const std::string& dispelledName = gameHandler.getSpellName(entry.spellId); - if (!dispelledName.empty()) - snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str()); - else - snprintf(text, sizeof(text), "Dispel"); - } else { - snprintf(text, sizeof(text), "Dispel"); - } - color = ImVec4(0.6f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::STEAL: - if (entry.spellId != 0) { - const std::string& stolenName = gameHandler.getSpellName(entry.spellId); - if (!stolenName.empty()) - snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str()); - else - snprintf(text, sizeof(text), "Spellsteal"); - } else { - snprintf(text, sizeof(text), "Spellsteal"); - } - color = ImVec4(0.8f, 0.7f, 1.0f, alpha); - break; - case game::CombatTextEntry::INTERRUPT: { - const std::string& interruptedName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; - if (!interruptedName.empty()) - snprintf(text, sizeof(text), "Interrupt %s", interruptedName.c_str()); - else - snprintf(text, sizeof(text), "Interrupt"); - color = ImVec4(1.0f, 0.6f, 0.9f, alpha); - break; - } - case game::CombatTextEntry::INSTAKILL: - snprintf(text, sizeof(text), outgoing ? "Kill!" : "Killed!"); - color = outgoing ? ImVec4(1.0f, 0.25f, 0.25f, alpha) - : ImVec4(1.0f, 0.1f, 0.1f, alpha); - break; - case game::CombatTextEntry::HONOR_GAIN: - snprintf(text, sizeof(text), "+%d Honor", entry.amount); - color = ImVec4(1.0f, 0.85f, 0.0f, alpha); - break; - case game::CombatTextEntry::GLANCING: - snprintf(text, sizeof(text), "~%d", entry.amount); - color = outgoing ? - ImVec4(0.75f, 0.75f, 0.5f, alpha) : - ImVec4(0.75f, 0.35f, 0.35f, alpha); - break; - case game::CombatTextEntry::CRUSHING: - snprintf(text, sizeof(text), "%d!", entry.amount); - color = outgoing ? - ImVec4(1.0f, 0.55f, 0.1f, alpha) : - ImVec4(1.0f, 0.15f, 0.15f, alpha); - break; - default: - snprintf(text, sizeof(text), "%d", entry.amount); - color = ImVec4(1.0f, 1.0f, 1.0f, alpha); - break; - } - - // --- Rendering style --- - bool isCrit = (entry.type == game::CombatTextEntry::CRIT_DAMAGE || - entry.type == game::CombatTextEntry::CRIT_HEAL); - float renderFontSize = isCrit ? baseFontSize * 1.35f : baseFontSize; - - ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast(alpha * 180)); - ImU32 textCol = ImGui::ColorConvertFloat4ToU32(color); - - // --- Try world-space anchor if we have a destination entity --- - // Types that should always stay as HUD elements (no world anchor) - bool isHudOnly = (entry.type == game::CombatTextEntry::XP_GAIN || - entry.type == game::CombatTextEntry::HONOR_GAIN || - entry.type == game::CombatTextEntry::PROC_TRIGGER); - - bool rendered = false; - if (!isHudOnly && camera && entry.dstGuid != 0) { - // Look up the destination entity's render position - glm::vec3 renderPos; - bool havePos = core::Application::getInstance().getRenderPositionForGuid(entry.dstGuid, renderPos); - if (!havePos) { - // Fallback to entity canonical position - auto entity = gameHandler.getEntityManager().getEntity(entry.dstGuid); - if (entity) { - auto* unit = dynamic_cast(entity.get()); - if (unit) { - renderPos = core::coords::canonicalToRender( - glm::vec3(unit->getX(), unit->getY(), unit->getZ())); - havePos = true; - } - } - } - - if (havePos) { - // Float upward from above the entity's head - renderPos.z += 2.5f + entry.age * 1.2f; - - // Project to screen - glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f); - if (clipPos.w > 0.01f) { - glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w; - if (ndc.x >= -1.5f && ndc.x <= 1.5f && ndc.y >= -1.5f && ndc.y <= 1.5f) { - float sx = (ndc.x * 0.5f + 0.5f) * screenW; - float sy = (ndc.y * 0.5f + 0.5f) * screenH; - - // Horizontal stagger using the random seed - sx += entry.xSeed * 40.0f; - - // Center the text horizontally on the projected point - ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text); - sx -= ts.x * 0.5f; - - // Clamp to screen bounds - sx = std::max(2.0f, std::min(sx, screenW - ts.x - 2.0f)); - - drawList->AddText(font, renderFontSize, - ImVec2(sx + 1.0f, sy + 1.0f), shadowCol, text); - drawList->AddText(font, renderFontSize, - ImVec2(sx, sy), textCol, text); - rendered = true; - } - } - } - } - - // --- HUD fallback for entries without world anchor or HUD-only types --- - if (!rendered) { - if (!needsHudWindow) { - needsHudWindow = true; - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(ImVec2(screenW, 400)); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; - ImGui::Begin("##CombatText", nullptr, flags); - } + if (ImGui::Begin("##CombatText", nullptr, flags)) { + // Incoming events (enemy attacks player) float near screen center (over the player). + // Outgoing events (player attacks enemy) float on the right side (near the target). + const float incomingX = screenW * 0.40f; + const float outgoingX = screenW * 0.68f; + int inIdx = 0, outIdx = 0; + for (const auto& entry : entries) { + float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME); float yOffset = 200.0f - entry.age * 60.0f; - int& idx = outgoing ? hudOutIdx : hudInIdx; - float baseX = outgoing ? hudOutgoingX : hudIncomingX; + const bool outgoing = entry.isPlayerSource; + + ImVec4 color; + char text[64]; + switch (entry.type) { + case game::CombatTextEntry::MELEE_DAMAGE: + case game::CombatTextEntry::SPELL_DAMAGE: + snprintf(text, sizeof(text), "-%d", entry.amount); + color = outgoing ? + ImVec4(1.0f, 1.0f, 0.3f, alpha) : // Outgoing = yellow + ImVec4(1.0f, 0.3f, 0.3f, alpha); // Incoming = red + break; + case game::CombatTextEntry::CRIT_DAMAGE: + snprintf(text, sizeof(text), "-%d!", entry.amount); + color = outgoing ? + ImVec4(1.0f, 0.8f, 0.0f, alpha) : // Outgoing crit = bright yellow + ImVec4(1.0f, 0.5f, 0.0f, alpha); // Incoming crit = orange + break; + case game::CombatTextEntry::HEAL: + snprintf(text, sizeof(text), "+%d", entry.amount); + color = ImVec4(0.3f, 1.0f, 0.3f, alpha); + break; + case game::CombatTextEntry::CRIT_HEAL: + snprintf(text, sizeof(text), "+%d!", entry.amount); + color = ImVec4(0.3f, 1.0f, 0.3f, alpha); + break; + case game::CombatTextEntry::MISS: + snprintf(text, sizeof(text), "Miss"); + color = ImVec4(0.7f, 0.7f, 0.7f, alpha); + break; + case game::CombatTextEntry::DODGE: + // outgoing=true: enemy dodged player's attack + // outgoing=false: player dodged incoming attack + snprintf(text, sizeof(text), outgoing ? "Dodge" : "You Dodge"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::PARRY: + snprintf(text, sizeof(text), outgoing ? "Parry" : "You Parry"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::BLOCK: + if (entry.amount > 0) + snprintf(text, sizeof(text), outgoing ? "Block %d" : "You Block %d", entry.amount); + else + snprintf(text, sizeof(text), outgoing ? "Block" : "You Block"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::EVADE: + snprintf(text, sizeof(text), outgoing ? "Evade" : "You Evade"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::PERIODIC_DAMAGE: + snprintf(text, sizeof(text), "-%d", entry.amount); + color = outgoing ? + ImVec4(1.0f, 0.9f, 0.3f, alpha) : // Outgoing DoT = pale yellow + ImVec4(1.0f, 0.4f, 0.4f, alpha); // Incoming DoT = pale red + break; + case game::CombatTextEntry::PERIODIC_HEAL: + snprintf(text, sizeof(text), "+%d", entry.amount); + color = ImVec4(0.4f, 1.0f, 0.5f, alpha); + break; + 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) { + case 1: color = ImVec4(1.0f, 0.2f, 0.2f, alpha); break; // Rage: red + case 2: color = ImVec4(1.0f, 0.6f, 0.1f, alpha); break; // Focus: orange + case 3: color = ImVec4(1.0f, 0.9f, 0.2f, alpha); break; // Energy: yellow + case 6: color = ImVec4(0.3f, 0.9f, 0.8f, alpha); break; // Runic Power: teal + default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; // Mana (0): blue + } + break; + case game::CombatTextEntry::POWER_DRAIN: + snprintf(text, sizeof(text), "-%d", entry.amount); + switch (entry.powerType) { + case 1: color = ImVec4(1.0f, 0.35f, 0.35f, alpha); break; + case 2: color = ImVec4(1.0f, 0.7f, 0.2f, alpha); break; + case 3: color = ImVec4(1.0f, 0.95f, 0.35f, alpha); break; + case 6: color = ImVec4(0.45f, 0.95f, 0.85f, alpha); break; + default: color = ImVec4(0.45f, 0.75f, 1.0f, alpha); break; + } + break; + case game::CombatTextEntry::XP_GAIN: + snprintf(text, sizeof(text), "+%d XP", entry.amount); + color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP + break; + case game::CombatTextEntry::IMMUNE: + snprintf(text, sizeof(text), "Immune!"); + color = ImVec4(0.9f, 0.9f, 0.9f, alpha); // White for immune + break; + case game::CombatTextEntry::ABSORB: + if (entry.amount > 0) + snprintf(text, sizeof(text), "Absorbed %d", entry.amount); + else + snprintf(text, sizeof(text), "Absorbed"); + color = ImVec4(0.5f, 0.8f, 1.0f, alpha); // Light blue for absorb + break; + case game::CombatTextEntry::RESIST: + if (entry.amount > 0) + snprintf(text, sizeof(text), "Resisted %d", entry.amount); + else + snprintf(text, sizeof(text), "Resisted"); + color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist + break; + case game::CombatTextEntry::DEFLECT: + snprintf(text, sizeof(text), outgoing ? "Deflect" : "You Deflect"); + color = outgoing ? ImVec4(0.7f, 0.7f, 0.7f, alpha) + : ImVec4(0.5f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::REFLECT: { + const std::string& reflectName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; + if (!reflectName.empty()) + snprintf(text, sizeof(text), outgoing ? "Reflected: %s" : "Reflect: %s", reflectName.c_str()); + else + snprintf(text, sizeof(text), outgoing ? "Reflected" : "You Reflect"); + color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha) + : ImVec4(0.75f, 0.85f, 1.0f, alpha); + break; + } + case game::CombatTextEntry::PROC_TRIGGER: { + const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; + if (!procName.empty()) + snprintf(text, sizeof(text), "%s!", procName.c_str()); + else + snprintf(text, sizeof(text), "PROC!"); + color = ImVec4(1.0f, 0.85f, 0.0f, alpha); // Gold for proc + break; + } + case game::CombatTextEntry::DISPEL: + if (entry.spellId != 0) { + const std::string& dispelledName = gameHandler.getSpellName(entry.spellId); + if (!dispelledName.empty()) + snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str()); + else + snprintf(text, sizeof(text), "Dispel"); + } else { + snprintf(text, sizeof(text), "Dispel"); + } + color = ImVec4(0.6f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::STEAL: + if (entry.spellId != 0) { + const std::string& stolenName = gameHandler.getSpellName(entry.spellId); + if (!stolenName.empty()) + snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str()); + else + snprintf(text, sizeof(text), "Spellsteal"); + } else { + snprintf(text, sizeof(text), "Spellsteal"); + } + color = ImVec4(0.8f, 0.7f, 1.0f, alpha); + break; + case game::CombatTextEntry::INTERRUPT: { + const std::string& interruptedName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; + if (!interruptedName.empty()) + snprintf(text, sizeof(text), "Interrupt %s", interruptedName.c_str()); + else + snprintf(text, sizeof(text), "Interrupt"); + color = ImVec4(1.0f, 0.6f, 0.9f, alpha); + break; + } + case game::CombatTextEntry::INSTAKILL: + snprintf(text, sizeof(text), outgoing ? "Kill!" : "Killed!"); + color = outgoing ? ImVec4(1.0f, 0.25f, 0.25f, alpha) + : ImVec4(1.0f, 0.1f, 0.1f, alpha); + break; + case game::CombatTextEntry::HONOR_GAIN: + snprintf(text, sizeof(text), "+%d Honor", entry.amount); + color = ImVec4(1.0f, 0.85f, 0.0f, alpha); // Gold for honor + break; + case game::CombatTextEntry::GLANCING: + snprintf(text, sizeof(text), "~%d", entry.amount); + color = outgoing ? + ImVec4(0.75f, 0.75f, 0.5f, alpha) : // Outgoing glancing = muted yellow + ImVec4(0.75f, 0.35f, 0.35f, alpha); // Incoming glancing = muted red + break; + case game::CombatTextEntry::CRUSHING: + snprintf(text, sizeof(text), "%d!", entry.amount); + color = outgoing ? + ImVec4(1.0f, 0.55f, 0.1f, alpha) : // Outgoing crushing = orange + ImVec4(1.0f, 0.15f, 0.15f, alpha); // Incoming crushing = bright red + break; + default: + snprintf(text, sizeof(text), "%d", entry.amount); + color = ImVec4(1.0f, 1.0f, 1.0f, alpha); + break; + } + + // Outgoing → right side (near target), incoming → center-left (near player) + int& idx = outgoing ? outIdx : inIdx; + float baseX = outgoing ? outgoingX : incomingX; float xOffset = baseX + (idx % 3 - 1) * 60.0f; ++idx; + // Crits render at 1.35× normal font size for visual impact + bool isCrit = (entry.type == game::CombatTextEntry::CRIT_DAMAGE || + entry.type == game::CombatTextEntry::CRIT_HEAL); + ImFont* font = ImGui::GetFont(); + float baseFontSize = ImGui::GetFontSize(); + float renderFontSize = isCrit ? baseFontSize * 1.35f : baseFontSize; + + // Advance cursor so layout accounting is correct, then read screen pos ImGui::SetCursorPos(ImVec2(xOffset, yOffset)); ImVec2 screenPos = ImGui::GetCursorScreenPos(); - ImDrawList* dl = ImGui::GetWindowDrawList(); + // Drop shadow for readability over complex backgrounds + ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast(alpha * 180)); + ImU32 textCol = ImGui::ColorConvertFloat4ToU32(color); + ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddText(font, renderFontSize, ImVec2(screenPos.x + 1.0f, screenPos.y + 1.0f), shadowCol, text); dl->AddText(font, renderFontSize, screenPos, textCol, text); + // Reserve space so ImGui doesn't clip the window prematurely ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text); ImGui::Dummy(ts); } } - - if (needsHudWindow) { - ImGui::End(); - } + ImGui::End(); } // ============================================================ @@ -11155,8 +10838,7 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { bool isPlayer = (entityPtr->getType() == game::ObjectType::PLAYER); bool isTarget = (guid == targetGuid); - // Player nameplates use Shift+V toggle; NPC/enemy nameplates use V toggle - if (isPlayer && !showFriendlyNameplates_) continue; + // Player nameplates are always shown; NPC nameplates respect the V-key toggle if (!isPlayer && !showNameplates_) continue; // For corpses (dead units), only show a minimal grey nameplate if selected @@ -11238,10 +10920,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { isTargetingPlayer = (unitTarget == playerGuid); } } - // Creature rank for border styling (Elite=gold double border, Boss=red, Rare=silver) - int creatureRank = -1; - if (!isPlayer) creatureRank = gameHandler.getCreatureRank(unit->getEntry()); - // Border: gold = currently selected, orange = targeting player, dark = default ImU32 borderColor = isTarget ? IM_COL32(255, 215, 0, A(255)) @@ -11265,24 +10943,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { } drawList->AddRect (ImVec2(barX - 1.0f, sy - 1.0f), ImVec2(barX + barW + 1.0f, sy + barH + 1.0f), borderColor, 2.0f); - // Elite/Boss/Rare decoration: extra outer border with rank-specific color - if (creatureRank == 1 || creatureRank == 2) { - // Elite / Rare Elite: gold double border - drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f), - ImVec2(barX + barW + 3.0f, sy + barH + 3.0f), - IM_COL32(255, 200, 50, A(200)), 3.0f); - } else if (creatureRank == 3) { - // Boss: red double border - drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f), - ImVec2(barX + barW + 3.0f, sy + barH + 3.0f), - IM_COL32(255, 40, 40, A(200)), 3.0f); - } else if (creatureRank == 4) { - // Rare: silver double border - drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f), - ImVec2(barX + barW + 3.0f, sy + barH + 3.0f), - IM_COL32(170, 200, 230, A(200)), 3.0f); - } - // HP % text centered on health bar (non-corpse, non-full-health for readability) if (!isCorpse && unit->getMaxHealth() > 0) { int hpPct = static_cast(healthPct * 100.0f + 0.5f); @@ -11304,32 +10964,15 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { float castPct = std::clamp((cs->timeTotal - cs->timeRemaining) / cs->timeTotal, 0.0f, 1.0f); const float cbH = 6.0f * nameplateScale_; - // Spell icon + name above the cast bar + // Spell name above the cast bar const std::string& spellName = gameHandler.getSpellName(cs->spellId); - { - auto* castAm = core::Application::getInstance().getAssetManager(); - VkDescriptorSet castIcon = (cs->spellId && castAm) - ? getSpellIcon(cs->spellId, castAm) : VK_NULL_HANDLE; - float iconSz = cbH + 8.0f; - if (castIcon) { - // Draw icon to the left of the cast bar - float iconX = barX - iconSz - 2.0f; - float iconY = castBarBaseY; - drawList->AddImage((ImTextureID)(uintptr_t)castIcon, - ImVec2(iconX, iconY), - ImVec2(iconX + iconSz, iconY + iconSz)); - drawList->AddRect(ImVec2(iconX - 1.0f, iconY - 1.0f), - ImVec2(iconX + iconSz + 1.0f, iconY + iconSz + 1.0f), - IM_COL32(0, 0, 0, A(180)), 1.0f); - } - if (!spellName.empty()) { - ImVec2 snSz = ImGui::CalcTextSize(spellName.c_str()); - float snX = sx - snSz.x * 0.5f; - float snY = castBarBaseY; - drawList->AddText(ImVec2(snX + 1.0f, snY + 1.0f), IM_COL32(0, 0, 0, A(140)), spellName.c_str()); - drawList->AddText(ImVec2(snX, snY), IM_COL32(255, 210, 100, A(220)), spellName.c_str()); - castBarBaseY += snSz.y + 2.0f; - } + if (!spellName.empty()) { + ImVec2 snSz = ImGui::CalcTextSize(spellName.c_str()); + float snX = sx - snSz.x * 0.5f; + float snY = castBarBaseY; + drawList->AddText(ImVec2(snX + 1.0f, snY + 1.0f), IM_COL32(0, 0, 0, A(140)), spellName.c_str()); + drawList->AddText(ImVec2(snX, snY), IM_COL32(255, 210, 100, A(220)), spellName.c_str()); + castBarBaseY += snSz.y + 2.0f; } // Cast bar: green = interruptible, red = uninterruptible; both pulse when >80% complete @@ -11391,79 +11034,14 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { ImVec2(dotX + dotSize + 1.0f, nameplateBottom + dotSize + 1.0f), IM_COL32(0, 0, 0, A(150)), 1.0f); - // Duration clock-sweep overlay (like target frame auras) - uint64_t nowMs = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - int32_t remainMs = aura.getRemainingMs(nowMs); - if (aura.maxDurationMs > 0 && remainMs > 0) { - float pct = 1.0f - static_cast(remainMs) / static_cast(aura.maxDurationMs); - pct = std::clamp(pct, 0.0f, 1.0f); - float cx = dotX + dotSize * 0.5f; - float cy = nameplateBottom + dotSize * 0.5f; - float r = dotSize * 0.5f; - float startAngle = -IM_PI * 0.5f; - float endAngle = startAngle + pct * IM_PI * 2.0f; - ImVec2 center(cx, cy); - const int segments = 12; - for (int seg = 0; seg < segments; seg++) { - float a0 = startAngle + (endAngle - startAngle) * seg / segments; - float a1 = startAngle + (endAngle - startAngle) * (seg + 1) / segments; - drawList->AddTriangleFilled( - center, - ImVec2(cx + r * std::cos(a0), cy + r * std::sin(a0)), - ImVec2(cx + r * std::cos(a1), cy + r * std::sin(a1)), - IM_COL32(0, 0, 0, A(100))); - } - } - - // Stack count on dot (upper-left corner) - if (aura.charges > 1) { - char stackBuf[8]; - snprintf(stackBuf, sizeof(stackBuf), "%d", aura.charges); - drawList->AddText(ImVec2(dotX + 1.0f, nameplateBottom), IM_COL32(0, 0, 0, A(200)), stackBuf); - drawList->AddText(ImVec2(dotX, nameplateBottom - 1.0f), IM_COL32(255, 255, 255, A(240)), stackBuf); - } - - // Duration text below dot - if (remainMs > 0) { - char durBuf[8]; - if (remainMs >= 60000) - snprintf(durBuf, sizeof(durBuf), "%dm", remainMs / 60000); - else - snprintf(durBuf, sizeof(durBuf), "%d", remainMs / 1000); - ImVec2 durSz = ImGui::CalcTextSize(durBuf); - float durX = dotX + (dotSize - durSz.x) * 0.5f; - float durY = nameplateBottom + dotSize + 1.0f; - drawList->AddText(ImVec2(durX + 1.0f, durY + 1.0f), IM_COL32(0, 0, 0, A(180)), durBuf); - // Color: red if < 5s, yellow if < 15s, white otherwise - ImU32 durCol = remainMs < 5000 ? IM_COL32(255, 60, 60, A(240)) - : remainMs < 15000 ? IM_COL32(255, 200, 60, A(240)) - : IM_COL32(230, 230, 230, A(220)); - drawList->AddText(ImVec2(durX, durY), durCol, durBuf); - } - - // Spell name + duration tooltip on hover + // Spell name tooltip on hover { ImVec2 mouse = ImGui::GetMousePos(); if (mouse.x >= dotX && mouse.x < dotX + dotSize && mouse.y >= nameplateBottom && mouse.y < nameplateBottom + dotSize) { const std::string& dotSpellName = gameHandler.getSpellName(aura.spellId); - if (!dotSpellName.empty()) { - if (remainMs > 0) { - int secs = remainMs / 1000; - int mins = secs / 60; - secs %= 60; - char tipBuf[128]; - if (mins > 0) - snprintf(tipBuf, sizeof(tipBuf), "%s (%dm %ds)", dotSpellName.c_str(), mins, secs); - else - snprintf(tipBuf, sizeof(tipBuf), "%s (%ds)", dotSpellName.c_str(), secs); - ImGui::SetTooltip("%s", tipBuf); - } else { - ImGui::SetTooltip("%s", dotSpellName.c_str()); - } - } + if (!dotSpellName.empty()) + ImGui::SetTooltip("%s", dotSpellName.c_str()); } } @@ -11515,33 +11093,9 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { ? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC : IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC } - // Sub-label below the name: guild tag for players, subtitle for NPCs - std::string subLabel; - if (isPlayer) { - uint32_t guildId = gameHandler.getEntityGuildId(guid); - if (guildId != 0) { - const std::string& gn = gameHandler.lookupGuildName(guildId); - if (!gn.empty()) subLabel = "<" + gn + ">"; - } - } else { - // NPC subtitle (e.g. "", "") - std::string sub = gameHandler.getCachedCreatureSubName(unit->getEntry()); - if (!sub.empty()) subLabel = "<" + sub + ">"; - } - if (!subLabel.empty()) nameY -= 10.0f; // shift name up for sub-label line - drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf); drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf); - // Sub-label below the name (WoW-style or in lighter color) - if (!subLabel.empty()) { - ImVec2 subSz = ImGui::CalcTextSize(subLabel.c_str()); - float subX = sx - subSz.x * 0.5f; - float subY = nameY + textSize.y + 1.0f; - drawList->AddText(ImVec2(subX + 1.0f, subY + 1.0f), IM_COL32(0, 0, 0, A(120)), subLabel.c_str()); - drawList->AddText(ImVec2(subX, subY), IM_COL32(180, 180, 180, A(200)), subLabel.c_str()); - } - // Group leader crown to the right of the name on player nameplates if (isPlayer && gameHandler.isInGroup() && gameHandler.getPartyData().leaderGuid == guid) { @@ -12374,42 +11928,6 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { // Durability Warning (equipment damage indicator) // ============================================================ -void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) { - auto* renderer = core::Application::getInstance().getRenderer(); - if (!renderer) return; - - // Build path: ~/.wowee/screenshots/WoWee_YYYYMMDD_HHMMSS.png - const char* home = std::getenv("HOME"); - if (!home) home = std::getenv("USERPROFILE"); - if (!home) home = "/tmp"; - std::string dir = std::string(home) + "/.wowee/screenshots"; - - auto now = std::chrono::system_clock::now(); - auto tt = std::chrono::system_clock::to_time_t(now); - std::tm tm{}; -#ifdef _WIN32 - localtime_s(&tm, &tt); -#else - localtime_r(&tt, &tm); -#endif - - char filename[128]; - std::snprintf(filename, sizeof(filename), - "WoWee_%04d%02d%02d_%02d%02d%02d.png", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec); - - std::string path = dir + "/" + filename; - - if (renderer->captureScreenshot(path)) { - game::MessageChatData sysMsg; - sysMsg.type = game::ChatType::SYSTEM; - sysMsg.language = game::ChatLanguage::UNIVERSAL; - sysMsg.message = "Screenshot saved: " + path; - core::Application::getInstance().getGameHandler()->addLocalChatMessage(sysMsg); - } -} - void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) { if (gameHandler.getPlayerGuid() == 0) return; @@ -13548,30 +13066,20 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { } ImGui::Spacing(); - // voteMask bits: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant - const uint8_t vm = roll.voteMask; - bool first = true; - if (vm & 0x02) { - if (ImGui::Button("Need", ImVec2(80, 30))) - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0); - first = false; + if (ImGui::Button("Need", ImVec2(80, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0); } - if (vm & 0x04) { - if (!first) ImGui::SameLine(); - if (ImGui::Button("Greed", ImVec2(80, 30))) - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1); - first = false; + ImGui::SameLine(); + if (ImGui::Button("Greed", ImVec2(80, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1); } - if (vm & 0x08) { - if (!first) ImGui::SameLine(); - if (ImGui::Button("Disenchant", ImVec2(95, 30))) - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2); - first = false; + ImGui::SameLine(); + if (ImGui::Button("Disenchant", ImVec2(95, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2); } - if (vm & 0x01) { - if (!first) ImGui::SameLine(); - if (ImGui::Button("Pass", ImVec2(70, 30))) - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); + ImGui::SameLine(); + if (ImGui::Button("Pass", ImVec2(70, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); } // Live roll results from group members @@ -13738,8 +13246,22 @@ void GameScreen::renderBgInvitePopup(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Battleground Ready!", nullptr, popupFlags)) { - // BG name from stored queue data - std::string bgName = slot->bgName.empty() ? "Battleground" : slot->bgName; + // BG name + std::string bgName; + if (slot->arenaType > 0) { + bgName = std::to_string(slot->arenaType) + "v" + std::to_string(slot->arenaType) + " Arena"; + } else { + switch (slot->bgTypeId) { + case 1: bgName = "Alterac Valley"; break; + case 2: bgName = "Warsong Gulch"; break; + case 3: bgName = "Arathi Basin"; break; + case 7: bgName = "Eye of the Storm"; break; + case 9: bgName = "Strand of the Ancients"; break; + case 11: bgName = "Isle of Conquest"; break; + default: bgName = "Battleground"; break; + } + } + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", bgName.c_str()); ImGui::TextWrapped("A spot has opened! You have %d seconds to enter.", static_cast(remaining)); ImGui::Spacing(); @@ -13942,62 +13464,6 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::EndPopup(); } - // Petition signatures window (shown when a petition item is used or offered) - if (gameHandler.hasPetitionSignaturesUI()) { - ImGui::OpenPopup("PetitionSignatures"); - gameHandler.clearPetitionSignaturesUI(); - } - if (ImGui::BeginPopupModal("PetitionSignatures", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - const auto& pInfo = gameHandler.getPetitionInfo(); - if (!pInfo.guildName.empty()) - ImGui::Text("Guild Charter: %s", pInfo.guildName.c_str()); - else - ImGui::Text("Guild Charter"); - ImGui::Separator(); - - ImGui::Text("Signatures: %u / %u", pInfo.signatureCount, pInfo.signaturesRequired); - ImGui::Spacing(); - - if (!pInfo.signatures.empty()) { - for (size_t i = 0; i < pInfo.signatures.size(); ++i) { - const auto& sig = pInfo.signatures[i]; - // Try to resolve name from entity manager - std::string sigName; - if (sig.playerGuid != 0) { - auto entity = gameHandler.getEntityManager().getEntity(sig.playerGuid); - if (entity) { - auto* unit = dynamic_cast(entity.get()); - if (unit) sigName = unit->getName(); - } - } - if (sigName.empty()) - sigName = "Player " + std::to_string(i + 1); - ImGui::BulletText("%s", sigName.c_str()); - } - ImGui::Spacing(); - } - - // If we're not the owner, show Sign button - bool isOwner = (pInfo.ownerGuid == gameHandler.getPlayerGuid()); - if (!isOwner) { - if (ImGui::Button("Sign", ImVec2(120, 0))) { - gameHandler.signPetition(pInfo.petitionGuid); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - } else if (pInfo.signatureCount >= pInfo.signaturesRequired) { - // Owner with enough sigs — turn in - if (ImGui::Button("Turn In", ImVec2(120, 0))) { - gameHandler.turnInPetition(pInfo.petitionGuid); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - } - if (ImGui::Button("Close", ImVec2(120, 0))) - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - if (!showGuildRoster_) return; // Get zone manager for name lookup @@ -14811,15 +14277,10 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { const auto& ts = arenaStats[ai]; ImGui::PushID(static_cast(ai)); - // Team header: "2v2: Team Name" or fallback "Team #id" - std::string teamLabel; - if (ts.teamType > 0) - teamLabel = std::to_string(ts.teamType) + "v" + std::to_string(ts.teamType) + ": "; - if (!ts.teamName.empty()) - teamLabel += ts.teamName; - else - teamLabel += "Team #" + std::to_string(ts.teamId); - ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", teamLabel.c_str()); + // Team header with rating + char teamLabel[48]; + snprintf(teamLabel, sizeof(teamLabel), "Team #%u", ts.teamId); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", teamLabel); ImGui::Indent(8.0f); // Rating and rank @@ -14845,10 +14306,6 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGui::Spacing(); ImGui::TextDisabled("-- Roster (%zu members) --", roster->members.size()); - ImGui::SameLine(); - if (ImGui::SmallButton("Refresh")) - gameHandler.requestArenaTeamRoster(ts.teamId); - // Column headers ImGui::Columns(4, "##arenaRosterCols", false); ImGui::SetColumnWidth(0, 110.0f); @@ -14884,10 +14341,6 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGui::NextColumn(); } ImGui::Columns(1); - } else { - ImGui::Spacing(); - if (ImGui::SmallButton("Load Roster")) - gameHandler.requestArenaTeamRoster(ts.teamId); } ImGui::Unindent(8.0f); @@ -16052,22 +15505,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { gameHandler.repairAll(vendor.vendorGuid, false); } if (ImGui::IsItemHovered()) { - // Show durability summary of all equipment - const auto& inv = gameHandler.getInventory(); - int damagedCount = 0; - int brokenCount = 0; - for (int s = 0; s < static_cast(game::EquipSlot::BAG1); s++) { - const auto& slot = inv.getEquipSlot(static_cast(s)); - if (slot.empty() || slot.item.maxDurability == 0) continue; - if (slot.item.curDurability == 0) brokenCount++; - else if (slot.item.curDurability < slot.item.maxDurability) damagedCount++; - } - if (brokenCount > 0) - ImGui::SetTooltip("Repair all equipped items\n%d damaged, %d broken", damagedCount, brokenCount); - else if (damagedCount > 0) - ImGui::SetTooltip("Repair all equipped items\n%d item%s need repair", damagedCount, damagedCount > 1 ? "s" : ""); - else - ImGui::SetTooltip("All equipment is in good condition"); + ImGui::SetTooltip("Repair all equipped items using your gold"); } if (gameHandler.isInGuild()) { ImGui::SameLine(); @@ -16304,19 +15742,8 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (ImGui::SmallButton(buyBtnId.c_str())) { int qty = vendorBuyQty; if (item.maxCount > 0 && qty > item.maxCount) qty = item.maxCount; - uint32_t totalCost = item.buyPrice * static_cast(qty); - if (totalCost >= 10000) { // >= 1 gold: confirm - vendorConfirmOpen_ = true; - vendorConfirmGuid_ = vendor.vendorGuid; - vendorConfirmItemId_ = item.itemId; - vendorConfirmSlot_ = item.slot; - vendorConfirmQty_ = static_cast(qty); - vendorConfirmPrice_ = totalCost; - vendorConfirmItemName_ = (info && info->valid) ? info->name : "Item"; - } else { - gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, - static_cast(qty)); - } + gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, + static_cast(qty)); } if (outOfStock) ImGui::EndDisabled(); @@ -16332,33 +15759,6 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (!open) { gameHandler.closeVendor(); } - - // Vendor purchase confirmation popup for expensive items - if (vendorConfirmOpen_) { - ImGui::OpenPopup("Confirm Purchase##vendor"); - vendorConfirmOpen_ = false; - } - if (ImGui::BeginPopupModal("Confirm Purchase##vendor", nullptr, - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { - ImGui::Text("Buy %s", vendorConfirmItemName_.c_str()); - if (vendorConfirmQty_ > 1) - ImGui::Text("Quantity: %u", vendorConfirmQty_); - uint32_t g = vendorConfirmPrice_ / 10000; - uint32_t s = (vendorConfirmPrice_ / 100) % 100; - uint32_t c = vendorConfirmPrice_ % 100; - ImGui::Text("Cost: %ug %us %uc", g, s, c); - ImGui::Spacing(); - if (ImGui::Button("Buy", ImVec2(80, 0))) { - gameHandler.buyItem(vendorConfirmGuid_, vendorConfirmItemId_, - vendorConfirmSlot_, vendorConfirmQty_); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(80, 0))) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } } // ============================================================ @@ -16872,119 +16272,6 @@ void GameScreen::renderEscapeMenu() { ImGui::End(); } -// ============================================================ -// Barber Shop Window -// ============================================================ - -void GameScreen::renderBarberShopWindow(game::GameHandler& gameHandler) { - if (!gameHandler.isBarberShopOpen()) { - barberInitialized_ = false; - return; - } - - const auto* ch = gameHandler.getActiveCharacter(); - if (!ch) return; - - uint8_t race = static_cast(ch->race); - game::Gender gender = ch->gender; - game::Race raceEnum = ch->race; - - // Initialize sliders from current appearance - if (!barberInitialized_) { - barberOrigHairStyle_ = static_cast((ch->appearanceBytes >> 16) & 0xFF); - barberOrigHairColor_ = static_cast((ch->appearanceBytes >> 24) & 0xFF); - barberOrigFacialHair_ = static_cast(ch->facialFeatures); - barberHairStyle_ = barberOrigHairStyle_; - barberHairColor_ = barberOrigHairColor_; - barberFacialHair_ = barberOrigFacialHair_; - barberInitialized_ = true; - } - - int maxHairStyle = static_cast(game::getMaxHairStyle(raceEnum, gender)); - int maxHairColor = static_cast(game::getMaxHairColor(raceEnum, gender)); - int maxFacialHair = static_cast(game::getMaxFacialFeature(raceEnum, gender)); - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - float screenH = window ? static_cast(window->getHeight()) : 720.0f; - float winW = 300.0f; - float winH = 220.0f; - ImGui::SetNextWindowPos(ImVec2((screenW - winW) / 2.0f, (screenH - winH) / 2.0f), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Appearing); - - ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; - bool open = true; - if (ImGui::Begin("Barber Shop", &open, flags)) { - ImGui::Text("Choose your new look:"); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::PushItemWidth(-1); - - // Hair Style - ImGui::Text("Hair Style"); - ImGui::SliderInt("##HairStyle", &barberHairStyle_, 0, maxHairStyle, - "%d"); - - // Hair Color - ImGui::Text("Hair Color"); - ImGui::SliderInt("##HairColor", &barberHairColor_, 0, maxHairColor, - "%d"); - - // Facial Hair / Piercings / Markings - const char* facialLabel = (gender == game::Gender::FEMALE) ? "Piercings" : "Facial Hair"; - // Some races use "Markings" or "Tusks" etc. - if (race == 8 || race == 6) facialLabel = "Features"; // Trolls, Tauren - ImGui::Text("%s", facialLabel); - ImGui::SliderInt("##FacialHair", &barberFacialHair_, 0, maxFacialHair, - "%d"); - - ImGui::PopItemWidth(); - - ImGui::Spacing(); - ImGui::Separator(); - - // Show whether anything changed - bool changed = (barberHairStyle_ != barberOrigHairStyle_ || - barberHairColor_ != barberOrigHairColor_ || - barberFacialHair_ != barberOrigFacialHair_); - - // OK / Reset / Cancel buttons - float btnW = 80.0f; - float totalW = btnW * 3 + ImGui::GetStyle().ItemSpacing.x * 2; - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) / 2.0f); - - if (!changed) ImGui::BeginDisabled(); - if (ImGui::Button("OK", ImVec2(btnW, 0))) { - gameHandler.sendAlterAppearance( - static_cast(barberHairStyle_), - static_cast(barberHairColor_), - static_cast(barberFacialHair_)); - // Keep window open — server will respond with SMSG_BARBER_SHOP_RESULT - } - if (!changed) ImGui::EndDisabled(); - - ImGui::SameLine(); - if (!changed) ImGui::BeginDisabled(); - if (ImGui::Button("Reset", ImVec2(btnW, 0))) { - barberHairStyle_ = barberOrigHairStyle_; - barberHairColor_ = barberOrigHairColor_; - barberFacialHair_ = barberOrigFacialHair_; - } - if (!changed) ImGui::EndDisabled(); - - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(btnW, 0))) { - gameHandler.closeBarberShop(); - } - } - ImGui::End(); - - if (!open) { - gameHandler.closeBarberShop(); - } -} - // ============================================================ // Pet Stable Window // ============================================================ @@ -19719,31 +19006,6 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } - // Instance difficulty indicator — just below zone name, inside minimap top edge - if (gameHandler.isInInstance()) { - static const char* kDiffLabels[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"}; - uint32_t diff = gameHandler.getInstanceDifficulty(); - const char* label = (diff < 4) ? kDiffLabels[diff] : "Unknown"; - - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize() * 0.85f; - ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label); - float tx = centerX - ts.x * 0.5f; - // Position below zone name: top edge + zone font size + small gap - float ty = centerY - mapRadius + 4.0f + ImGui::GetFontSize() + 2.0f; - float pad = 2.0f; - - // Color-code: heroic=orange, normal=light gray - ImU32 bgCol = gameHandler.isInstanceHeroic() ? IM_COL32(120, 60, 0, 180) : IM_COL32(0, 0, 0, 160); - ImU32 textCol = gameHandler.isInstanceHeroic() ? IM_COL32(255, 180, 50, 255) : IM_COL32(200, 200, 200, 220); - - drawList->AddRectFilled( - ImVec2(tx - pad, ty - pad), - ImVec2(tx + ts.x + pad, ty + ts.y + pad), - bgCol, 2.0f); - drawList->AddText(font, fontSize, ImVec2(tx, ty), textCol, label); - } - // Hover tooltip and right-click context menu { ImVec2 mouse = ImGui::GetMousePos(); @@ -20006,37 +19268,6 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } ImGui::End(); - // Clock display at bottom-right of minimap (local time) - { - auto now = std::chrono::system_clock::now(); - auto tt = std::chrono::system_clock::to_time_t(now); - std::tm tmBuf{}; -#ifdef _WIN32 - localtime_s(&tmBuf, &tt); -#else - localtime_r(&tt, &tmBuf); -#endif - char clockText[16]; - std::snprintf(clockText, sizeof(clockText), "%d:%02d %s", - (tmBuf.tm_hour % 12 == 0) ? 12 : tmBuf.tm_hour % 12, - tmBuf.tm_min, - tmBuf.tm_hour >= 12 ? "PM" : "AM"); - ImVec2 clockSz = ImGui::CalcTextSize(clockText); - float clockW = clockSz.x + 10.0f; - float clockH = clockSz.y + 6.0f; - ImGui::SetNextWindowPos(ImVec2(centerX + mapRadius - clockW - 2.0f, - centerY + mapRadius - clockH - 2.0f), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(clockW, clockH), ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.45f); - ImGuiWindowFlags clockFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoInputs; - if (ImGui::Begin("##MinimapClock", nullptr, clockFlags)) { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.8f, 0.85f), "%s", clockText); - } - ImGui::End(); - } - // Indicators below the minimap (stacked: new mail, then BG queue, then latency) float indicatorX = centerX - mapRadius; float nextIndicatorY = centerY + mapRadius + 4.0f; @@ -20180,28 +19411,18 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { nextIndicatorY += kIndicatorH; } - // Latency + FPS indicator — centered at top of screen + // Latency indicator — centered at top of screen uint32_t latMs = gameHandler.getLatencyMs(); - if (showLatencyMeter_ && gameHandler.getState() == game::WorldState::IN_WORLD) { - float currentFps = ImGui::GetIO().Framerate; + if (showLatencyMeter_ && latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) { ImVec4 latColor; if (latMs < 100) latColor = ImVec4(0.3f, 1.0f, 0.3f, 0.9f); else if (latMs < 250) latColor = ImVec4(1.0f, 1.0f, 0.3f, 0.9f); else if (latMs < 500) latColor = ImVec4(1.0f, 0.6f, 0.1f, 0.9f); else latColor = ImVec4(1.0f, 0.2f, 0.2f, 0.9f); - ImVec4 fpsColor; - if (currentFps >= 60.0f) fpsColor = ImVec4(0.3f, 1.0f, 0.3f, 0.9f); - else if (currentFps >= 30.0f) fpsColor = ImVec4(1.0f, 1.0f, 0.3f, 0.9f); - else fpsColor = ImVec4(1.0f, 0.3f, 0.3f, 0.9f); - - char infoText[64]; - if (latMs > 0) - snprintf(infoText, sizeof(infoText), "%.0f fps | %u ms", currentFps, latMs); - else - snprintf(infoText, sizeof(infoText), "%.0f fps", currentFps); - - ImVec2 textSize = ImGui::CalcTextSize(infoText); + char latBuf[32]; + snprintf(latBuf, sizeof(latBuf), "%u ms", latMs); + ImVec2 textSize = ImGui::CalcTextSize(latBuf); float latW = textSize.x + 16.0f; float latH = textSize.y + 8.0f; ImGuiIO& lio = ImGui::GetIO(); @@ -20210,14 +19431,7 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { ImGui::SetNextWindowSize(ImVec2(latW, latH), ImGuiCond_Always); ImGui::SetNextWindowBgAlpha(0.45f); if (ImGui::Begin("##LatencyIndicator", nullptr, indicatorFlags)) { - // Color the FPS and latency portions differently - ImGui::TextColored(fpsColor, "%.0f fps", currentFps); - if (latMs > 0) { - ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.7f), "|"); - ImGui::SameLine(0, 4); - ImGui::TextColored(latColor, "%u ms", latMs); - } + ImGui::TextColored(latColor, "%s", latBuf); } ImGui::End(); } @@ -20386,16 +19600,8 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game: pos += replacement.length(); } - // Resolve class and race names for $C and $R placeholders - std::string className = "Adventurer"; - std::string raceName = "Unknown"; - if (character) { - className = game::getClassName(character->characterClass); - raceName = game::getRaceName(character->race); - } - // Replace simple placeholders. - // $n/$N = player name, $c/$C = class name, $r/$R = race name + // $n = player name // $p = subject pronoun (he/she/they) // $o = object pronoun (him/her/them) // $s = possessive adjective (his/her/their) @@ -20409,8 +19615,6 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game: std::string replacement; switch (code) { case 'n': case 'N': replacement = playerName; break; - case 'c': case 'C': replacement = className; break; - case 'r': case 'R': replacement = raceName; break; case 'p': replacement = pronouns.subject; break; case 'o': replacement = pronouns.object; break; case 's': replacement = pronouns.possessive; break; @@ -20545,7 +19749,6 @@ void GameScreen::saveSettings() { out << "show_keyring=" << (pendingShowKeyring ? 1 : 0) << "\n"; out << "action_bar_scale=" << pendingActionBarScale << "\n"; out << "nameplate_scale=" << nameplateScale_ << "\n"; - out << "show_friendly_nameplates=" << (showFriendlyNameplates_ ? 1 : 0) << "\n"; out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n"; out << "action_bar2_offset_x=" << pendingActionBar2OffsetX << "\n"; out << "action_bar2_offset_y=" << pendingActionBar2OffsetY << "\n"; @@ -20675,8 +19878,6 @@ void GameScreen::loadSettings() { pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f); } else if (key == "nameplate_scale") { nameplateScale_ = std::clamp(std::stof(val), 0.5f, 2.0f); - } else if (key == "show_friendly_nameplates") { - showFriendlyNameplates_ = (std::stoi(val) != 0); } else if (key == "show_action_bar2") { pendingShowActionBar2 = (std::stoi(val) != 0); } else if (key == "action_bar2_offset_x") { @@ -23222,36 +22423,36 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { ImGui::Text("Dungeon:"); struct DungeonEntry { uint32_t id; const char* name; }; - // Category 0=Random, 1=Classic, 2=TBC, 3=WotLK - struct DungeonEntryEx { uint32_t id; const char* name; uint8_t cat; }; - static const DungeonEntryEx kDungeons[] = { - { 861, "Random Dungeon", 0 }, - { 862, "Random Heroic", 0 }, - { 36, "Deadmines", 1 }, - { 43, "Ragefire Chasm", 1 }, - { 47, "Razorfen Kraul", 1 }, - { 48, "Blackfathom Deeps", 1 }, - { 52, "Uldaman", 1 }, - { 57, "Dire Maul: East", 1 }, - { 70, "Onyxia's Lair", 1 }, - { 264, "The Blood Furnace", 2 }, - { 269, "The Shattered Halls", 2 }, - { 576, "The Nexus", 3 }, - { 578, "The Oculus", 3 }, - { 595, "The Culling of Stratholme", 3 }, - { 599, "Halls of Stone", 3 }, - { 600, "Drak'Tharon Keep", 3 }, - { 601, "Azjol-Nerub", 3 }, - { 604, "Gundrak", 3 }, - { 608, "Violet Hold", 3 }, - { 619, "Ahn'kahet: Old Kingdom", 3 }, - { 623, "Halls of Lightning", 3 }, - { 632, "The Forge of Souls", 3 }, - { 650, "Trial of the Champion", 3 }, - { 658, "Pit of Saron", 3 }, - { 668, "Halls of Reflection", 3 }, + static const DungeonEntry kDungeons[] = { + { 861, "Random Dungeon" }, + { 862, "Random Heroic" }, + // Vanilla classics + { 36, "Deadmines" }, + { 43, "Ragefire Chasm" }, + { 47, "Razorfen Kraul" }, + { 48, "Blackfathom Deeps" }, + { 52, "Uldaman" }, + { 57, "Dire Maul: East" }, + { 70, "Onyxia's Lair" }, + // TBC heroics + { 264, "The Blood Furnace" }, + { 269, "The Shattered Halls" }, + // WotLK normals/heroics + { 576, "The Nexus" }, + { 578, "The Oculus" }, + { 595, "The Culling of Stratholme" }, + { 599, "Halls of Stone" }, + { 600, "Drak'Tharon Keep" }, + { 601, "Azjol-Nerub" }, + { 604, "Gundrak" }, + { 608, "Violet Hold" }, + { 619, "Ahn'kahet: Old Kingdom" }, + { 623, "Halls of Lightning" }, + { 632, "The Forge of Souls" }, + { 650, "Trial of the Champion" }, + { 658, "Pit of Saron" }, + { 668, "Halls of Reflection" }, }; - static const char* kCatHeaders[] = { nullptr, "-- Classic --", "-- TBC --", "-- WotLK --" }; // Find current index int curIdx = 0; @@ -23261,15 +22462,7 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { ImGui::SetNextItemWidth(-1); if (ImGui::BeginCombo("##dungeon", kDungeons[curIdx].name)) { - uint8_t lastCat = 255; for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { - if (kDungeons[i].cat != lastCat && kCatHeaders[kDungeons[i].cat]) { - if (lastCat != 255) ImGui::Separator(); - ImGui::TextDisabled("%s", kCatHeaders[kDungeons[i].cat]); - lastCat = kDungeons[i].cat; - } else if (kDungeons[i].cat != lastCat) { - lastCat = kDungeons[i].cat; - } bool selected = (kDungeons[i].id == lfgSelectedDungeon_); if (ImGui::Selectable(kDungeons[i].name, selected)) lfgSelectedDungeon_ = kDungeons[i].id; diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 366e9fa0..b4e2ac89 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -871,35 +871,6 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { ImGui::EndPopup(); } - // Stack split popup - if (splitConfirmOpen_) { - ImVec2 mousePos = ImGui::GetIO().MousePos; - ImGui::SetNextWindowPos(ImVec2(mousePos.x - 80.0f, mousePos.y - 20.0f), ImGuiCond_Always); - ImGui::OpenPopup("##SplitStack"); - splitConfirmOpen_ = false; - } - if (ImGui::BeginPopup("##SplitStack", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { - ImGui::Text("Split %s", splitItemName_.c_str()); - ImGui::Spacing(); - ImGui::SetNextItemWidth(120.0f); - ImGui::SliderInt("##splitcount", &splitCount_, 1, splitMax_ - 1); - ImGui::Spacing(); - if (ImGui::Button("OK", ImVec2(55, 0))) { - if (gameHandler_ && splitCount_ > 0 && splitCount_ < splitMax_) { - gameHandler_->splitItem(splitBag_, splitSlot_, static_cast(splitCount_)); - } - splitItemName_.clear(); - inventoryDirty = true; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(55, 0))) { - splitItemName_.clear(); - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - // Draw held item at cursor renderHeldItem(); } @@ -2331,39 +2302,22 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite } } - // Shift+right-click: split stack (if stackable >1) or destroy confirmation + // Shift+right-click: open destroy confirmation for non-quest items if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && - !holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0) { - if (item.stackCount > 1 && item.maxStack > 1) { - // Open split popup for stackable items - splitConfirmOpen_ = true; - splitItemName_ = item.name; - splitMax_ = static_cast(item.stackCount); - splitCount_ = splitMax_ / 2; - if (splitCount_ < 1) splitCount_ = 1; - if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { - splitBag_ = 0xFF; - splitSlot_ = static_cast(23 + backpackIndex); - } else if (kind == SlotKind::BACKPACK && isBagSlot) { - splitBag_ = static_cast(19 + bagIndex); - splitSlot_ = static_cast(bagSlotIndex); - } - } else if (item.bindType != 4) { - // Destroy confirmation for non-quest, non-stackable items - destroyConfirmOpen_ = true; - destroyItemName_ = item.name; - destroyCount_ = static_cast(std::clamp( - std::max(1u, item.stackCount), 1u, 255u)); - if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { - destroyBag_ = 0xFF; - destroySlot_ = static_cast(23 + backpackIndex); - } else if (kind == SlotKind::BACKPACK && isBagSlot) { - destroyBag_ = static_cast(19 + bagIndex); - destroySlot_ = static_cast(bagSlotIndex); - } else if (kind == SlotKind::EQUIPMENT) { - destroyBag_ = 0xFF; - destroySlot_ = static_cast(equipSlot); - } + !holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0 && item.bindType != 4) { + destroyConfirmOpen_ = true; + destroyItemName_ = item.name; + destroyCount_ = static_cast(std::clamp( + std::max(1u, item.stackCount), 1u, 255u)); + if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + destroyBag_ = 0xFF; + destroySlot_ = static_cast(23 + backpackIndex); + } else if (kind == SlotKind::BACKPACK && isBagSlot) { + destroyBag_ = static_cast(19 + bagIndex); + destroySlot_ = static_cast(bagSlotIndex); + } else if (kind == SlotKind::EQUIPMENT) { + destroyBag_ = 0xFF; + destroySlot_ = static_cast(equipSlot); } } diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index fe5cd2cb..92b52bd9 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -82,14 +82,6 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler pos += replacement.length(); } - // Resolve class and race names for $C and $R placeholders - std::string className = "Adventurer"; - std::string raceName = "Unknown"; - if (character) { - className = game::getClassName(character->characterClass); - raceName = game::getRaceName(character->race); - } - // Replace simple placeholders pos = 0; while ((pos = result.find('$', pos)) != std::string::npos) { @@ -100,12 +92,11 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler switch (code) { case 'n': case 'N': replacement = playerName; break; - case 'c': case 'C': replacement = className; break; - case 'r': case 'R': replacement = raceName; break; case 'p': replacement = pronouns.subject; break; case 'o': replacement = pronouns.object; break; case 's': replacement = pronouns.possessive; break; case 'S': replacement = pronouns.possessiveP; break; + case 'r': replacement = pronouns.object; break; case 'b': case 'B': replacement = "\n"; break; case 'g': case 'G': pos++; continue; default: pos++; continue; diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index 5f87712f..c2b92eff 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -176,29 +176,6 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { ImGui::EndTabBar(); } - - // Talent learn confirmation popup - if (talentConfirmOpen_) { - ImGui::OpenPopup("Learn Talent?##talent_confirm"); - talentConfirmOpen_ = false; - } - if (ImGui::BeginPopupModal("Learn Talent?##talent_confirm", nullptr, - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", pendingTalentName_.c_str()); - ImGui::Text("Rank %u", pendingTalentRank_ + 1); - ImGui::Spacing(); - ImGui::TextWrapped("Spend a talent point?"); - ImGui::Spacing(); - if (ImGui::Button("Learn", ImVec2(80, 0))) { - gameHandler.learnTalent(pendingTalentId_, pendingTalentRank_); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(80, 0))) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } } void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId, @@ -597,15 +574,10 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, ImGui::EndTooltip(); } - // Handle click — open confirmation dialog instead of learning directly + // Handle click — currentRank is 1-indexed (0=not learned, 1=rank1, ...) + // CMSG_LEARN_TALENT requestedRank must equal current count of learned ranks (same value) if (clicked && canLearn && prereqsMet) { - talentConfirmOpen_ = true; - pendingTalentId_ = talent.talentId; - pendingTalentRank_ = currentRank; - uint32_t nextSpell = (currentRank < 5) ? talent.rankSpells[currentRank] : 0; - pendingTalentName_ = nextSpell ? gameHandler.getSpellName(nextSpell) : ""; - if (pendingTalentName_.empty()) - pendingTalentName_ = spellId ? gameHandler.getSpellName(spellId) : "Talent"; + gameHandler.learnTalent(talent.talentId, currentRank); } ImGui::PopID();