mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +00:00
chore(refactor): god-object decomposition and mega-file splits
Split all mega-files by single-responsibility concern and partially extracting AudioCoordinator and OverlaySystem from the Renderer facade. No behavioral changes. Splits: - game_handler.cpp (5,247 LOC) → core + callbacks + packets (3 files) - world_packets.cpp (4,453 LOC) → economy/entity/social/world (4 files) - game_screen.cpp (5,786 LOC) → core + frames + hud + minimap (4 files) - m2_renderer.cpp (3,343 LOC) → core + instance + particles + render (4 files) - chat_panel.cpp (3,140 LOC) → core + commands + utils (3 files) - entity_spawner.cpp (2,750 LOC) → core + player + processing (3 files) Extractions: - AudioCoordinator: include/audio/ + src/audio/ (owned by Renderer) - OverlaySystem: include/rendering/ + src/rendering/overlay_system.* CMakeLists.txt: registered all 17 new translation units. Related handler/callback files: minor include fixups post-split.
This commit is contained in:
parent
6dcc06697b
commit
34c0e3ca28
49 changed files with 29113 additions and 28109 deletions
|
|
@ -488,6 +488,8 @@ set(WOWEE_SOURCES
|
|||
# Core
|
||||
src/core/application.cpp
|
||||
src/core/entity_spawner.cpp
|
||||
src/core/entity_spawner_player.cpp
|
||||
src/core/entity_spawner_processing.cpp
|
||||
src/core/appearance_composer.cpp
|
||||
src/core/world_loader.cpp
|
||||
src/core/npc_interaction_callback_handler.cpp
|
||||
|
|
@ -525,6 +527,8 @@ set(WOWEE_SOURCES
|
|||
src/game/opcode_table.cpp
|
||||
src/game/update_field_table.cpp
|
||||
src/game/game_handler.cpp
|
||||
src/game/game_handler_packets.cpp
|
||||
src/game/game_handler_callbacks.cpp
|
||||
src/game/chat_handler.cpp
|
||||
src/game/movement_handler.cpp
|
||||
src/game/combat_handler.cpp
|
||||
|
|
@ -544,6 +548,10 @@ set(WOWEE_SOURCES
|
|||
src/game/entity.cpp
|
||||
src/game/opcodes.cpp
|
||||
src/game/world_packets.cpp
|
||||
src/game/world_packets_social.cpp
|
||||
src/game/world_packets_entity.cpp
|
||||
src/game/world_packets_world.cpp
|
||||
src/game/world_packets_economy.cpp
|
||||
src/game/packet_parsers_tbc.cpp
|
||||
src/game/packet_parsers_classic.cpp
|
||||
src/game/character.cpp
|
||||
|
|
@ -611,6 +619,9 @@ set(WOWEE_SOURCES
|
|||
src/rendering/character_preview.cpp
|
||||
src/rendering/wmo_renderer.cpp
|
||||
src/rendering/m2_renderer.cpp
|
||||
src/rendering/m2_renderer_render.cpp
|
||||
src/rendering/m2_renderer_particles.cpp
|
||||
src/rendering/m2_renderer_instance.cpp
|
||||
src/rendering/m2_model_classifier.cpp
|
||||
src/rendering/render_graph.cpp
|
||||
src/rendering/quest_marker_renderer.cpp
|
||||
|
|
@ -622,6 +633,7 @@ set(WOWEE_SOURCES
|
|||
src/rendering/charge_effect.cpp
|
||||
src/rendering/spell_visual_system.cpp
|
||||
src/rendering/post_process_pipeline.cpp
|
||||
src/rendering/overlay_system.cpp
|
||||
src/rendering/animation_controller.cpp
|
||||
src/rendering/animation/animation_ids.cpp
|
||||
src/rendering/animation/emote_registry.cpp
|
||||
|
|
@ -643,7 +655,12 @@ set(WOWEE_SOURCES
|
|||
src/ui/character_create_screen.cpp
|
||||
src/ui/character_screen.cpp
|
||||
src/ui/game_screen.cpp
|
||||
src/ui/game_screen_frames.cpp
|
||||
src/ui/game_screen_hud.cpp
|
||||
src/ui/game_screen_minimap.cpp
|
||||
src/ui/chat_panel.cpp
|
||||
src/ui/chat_panel_commands.cpp
|
||||
src/ui/chat_panel_utils.cpp
|
||||
src/ui/toast_manager.cpp
|
||||
src/ui/dialog_manager.cpp
|
||||
src/ui/settings_panel.cpp
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace game { class ZoneManager; }
|
||||
namespace audio {
|
||||
|
||||
class MusicManager;
|
||||
|
|
@ -17,6 +21,26 @@ class CombatSoundManager;
|
|||
class SpellSoundManager;
|
||||
class MovementSoundManager;
|
||||
|
||||
/// Flat context passed from Renderer into updateZoneAudio() each frame.
|
||||
/// All values are pre-queried so AudioCoordinator needs no rendering pointers.
|
||||
struct ZoneAudioContext {
|
||||
float deltaTime = 0.0f;
|
||||
glm::vec3 cameraPosition{0.0f};
|
||||
bool isSwimming = false;
|
||||
bool insideWmo = false;
|
||||
uint32_t insideWmoId = 0;
|
||||
// Visual weather state for ambient audio sync
|
||||
int weatherType = 0; // 0=none, 1=rain, 2=snow, 3=storm
|
||||
float weatherIntensity = 0.0f;
|
||||
// Terrain tile for offline zone lookup
|
||||
int tileX = 0, tileY = 0;
|
||||
bool hasTile = false;
|
||||
// Server-authoritative zone (from SMSG_INIT_WORLD_STATES); 0 = offline
|
||||
uint32_t serverZoneId = 0;
|
||||
// Zone manager pointer (for zone info and music queries)
|
||||
game::ZoneManager* zoneManager = nullptr;
|
||||
};
|
||||
|
||||
/// Coordinates all audio subsystems.
|
||||
/// Extracted from Renderer to separate audio lifecycle from rendering.
|
||||
/// Owned by Application; Renderer and UI components access through Application.
|
||||
|
|
@ -35,6 +59,13 @@ public:
|
|||
/// Shutdown all audio managers and engine.
|
||||
void shutdown();
|
||||
|
||||
/// Per-frame zone detection, music transitions, and ambient weather sync.
|
||||
/// Called from Renderer::update() with a pre-filled context.
|
||||
void updateZoneAudio(const ZoneAudioContext& ctx);
|
||||
|
||||
const std::string& getCurrentZoneName() const { return currentZoneName_; }
|
||||
uint32_t getCurrentZoneId() const { return currentZoneId_; }
|
||||
|
||||
// Accessors for all audio managers (same interface as Renderer had)
|
||||
MusicManager* getMusicManager() { return musicManager_.get(); }
|
||||
FootstepManager* getFootstepManager() { return footstepManager_.get(); }
|
||||
|
|
@ -48,6 +79,8 @@ public:
|
|||
MovementSoundManager* getMovementSoundManager() { return movementSoundManager_.get(); }
|
||||
|
||||
private:
|
||||
void playZoneMusic(const std::string& music);
|
||||
|
||||
std::unique_ptr<MusicManager> musicManager_;
|
||||
std::unique_ptr<FootstepManager> footstepManager_;
|
||||
std::unique_ptr<ActivitySoundManager> activitySoundManager_;
|
||||
|
|
@ -60,6 +93,13 @@ private:
|
|||
std::unique_ptr<MovementSoundManager> movementSoundManager_;
|
||||
|
||||
bool audioAvailable_ = false;
|
||||
|
||||
// Zone/music state — moved from Renderer
|
||||
uint32_t currentZoneId_ = 0;
|
||||
std::string currentZoneName_;
|
||||
bool inTavern_ = false;
|
||||
bool inBlacksmith_ = false;
|
||||
float musicSwitchCooldown_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
|
|
|
|||
|
|
@ -1825,7 +1825,11 @@ public:
|
|||
}
|
||||
// Convenience: invoke a callback with a sound manager obtained from the renderer.
|
||||
template<typename ManagerGetter, typename Callback>
|
||||
void withSoundManager(ManagerGetter getter, Callback cb);
|
||||
void withSoundManager(ManagerGetter getter, Callback cb) {
|
||||
if (auto* ac = services_.audioCoordinator) {
|
||||
if (auto* mgr = (ac->*getter)()) cb(mgr);
|
||||
}
|
||||
}
|
||||
|
||||
// Reputation change toast: factionName, delta, new standing
|
||||
using RepChangeCallback = std::function<void(const std::string& factionName, int32_t delta, int32_t standing)>;
|
||||
|
|
@ -2138,17 +2142,370 @@ public:
|
|||
*/
|
||||
void resetDbcCaches();
|
||||
|
||||
private:
|
||||
friend class ChatHandler;
|
||||
friend class MovementHandler;
|
||||
friend class CombatHandler;
|
||||
friend class SpellHandler;
|
||||
friend class InventoryHandler;
|
||||
friend class SocialHandler;
|
||||
friend class QuestHandler;
|
||||
friend class WardenHandler;
|
||||
friend class EntityController;
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Domain handler access — public accessors for friend-class elimination
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Handler & Subsystem Accessors (unique_ptr → raw pointer) ─────
|
||||
network::WorldSocket* getSocket() { return socket.get(); }
|
||||
const network::WorldSocket* getSocket() const { return socket.get(); }
|
||||
ChatHandler* getChatHandler() { return chatHandler_.get(); }
|
||||
CombatHandler* getCombatHandler() { return combatHandler_.get(); }
|
||||
MovementHandler* getMovementHandler() { return movementHandler_.get(); }
|
||||
SpellHandler* getSpellHandler() { return spellHandler_.get(); }
|
||||
|
||||
// ── Mutable Accessors for Members with Existing Const Getters ────
|
||||
void setTargetGuidRaw(uint64_t g) { targetGuid = g; }
|
||||
uint64_t& lastTargetGuidRef() { return lastTargetGuid; }
|
||||
uint64_t& focusGuidRef() { return focusGuid; }
|
||||
uint64_t& mouseoverGuidRef() { return mouseoverGuid_; }
|
||||
MovementInfo& movementInfoRef() { return movementInfo; }
|
||||
Inventory& inventoryRef() { return inventory; }
|
||||
|
||||
// ── Core / Session ───────────────────────────────────────────────
|
||||
uint32_t getBuild() const { return build; }
|
||||
const std::vector<uint8_t>& getSessionKey() const { return sessionKey; }
|
||||
auto& charactersRef() { return characters; }
|
||||
auto& updateFieldTableRef() { return updateFieldTable_; }
|
||||
auto& lastPlayerFieldsRef() { return lastPlayerFields_; }
|
||||
auto& timeSinceLastPingRef() { return timeSinceLastPing; }
|
||||
auto& activeCharacterGuidRef() { return activeCharacterGuid_; }
|
||||
|
||||
// ── Character & Appearance ───────────────────────────────────────
|
||||
auto& chosenTitleBitRef() { return chosenTitleBit_; }
|
||||
auto& cloakVisibleRef() { return cloakVisible_; }
|
||||
auto& helmVisibleRef() { return helmVisible_; }
|
||||
auto& currentMountDisplayIdRef() { return currentMountDisplayId_; }
|
||||
auto& mountAuraSpellIdRef() { return mountAuraSpellId_; }
|
||||
auto& shapeshiftFormIdRef() { return shapeshiftFormId_; }
|
||||
auto& playerRaceRef() { return playerRace_; }
|
||||
auto& serverPlayerLevelRef() { return serverPlayerLevel_; }
|
||||
|
||||
// ── AFK / DND ────────────────────────────────────────────────────
|
||||
auto& afkMessageRef() { return afkMessage_; }
|
||||
auto& afkStatusRef() { return afkStatus_; }
|
||||
auto& dndMessageRef() { return dndMessage_; }
|
||||
auto& dndStatusRef() { return dndStatus_; }
|
||||
|
||||
// ── Movement & Transport ─────────────────────────────────────────
|
||||
auto& followRenderPosRef() { return followRenderPos_; }
|
||||
auto& followTargetGuidRef() { return followTargetGuid_; }
|
||||
auto& serverRunSpeedRef() { return serverRunSpeed_; }
|
||||
auto& onTaxiFlightRef() { return onTaxiFlight_; }
|
||||
auto& taxiLandingCooldownRef() { return taxiLandingCooldown_; }
|
||||
auto& taxiMountActiveRef() { return taxiMountActive_; }
|
||||
auto& taxiStartGraceRef() { return taxiStartGrace_; }
|
||||
auto& vehicleIdRef() { return vehicleId_; }
|
||||
auto& playerTransportGuidRef() { return playerTransportGuid_; }
|
||||
auto& playerTransportOffsetRef() { return playerTransportOffset_; }
|
||||
auto& playerTransportStickyGuidRef() { return playerTransportStickyGuid_; }
|
||||
auto& playerTransportStickyTimerRef() { return playerTransportStickyTimer_; }
|
||||
auto& transportAttachmentsRef() { return transportAttachments_; }
|
||||
|
||||
// ── Inventory & Equipment ────────────────────────────────────────
|
||||
auto& actionBarRef() { return actionBar; }
|
||||
auto& backpackSlotGuidsRef() { return backpackSlotGuids_; }
|
||||
auto& equipSlotGuidsRef() { return equipSlotGuids_; }
|
||||
auto& keyringSlotGuidsRef() { return keyringSlotGuids_; }
|
||||
auto& containerContentsRef() { return containerContents_; }
|
||||
auto& invSlotBaseRef() { return invSlotBase_; }
|
||||
auto& packSlotBaseRef() { return packSlotBase_; }
|
||||
auto& visibleItemEntryBaseRef() { return visibleItemEntryBase_; }
|
||||
auto& visibleItemLayoutVerifiedRef() { return visibleItemLayoutVerified_; }
|
||||
auto& visibleItemStrideRef() { return visibleItemStride_; }
|
||||
auto& itemInfoCacheRef() { return itemInfoCache_; }
|
||||
auto& lastEquipDisplayIdsRef() { return lastEquipDisplayIds_; }
|
||||
auto& onlineEquipDirtyRef() { return onlineEquipDirty_; }
|
||||
auto& onlineItemsRef() { return onlineItems_; }
|
||||
auto& inspectedPlayerItemEntriesRef() { return inspectedPlayerItemEntries_; }
|
||||
auto& otherPlayerVisibleDirtyRef() { return otherPlayerVisibleDirty_; }
|
||||
auto& otherPlayerVisibleItemEntriesRef() { return otherPlayerVisibleItemEntries_; }
|
||||
auto& otherPlayerMoveTimeMsRef() { return otherPlayerMoveTimeMs_; }
|
||||
auto& pendingItemPushNotifsRef() { return pendingItemPushNotifs_; }
|
||||
auto& pendingItemQueriesRef() { return pendingItemQueries_; }
|
||||
auto& pendingMoneyDeltaRef() { return pendingMoneyDelta_; }
|
||||
auto& pendingMoneyDeltaTimerRef() { return pendingMoneyDeltaTimer_; }
|
||||
auto& pendingAutoInspectRef() { return pendingAutoInspect_; }
|
||||
auto& pendingGameObjectLootRetriesRef() { return pendingGameObjectLootRetries_; }
|
||||
auto& tempEnchantTimersRef() { return tempEnchantTimers_; }
|
||||
auto& localLootStateRef() { return localLootState_; }
|
||||
static const auto& getTempEnchantSlotNames() { return kTempEnchantSlotNames; }
|
||||
|
||||
// ── Combat & Player Stats ────────────────────────────────────────
|
||||
auto& comboPointsRef() { return comboPoints_; }
|
||||
auto& comboTargetRef() { return comboTarget_; }
|
||||
auto& isRestingRef() { return isResting_; }
|
||||
auto& playerArenaPointsRef() { return playerArenaPoints_; }
|
||||
auto& playerArmorRatingRef() { return playerArmorRating_; }
|
||||
auto& playerBlockPctRef() { return playerBlockPct_; }
|
||||
auto& playerCombatRatingsRef() { return playerCombatRatings_; }
|
||||
auto& playerCritPctRef() { return playerCritPct_; }
|
||||
auto& playerDodgePctRef() { return playerDodgePct_; }
|
||||
auto& playerHealBonusRef() { return playerHealBonus_; }
|
||||
auto& playerHonorPointsRef() { return playerHonorPoints_; }
|
||||
auto& playerMeleeAPRef() { return playerMeleeAP_; }
|
||||
auto& playerMoneyCopperRef() { return playerMoneyCopper_; }
|
||||
auto& playerNextLevelXpRef() { return playerNextLevelXp_; }
|
||||
auto& playerParryPctRef() { return playerParryPct_; }
|
||||
auto& playerRangedAPRef() { return playerRangedAP_; }
|
||||
auto& playerRangedCritPctRef() { return playerRangedCritPct_; }
|
||||
auto* playerResistancesArr() { return playerResistances_; }
|
||||
auto& playerRestedXpRef() { return playerRestedXp_; }
|
||||
auto* playerSpellCritPctArr() { return playerSpellCritPct_; }
|
||||
auto* playerSpellDmgBonusArr() { return playerSpellDmgBonus_; }
|
||||
auto& playerStatsArr() { return playerStats_; }
|
||||
auto& playerXpRef() { return playerXp_; }
|
||||
|
||||
// ── Skills ───────────────────────────────────────────────────────
|
||||
auto& playerSkillsRef() { return playerSkills_; }
|
||||
auto& skillLineAbilityLoadedRef() { return skillLineAbilityLoaded_; }
|
||||
auto& skillLineCategoriesRef() { return skillLineCategories_; }
|
||||
auto& skillLineDbcLoadedRef() { return skillLineDbcLoaded_; }
|
||||
auto& skillLineNamesRef() { return skillLineNames_; }
|
||||
auto& spellToSkillLineRef() { return spellToSkillLine_; }
|
||||
|
||||
// ── Spells & Talents ─────────────────────────────────────────────
|
||||
auto& activeTalentSpecRef() { return activeTalentSpec_; }
|
||||
auto* unspentTalentPointsArr() { return unspentTalentPoints_; }
|
||||
auto* learnedTalentsArr() { return learnedTalents_; }
|
||||
auto& learnedGlyphsRef() { return learnedGlyphs_; }
|
||||
auto& talentsInitializedRef() { return talentsInitialized_; }
|
||||
auto& spellFlatModsRef() { return spellFlatMods_; }
|
||||
auto& spellPctModsRef() { return spellPctMods_; }
|
||||
auto& spellNameCacheRef() { return spellNameCache_; }
|
||||
auto& spellNameCacheLoadedRef() { return spellNameCacheLoaded_; }
|
||||
|
||||
// ── Quests & Achievements ────────────────────────────────────────
|
||||
auto& completedQuestsRef() { return completedQuests_; }
|
||||
auto& npcQuestStatusRef() { return npcQuestStatus_; }
|
||||
auto& achievementDatesRef() { return achievementDates_; }
|
||||
auto& achievementNameCacheRef() { return achievementNameCache_; }
|
||||
auto& earnedAchievementsRef() { return earnedAchievements_; }
|
||||
|
||||
// ── Social, Chat & Contacts ──────────────────────────────────────
|
||||
auto& contactsRef() { return contacts_; }
|
||||
auto& friendGuidsRef() { return friendGuids_; }
|
||||
auto& friendsCacheRef() { return friendsCache; }
|
||||
auto& ignoreCacheRef() { return ignoreCache; }
|
||||
auto& ignoreListGuidsRef() { return ignoreListGuids_; }
|
||||
auto& lastContactListCountRef() { return lastContactListCount_; }
|
||||
auto& lastContactListMaskRef() { return lastContactListMask_; }
|
||||
auto& lastWhisperSenderRef() { return lastWhisperSender_; }
|
||||
auto& lastWhisperSenderGuidRef() { return lastWhisperSenderGuid_; }
|
||||
auto& mailInboxRef() { return mailInbox_; }
|
||||
|
||||
// ── World, Map & Zones ───────────────────────────────────────────
|
||||
auto& currentMapIdRef() { return currentMapId_; }
|
||||
auto& inInstanceRef() { return inInstance_; }
|
||||
auto& worldStateMapIdRef() { return worldStateMapId_; }
|
||||
auto& worldStatesRef() { return worldStates_; }
|
||||
auto& worldStateZoneIdRef() { return worldStateZoneId_; }
|
||||
auto& minimapPingsRef() { return minimapPings_; }
|
||||
auto& gossipPoisRef() { return gossipPois_; }
|
||||
auto& playerExploredZonesRef() { return playerExploredZones_; }
|
||||
auto& hasPlayerExploredZonesRef() { return hasPlayerExploredZones_; }
|
||||
auto& factionStandingsRef() { return factionStandings_; }
|
||||
auto& initialFactionsRef() { return initialFactions_; }
|
||||
auto& watchedFactionIdRef() { return watchedFactionId_; }
|
||||
|
||||
// ── Corpse & Home Bind ───────────────────────────────────────────
|
||||
auto& corpseGuidRef() { return corpseGuid_; }
|
||||
auto& corpseMapIdRef() { return corpseMapId_; }
|
||||
auto& corpseReclaimAvailableMsRef() { return corpseReclaimAvailableMs_; }
|
||||
auto& corpseXRef() { return corpseX_; }
|
||||
auto& corpseYRef() { return corpseY_; }
|
||||
auto& corpseZRef() { return corpseZ_; }
|
||||
auto& hasHomeBindRef() { return hasHomeBind_; }
|
||||
auto& homeBindMapIdRef() { return homeBindMapId_; }
|
||||
auto& homeBindPosRef() { return homeBindPos_; }
|
||||
|
||||
// ── Area Triggers ────────────────────────────────────────────────
|
||||
auto& activeAreaTriggersRef() { return activeAreaTriggers_; }
|
||||
auto& areaTriggerCheckTimerRef() { return areaTriggerCheckTimer_; }
|
||||
auto& areaTriggerDbcLoadedRef() { return areaTriggerDbcLoaded_; }
|
||||
auto& areaTriggerMsgsRef() { return areaTriggerMsgs_; }
|
||||
auto& areaTriggersRef() { return areaTriggers_; }
|
||||
auto& areaTriggerSuppressFirstRef() { return areaTriggerSuppressFirst_; }
|
||||
|
||||
// ── Death & Resurrection ─────────────────────────────────────────
|
||||
auto& playerDeadRef() { return playerDead_; }
|
||||
auto& releasedSpiritRef() { return releasedSpirit_; }
|
||||
auto& repopPendingRef() { return repopPending_; }
|
||||
auto& lastRepopRequestMsRef() { return lastRepopRequestMs_; }
|
||||
auto& pendingSpiritHealerGuidRef() { return pendingSpiritHealerGuid_; }
|
||||
auto& resurrectCasterGuidRef() { return resurrectCasterGuid_; }
|
||||
auto& resurrectIsSpiritHealerRef() { return resurrectIsSpiritHealer_; }
|
||||
auto& resurrectPendingRef() { return resurrectPending_; }
|
||||
auto& resurrectRequestPendingRef() { return resurrectRequestPending_; }
|
||||
auto& selfResAvailableRef() { return selfResAvailable_; }
|
||||
|
||||
// ── Summon & Battlefield ─────────────────────────────────────────
|
||||
auto& pendingSummonRequestRef() { return pendingSummonRequest_; }
|
||||
auto& summonerGuidRef() { return summonerGuid_; }
|
||||
auto& summonerNameRef() { return summonerName_; }
|
||||
auto& summonTimeoutSecRef() { return summonTimeoutSec_; }
|
||||
auto& bfMgrInvitePendingRef() { return bfMgrInvitePending_; }
|
||||
|
||||
// ── Pet & Stable ─────────────────────────────────────────────────
|
||||
auto& petActionSlotsRef() { return petActionSlots_; }
|
||||
auto& petAutocastSpellsRef() { return petAutocastSpells_; }
|
||||
auto& petCommandRef() { return petCommand_; }
|
||||
auto& petGuidRef() { return petGuid_; }
|
||||
auto& petReactRef() { return petReact_; }
|
||||
auto& petSpellListRef() { return petSpellList_; }
|
||||
auto& stabledPetsRef() { return stabledPets_; }
|
||||
auto& stableMasterGuidRef() { return stableMasterGuid_; }
|
||||
auto& stableNumSlotsRef() { return stableNumSlots_; }
|
||||
auto& stableWindowOpenRef() { return stableWindowOpen_; }
|
||||
|
||||
// ── Trainer, GM & Misc ───────────────────────────────────────────
|
||||
auto& currentTrainerListRef() { return currentTrainerList_; }
|
||||
auto& trainerTabsRef() { return trainerTabs_; }
|
||||
auto& gmTicketActiveRef() { return gmTicketActive_; }
|
||||
auto& gmTicketTextRef() { return gmTicketText_; }
|
||||
auto& bookPagesRef() { return bookPages_; }
|
||||
auto& activeTotemSlotsRef() { return activeTotemSlots_; }
|
||||
auto& unitAurasCacheRef() { return unitAurasCache_; }
|
||||
auto& lastInteractedGoGuidRef() { return lastInteractedGoGuid_; }
|
||||
auto& pendingGameObjectInteractGuidRef() { return pendingGameObjectInteractGuid_; }
|
||||
|
||||
// ── Tab Cycling ──────────────────────────────────────────────────
|
||||
auto& tabCycleIndexRef() { return tabCycleIndex; }
|
||||
auto& tabCycleListRef() { return tabCycleList; }
|
||||
auto& tabCycleStaleRef() { return tabCycleStale; }
|
||||
|
||||
// ── UI & Event Callbacks ─────────────────────────────────────────
|
||||
auto& achievementEarnedCallbackRef() { return achievementEarnedCallback_; }
|
||||
auto& addonChatCallbackRef() { return addonChatCallback_; }
|
||||
auto& addonEventCallbackRef() { return addonEventCallback_; }
|
||||
auto& appearanceChangedCallbackRef() { return appearanceChangedCallback_; }
|
||||
auto& autoFollowCallbackRef() { return autoFollowCallback_; }
|
||||
auto& chargeCallbackRef() { return chargeCallback_; }
|
||||
auto& chatBubbleCallbackRef() { return chatBubbleCallback_; }
|
||||
auto& creatureDespawnCallbackRef() { return creatureDespawnCallback_; }
|
||||
auto& creatureMoveCallbackRef() { return creatureMoveCallback_; }
|
||||
auto& creatureSpawnCallbackRef() { return creatureSpawnCallback_; }
|
||||
auto& emoteAnimCallbackRef() { return emoteAnimCallback_; }
|
||||
auto& gameObjectDespawnCallbackRef() { return gameObjectDespawnCallback_; }
|
||||
auto& gameObjectMoveCallbackRef() { return gameObjectMoveCallback_; }
|
||||
auto& gameObjectSpawnCallbackRef() { return gameObjectSpawnCallback_; }
|
||||
auto& gameObjectStateCallbackRef() { return gameObjectStateCallback_; }
|
||||
auto& ghostStateCallbackRef() { return ghostStateCallback_; }
|
||||
auto& hearthstonePreloadCallbackRef() { return hearthstonePreloadCallback_; }
|
||||
auto& hitReactionCallbackRef() { return hitReactionCallback_; }
|
||||
auto& itemLootCallbackRef() { return itemLootCallback_; }
|
||||
auto& knockBackCallbackRef() { return knockBackCallback_; }
|
||||
auto& lootWindowCallbackRef() { return lootWindowCallback_; }
|
||||
auto& meleeSwingCallbackRef() { return meleeSwingCallback_; }
|
||||
auto& mountCallbackRef() { return mountCallback_; }
|
||||
auto& npcAggroCallbackRef() { return npcAggroCallback_; }
|
||||
auto& npcDeathCallbackRef() { return npcDeathCallback_; }
|
||||
auto& npcFarewellCallbackRef() { return npcFarewellCallback_; }
|
||||
auto& npcGreetingCallbackRef() { return npcGreetingCallback_; }
|
||||
auto& npcRespawnCallbackRef() { return npcRespawnCallback_; }
|
||||
auto& npcSwingCallbackRef() { return npcSwingCallback_; }
|
||||
auto& npcVendorCallbackRef() { return npcVendorCallback_; }
|
||||
auto& openLfgCallbackRef() { return openLfgCallback_; }
|
||||
auto& otherPlayerLevelUpCallbackRef() { return otherPlayerLevelUpCallback_; }
|
||||
auto& playerDespawnCallbackRef() { return playerDespawnCallback_; }
|
||||
auto& playerEquipmentCallbackRef() { return playerEquipmentCallback_; }
|
||||
auto& playerHealthCallbackRef() { return playerHealthCallback_; }
|
||||
auto& playerSpawnCallbackRef() { return playerSpawnCallback_; }
|
||||
auto& pvpHonorCallbackRef() { return pvpHonorCallback_; }
|
||||
auto& questCompleteCallbackRef() { return questCompleteCallback_; }
|
||||
auto& questProgressCallbackRef() { return questProgressCallback_; }
|
||||
auto& repChangeCallbackRef() { return repChangeCallback_; }
|
||||
auto& spellCastAnimCallbackRef() { return spellCastAnimCallback_; }
|
||||
auto& spellCastFailedCallbackRef() { return spellCastFailedCallback_; }
|
||||
auto& sprintAuraCallbackRef() { return sprintAuraCallback_; }
|
||||
auto& stealthStateCallbackRef() { return stealthStateCallback_; }
|
||||
auto& stunStateCallbackRef() { return stunStateCallback_; }
|
||||
auto& taxiFlightStartCallbackRef() { return taxiFlightStartCallback_; }
|
||||
auto& taxiOrientationCallbackRef() { return taxiOrientationCallback_; }
|
||||
auto& taxiPrecacheCallbackRef() { return taxiPrecacheCallback_; }
|
||||
auto& transportMoveCallbackRef() { return transportMoveCallback_; }
|
||||
auto& unitAnimHintCallbackRef() { return unitAnimHintCallback_; }
|
||||
auto& unitMoveFlagsCallbackRef() { return unitMoveFlagsCallback_; }
|
||||
auto& worldEntryCallbackRef() { return worldEntryCallback_; }
|
||||
|
||||
// ── Methods moved from private (domain handler use) ──────────────
|
||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId,
|
||||
bool isPlayerSource, uint8_t powerType = 0,
|
||||
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
|
||||
bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId);
|
||||
void addSystemChatMessage(const std::string& message);
|
||||
void sendPing();
|
||||
void setTransportAttachment(uint64_t childGuid, ObjectType type,
|
||||
uint64_t transportGuid, const glm::vec3& localOffset,
|
||||
bool hasLocalOrientation, float localOrientation);
|
||||
void clearTransportAttachment(uint64_t childGuid);
|
||||
std::string guidToUnitId(uint64_t guid) const;
|
||||
Unit* getUnitByGuid(uint64_t guid);
|
||||
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
|
||||
void rebuildOnlineInventory();
|
||||
void maybeDetectVisibleItemLayout();
|
||||
void updateOtherPlayerVisibleItems(uint64_t guid, const std::map<uint16_t, uint32_t>& fields);
|
||||
void detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields);
|
||||
bool applyInventoryFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& fields);
|
||||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void sanitizeMovementForTaxi();
|
||||
void loadSpellNameCache() const;
|
||||
void loadFactionNameCache() const;
|
||||
void loadAchievementNameCache();
|
||||
void loadSkillLineDbc();
|
||||
void loadSkillLineAbilityDbc();
|
||||
std::string getFactionName(uint32_t factionId) const;
|
||||
std::string getLfgDungeonName(uint32_t dungeonId) const;
|
||||
void queryItemInfo(uint32_t entry, uint64_t guid);
|
||||
|
||||
// --- Inner types exposed for former friend classes ---
|
||||
struct TransportAttachment {
|
||||
ObjectType type = ObjectType::OBJECT;
|
||||
uint64_t transportGuid = 0;
|
||||
glm::vec3 localOffset{0.0f};
|
||||
float localOrientation = 0.0f;
|
||||
bool hasLocalOrientation = false;
|
||||
};
|
||||
struct AreaTriggerEntry {
|
||||
uint32_t id = 0;
|
||||
uint32_t mapId = 0;
|
||||
float x = 0, y = 0, z = 0;
|
||||
float radius = 0;
|
||||
float boxLength = 0, boxWidth = 0, boxHeight = 0;
|
||||
float boxYaw = 0;
|
||||
};
|
||||
struct PendingLootRetry {
|
||||
uint64_t guid = 0;
|
||||
float timer = 0.0f;
|
||||
uint8_t remainingRetries = 0;
|
||||
bool sendLoot = false;
|
||||
};
|
||||
struct SpellNameEntry {
|
||||
std::string name; std::string rank; std::string description;
|
||||
uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 0;
|
||||
int32_t effectBasePoints[3] = {0, 0, 0};
|
||||
float durationSec = 0.0f;
|
||||
};
|
||||
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;
|
||||
std::string getAreaName(uint32_t areaId) const;
|
||||
struct OnlineItemInfo {
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackCount = 1;
|
||||
uint32_t curDurability = 0;
|
||||
uint32_t maxDurability = 0;
|
||||
uint32_t permanentEnchantId = 0;
|
||||
uint32_t temporaryEnchantId = 0;
|
||||
std::array<uint32_t, 3> socketEnchantIds{};
|
||||
};
|
||||
bool isHostileFaction(uint32_t factionTemplateId) const {
|
||||
auto it = factionHostileMap_.find(factionTemplateId);
|
||||
return it != factionHostileMap_.end() ? it->second : true;
|
||||
}
|
||||
|
||||
private:
|
||||
// Dead: autoTargetAttacker moved to CombatHandler
|
||||
|
||||
/**
|
||||
|
|
@ -2223,16 +2580,8 @@ private:
|
|||
void handlePong(network::Packet& packet);
|
||||
|
||||
void handleItemQueryResponse(network::Packet& packet);
|
||||
void queryItemInfo(uint32_t entry, uint64_t guid);
|
||||
void rebuildOnlineInventory();
|
||||
void maybeDetectVisibleItemLayout();
|
||||
void updateOtherPlayerVisibleItems(uint64_t guid, const std::map<uint16_t, uint32_t>& fields);
|
||||
void emitOtherPlayerEquipment(uint64_t guid);
|
||||
void emitAllOtherPlayerEquipment();
|
||||
void detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields);
|
||||
bool applyInventoryFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& fields);
|
||||
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
|
||||
|
||||
// handleAttackStart, handleAttackStop, handleAttackerStateUpdate,
|
||||
// handleSpellDamageLog, handleSpellHealLog removed
|
||||
|
|
@ -2257,8 +2606,6 @@ private:
|
|||
void clearPendingQuestAccept(uint32_t questId);
|
||||
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);
|
||||
bool hasQuestInLog(uint32_t questId) const;
|
||||
std::string guidToUnitId(uint64_t guid) const;
|
||||
Unit* getUnitByGuid(uint64_t guid);
|
||||
std::string getQuestTitle(uint32_t questId) const;
|
||||
const QuestLogEntry* findQuestLogEntry(uint32_t questId) const;
|
||||
int findQuestLogSlotIndexFromServer(uint32_t questId) const;
|
||||
|
|
@ -2302,15 +2649,7 @@ private:
|
|||
|
||||
// ---- Logout handlers ----
|
||||
|
||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType = 0,
|
||||
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
|
||||
bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId);
|
||||
void addSystemChatMessage(const std::string& message);
|
||||
|
||||
/**
|
||||
* Send CMSG_PING to server (heartbeat)
|
||||
*/
|
||||
void sendPing();
|
||||
|
||||
/**
|
||||
* Send CMSG_AUTH_SESSION to server
|
||||
|
|
@ -2332,10 +2671,6 @@ private:
|
|||
*/
|
||||
void fail(const std::string& reason);
|
||||
void updateAttachedTransportChildren(float deltaTime);
|
||||
void setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid,
|
||||
const glm::vec3& localOffset, bool hasLocalOrientation,
|
||||
float localOrientation);
|
||||
void clearTransportAttachment(uint64_t childGuid);
|
||||
|
||||
// Explicit service dependencies (owned by Application)
|
||||
GameServices& services_;
|
||||
|
|
@ -2487,15 +2822,6 @@ private:
|
|||
uint64_t lastWhisperSenderGuid_ = 0;
|
||||
|
||||
// ---- Online item tracking ----
|
||||
struct OnlineItemInfo {
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackCount = 1;
|
||||
uint32_t curDurability = 0;
|
||||
uint32_t maxDurability = 0;
|
||||
uint32_t permanentEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 0 (enchanting)
|
||||
uint32_t temporaryEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 1 (sharpening stones, poisons)
|
||||
std::array<uint32_t, 3> socketEnchantIds{}; // ITEM_ENCHANTMENT_SLOT 2-4 (gems)
|
||||
};
|
||||
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
|
||||
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
|
||||
std::unordered_set<uint32_t> pendingItemQueries_;
|
||||
|
|
@ -2576,13 +2902,6 @@ private:
|
|||
VehicleStateCallback vehicleStateCallback_;
|
||||
|
||||
// Transport tracking
|
||||
struct TransportAttachment {
|
||||
ObjectType type = ObjectType::OBJECT;
|
||||
uint64_t transportGuid = 0;
|
||||
glm::vec3 localOffset{0.0f};
|
||||
float localOrientation = 0.0f;
|
||||
bool hasLocalOrientation = false;
|
||||
};
|
||||
std::unordered_map<uint64_t, TransportAttachment> transportAttachments_;
|
||||
// Transport GUID tracking moved to EntityController
|
||||
uint64_t playerTransportGuid_ = 0; // Transport the player is riding (0 = none)
|
||||
|
|
@ -2606,14 +2925,6 @@ private:
|
|||
bool talentsInitialized_ = false; // Reset on world entry; guards first-spec selection
|
||||
|
||||
// ---- Area trigger detection ----
|
||||
struct AreaTriggerEntry {
|
||||
uint32_t id = 0;
|
||||
uint32_t mapId = 0;
|
||||
float x = 0, y = 0, z = 0; // canonical WoW coords (converted from DBC)
|
||||
float radius = 0;
|
||||
float boxLength = 0, boxWidth = 0, boxHeight = 0;
|
||||
float boxYaw = 0;
|
||||
};
|
||||
bool areaTriggerDbcLoaded_ = false;
|
||||
std::vector<AreaTriggerEntry> areaTriggers_;
|
||||
std::unordered_set<uint32_t> activeAreaTriggers_; // triggers player is currently inside
|
||||
|
|
@ -2709,8 +3020,6 @@ private:
|
|||
// factionId → repListId reverse mapping
|
||||
mutable std::unordered_map<uint32_t, uint32_t> factionIdToRepList_;
|
||||
mutable bool factionNameCacheLoaded_ = false;
|
||||
void loadFactionNameCache() const;
|
||||
std::string getFactionName(uint32_t factionId) const;
|
||||
|
||||
// ---- Group ----
|
||||
GroupListData partyData;
|
||||
|
|
@ -2800,12 +3109,6 @@ private:
|
|||
bool itemAutoLootSent = false;
|
||||
};
|
||||
std::unordered_map<uint64_t, LocalLootState> localLootState_;
|
||||
struct PendingLootRetry {
|
||||
uint64_t guid = 0;
|
||||
float timer = 0.0f;
|
||||
uint8_t remainingRetries = 0;
|
||||
bool sendLoot = false;
|
||||
};
|
||||
std::vector<PendingLootRetry> pendingGameObjectLootRetries_;
|
||||
struct PendingLootOpen {
|
||||
uint64_t guid = 0;
|
||||
|
|
@ -2879,10 +3182,6 @@ private:
|
|||
|
||||
// Faction hostility lookup (populated from FactionTemplate.dbc)
|
||||
std::unordered_map<uint32_t, bool> factionHostileMap_;
|
||||
bool isHostileFaction(uint32_t factionTemplateId) const {
|
||||
auto it = factionHostileMap_.find(factionTemplateId);
|
||||
return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown
|
||||
}
|
||||
|
||||
// Vehicle (WotLK): non-zero when player is seated in a vehicle
|
||||
uint32_t vehicleId_ = 0;
|
||||
|
|
@ -2916,7 +3215,6 @@ private:
|
|||
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
|
||||
std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper
|
||||
uint32_t nextMovementTimestampMs();
|
||||
void sanitizeMovementForTaxi();
|
||||
void updateClientTaxi(float deltaTime);
|
||||
|
||||
// Mail
|
||||
|
|
@ -2980,12 +3278,6 @@ private:
|
|||
// Trainer
|
||||
bool trainerWindowOpen_ = false;
|
||||
TrainerListData currentTrainerList_;
|
||||
struct SpellNameEntry {
|
||||
std::string name; std::string rank; std::string description;
|
||||
uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 0;
|
||||
int32_t effectBasePoints[3] = {0, 0, 0};
|
||||
float durationSec = 0.0f; // resolved from DurationIndex → SpellDuration.dbc
|
||||
};
|
||||
mutable std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||||
mutable bool spellNameCacheLoaded_ = false;
|
||||
|
||||
|
|
@ -3004,7 +3296,6 @@ private:
|
|||
std::unordered_map<uint32_t, std::string> achievementDescCache_;
|
||||
std::unordered_map<uint32_t, uint32_t> achievementPointsCache_;
|
||||
bool achievementNameCacheLoaded_ = false;
|
||||
void loadAchievementNameCache();
|
||||
// Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
std::unordered_set<uint32_t> earnedAchievements_;
|
||||
// Earn dates: achievementId → WoW PackedTime (from SMSG_ACHIEVEMENT_EARNED / SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
|
|
@ -3021,7 +3312,6 @@ private:
|
|||
mutable std::unordered_map<uint32_t, std::string> areaNameCache_;
|
||||
mutable bool areaNameCacheLoaded_ = false;
|
||||
void loadAreaNameCache() const;
|
||||
std::string getAreaName(uint32_t areaId) const;
|
||||
|
||||
// Map name cache (lazy-loaded from Map.dbc; maps mapId → localized display name)
|
||||
mutable std::unordered_map<uint32_t, std::string> mapNameCache_;
|
||||
|
|
@ -3032,9 +3322,7 @@ private:
|
|||
mutable std::unordered_map<uint32_t, std::string> lfgDungeonNameCache_;
|
||||
mutable bool lfgDungeonNameCacheLoaded_ = false;
|
||||
void loadLfgDungeonDbc() const;
|
||||
std::string getLfgDungeonName(uint32_t dungeonId) const;
|
||||
std::vector<TrainerTab> trainerTabs_;
|
||||
void loadSpellNameCache() const;
|
||||
void preloadDBCCaches() const;
|
||||
void categorizeTrainerSpells();
|
||||
|
||||
|
|
@ -3127,15 +3415,9 @@ private:
|
|||
bool spellBookTabsDirty_ = true;
|
||||
bool skillLineDbcLoaded_ = false;
|
||||
bool skillLineAbilityLoaded_ = false;
|
||||
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;
|
||||
std::vector<uint32_t> playerExploredZones_ =
|
||||
std::vector<uint32_t>(PLAYER_EXPLORED_ZONES_COUNT, 0u);
|
||||
bool hasPlayerExploredZones_ = false;
|
||||
void loadSkillLineDbc();
|
||||
void loadSkillLineAbilityDbc();
|
||||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
void applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields);
|
||||
// Apply packed kill counts from player update fields to a quest entry that has
|
||||
// already had its killObjectives populated from SMSG_QUEST_QUERY_RESPONSE.
|
||||
void applyPackedKillCountsFromFields(QuestLogEntry& quest);
|
||||
|
|
|
|||
73
include/rendering/overlay_system.hpp
Normal file
73
include/rendering/overlay_system.hpp
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#pragma once
|
||||
|
||||
#include <glm/vec3.hpp>
|
||||
#include <glm/vec4.hpp>
|
||||
#include <glm/mat4x4.hpp>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
class VkContext;
|
||||
|
||||
/// Manages selection circle and fullscreen overlay Vulkan pipelines.
|
||||
/// Extracted from Renderer to isolate overlay rendering resources.
|
||||
class OverlaySystem {
|
||||
public:
|
||||
/// Height query callable: returns floor height at (x, y) or (x, y, probeZ).
|
||||
using HeightQuery2D = std::function<std::optional<float>(float x, float y)>;
|
||||
using HeightQuery3D = std::function<std::optional<float>(float x, float y, float probeZ)>;
|
||||
|
||||
explicit OverlaySystem(VkContext* ctx);
|
||||
~OverlaySystem();
|
||||
|
||||
OverlaySystem(const OverlaySystem&) = delete;
|
||||
OverlaySystem& operator=(const OverlaySystem&) = delete;
|
||||
|
||||
// Selection circle
|
||||
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
|
||||
void clearSelectionCircle();
|
||||
void renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection,
|
||||
VkCommandBuffer cmd,
|
||||
HeightQuery2D terrainHeight,
|
||||
HeightQuery3D wmoHeight,
|
||||
HeightQuery3D m2Height);
|
||||
|
||||
// Fullscreen color overlay (underwater tint, etc.)
|
||||
void renderOverlay(const glm::vec4& color, VkCommandBuffer cmd);
|
||||
|
||||
/// Destroy all Vulkan resources (called before VkContext teardown).
|
||||
void cleanup();
|
||||
|
||||
/// Recreate pipelines after swapchain resize / MSAA change.
|
||||
void recreatePipelines();
|
||||
|
||||
private:
|
||||
void initSelectionCircle();
|
||||
void initOverlayPipeline();
|
||||
|
||||
VkContext* vkCtx_ = nullptr;
|
||||
|
||||
// Selection circle resources
|
||||
VkPipeline selCirclePipeline_ = VK_NULL_HANDLE;
|
||||
VkPipelineLayout selCirclePipelineLayout_ = VK_NULL_HANDLE;
|
||||
::VkBuffer selCircleVertBuf_ = VK_NULL_HANDLE;
|
||||
VmaAllocation selCircleVertAlloc_ = VK_NULL_HANDLE;
|
||||
::VkBuffer selCircleIdxBuf_ = VK_NULL_HANDLE;
|
||||
VmaAllocation selCircleIdxAlloc_ = VK_NULL_HANDLE;
|
||||
int selCircleVertCount_ = 0;
|
||||
glm::vec3 selCirclePos_{0.0f};
|
||||
glm::vec3 selCircleColor_{1.0f, 0.0f, 0.0f};
|
||||
float selCircleRadius_ = 1.5f;
|
||||
bool selCircleVisible_ = false;
|
||||
|
||||
// Fullscreen overlay resources
|
||||
VkPipeline overlayPipeline_ = VK_NULL_HANDLE;
|
||||
VkPipelineLayout overlayPipelineLayout_ = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -53,11 +53,11 @@ class AmdFsr3Runtime;
|
|||
class SpellVisualSystem;
|
||||
class PostProcessPipeline;
|
||||
class AnimationController;
|
||||
enum class RangedWeaponType : uint8_t;
|
||||
class LevelUpEffect;
|
||||
class ChargeEffect;
|
||||
class SwimEffects;
|
||||
class RenderGraph;
|
||||
class OverlaySystem;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
|
|
@ -139,7 +139,8 @@ public:
|
|||
WorldMap* getWorldMap() const { return worldMap.get(); }
|
||||
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
|
||||
SkySystem* getSkySystem() const { return skySystem.get(); }
|
||||
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
||||
const std::string& getCurrentZoneName() const;
|
||||
uint32_t getCurrentZoneId() const;
|
||||
bool isPlayerIndoors() const { return playerIndoors_; }
|
||||
VkContext* getVkContext() const { return vkCtx; }
|
||||
VkDescriptorSetLayout getPerFrameSetLayout() const { return perFrameSetLayout; }
|
||||
|
|
@ -152,52 +153,17 @@ public:
|
|||
float getCharacterYaw() const { return characterYaw; }
|
||||
void setCharacterYaw(float yawDeg) { characterYaw = yawDeg; }
|
||||
|
||||
// Emote support — delegates to AnimationController (§4.2)
|
||||
void playEmote(const std::string& emoteName);
|
||||
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)
|
||||
// Delegates to SpellVisualSystem (owned by Renderer)
|
||||
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
||||
bool useImpactKit = false);
|
||||
SpellVisualSystem* getSpellVisualSystem() const { return spellVisualSystem_.get(); }
|
||||
bool isEmoteActive() const;
|
||||
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
|
||||
static uint32_t getEmoteDbcId(const std::string& emoteName);
|
||||
static std::string getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName = nullptr);
|
||||
static uint32_t getEmoteAnimByDbcId(uint32_t dbcId);
|
||||
|
||||
// Targeting support — delegates to AnimationController (§4.2)
|
||||
void setTargetPosition(const glm::vec3* pos);
|
||||
void setInCombat(bool combat);
|
||||
// Combat visual state (compound: resets AnimationController + SpellVisualSystem)
|
||||
void resetCombatVisualState();
|
||||
bool isMoving() const;
|
||||
void triggerMeleeSwing();
|
||||
void setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose = false,
|
||||
bool isFist = false, bool isDagger = false,
|
||||
bool hasOffHand = false, bool hasShield = false);
|
||||
void triggerSpecialAttack(uint32_t spellId);
|
||||
void setEquippedRangedType(RangedWeaponType type);
|
||||
void triggerRangedShot();
|
||||
RangedWeaponType getEquippedRangedType() const;
|
||||
void setCharging(bool charging);
|
||||
bool isCharging() const;
|
||||
void startChargeEffect(const glm::vec3& position, const glm::vec3& direction);
|
||||
void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction);
|
||||
void stopChargeEffect();
|
||||
|
||||
// Mount rendering — delegates to AnimationController (§4.2)
|
||||
void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath = "");
|
||||
void setTaxiFlight(bool onTaxi);
|
||||
void setMountPitchRoll(float pitch, float roll);
|
||||
void clearMount();
|
||||
bool isMounted() const;
|
||||
|
||||
// AnimationController access (§4.2)
|
||||
// Sub-system accessors (§4.2)
|
||||
AnimationController* getAnimationController() const { return animationController_.get(); }
|
||||
LevelUpEffect* getLevelUpEffect() const { return levelUpEffect.get(); }
|
||||
ChargeEffect* getChargeEffect() const { return chargeEffect.get(); }
|
||||
|
|
@ -286,33 +252,8 @@ public:
|
|||
|
||||
// Post-process pipeline API — delegates to PostProcessPipeline (§4.3)
|
||||
PostProcessPipeline* getPostProcessPipeline() const;
|
||||
void setFXAAEnabled(bool enabled);
|
||||
bool isFXAAEnabled() const;
|
||||
void setFSREnabled(bool enabled);
|
||||
bool isFSREnabled() const;
|
||||
void setFSRQuality(float scaleFactor);
|
||||
void setFSRSharpness(float sharpness);
|
||||
float getFSRScaleFactor() const;
|
||||
float getFSRSharpness() const;
|
||||
void setFSR2Enabled(bool enabled);
|
||||
bool isFSR2Enabled() const;
|
||||
void setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY);
|
||||
void setAmdFsr3FramegenEnabled(bool enabled);
|
||||
bool isAmdFsr3FramegenEnabled() const;
|
||||
float getFSR2JitterSign() const;
|
||||
float getFSR2MotionVecScaleX() const;
|
||||
float getFSR2MotionVecScaleY() const;
|
||||
bool isAmdFsr2SdkAvailable() const;
|
||||
bool isAmdFsr3FramegenSdkAvailable() const;
|
||||
bool isAmdFsr3FramegenRuntimeActive() const;
|
||||
bool isAmdFsr3FramegenRuntimeReady() const;
|
||||
const char* getAmdFsr3FramegenRuntimePath() const;
|
||||
const std::string& getAmdFsr3FramegenRuntimeError() const;
|
||||
size_t getAmdFsr3UpscaleDispatchCount() const;
|
||||
size_t getAmdFsr3FramegenDispatchCount() const;
|
||||
size_t getAmdFsr3FallbackCount() const;
|
||||
void setBrightness(float b);
|
||||
float getBrightness() const;
|
||||
|
||||
void setWaterRefractionEnabled(bool enabled);
|
||||
bool isWaterRefractionEnabled() const;
|
||||
|
|
@ -332,12 +273,7 @@ private:
|
|||
// Post-process pipeline — owns all FSR/FXAA/FSR2 state (extracted §4.3)
|
||||
std::unique_ptr<PostProcessPipeline> postProcessPipeline_;
|
||||
|
||||
uint32_t currentZoneId = 0;
|
||||
std::string currentZoneName;
|
||||
bool inTavern_ = false;
|
||||
bool inBlacksmith_ = false;
|
||||
bool playerIndoors_ = false; // Cached WMO inside state for macro conditionals
|
||||
float musicSwitchCooldown_ = 0.0f;
|
||||
bool deferredWorldInitEnabled_ = true;
|
||||
bool deferredWorldInitPending_ = false;
|
||||
uint8_t deferredWorldInitStage_ = 0;
|
||||
|
|
@ -350,26 +286,8 @@ private:
|
|||
|
||||
|
||||
|
||||
// Selection circle rendering (Vulkan)
|
||||
VkPipeline selCirclePipeline = VK_NULL_HANDLE;
|
||||
VkPipelineLayout selCirclePipelineLayout = VK_NULL_HANDLE;
|
||||
::VkBuffer selCircleVertBuf = VK_NULL_HANDLE;
|
||||
VmaAllocation selCircleVertAlloc = VK_NULL_HANDLE;
|
||||
::VkBuffer selCircleIdxBuf = VK_NULL_HANDLE;
|
||||
VmaAllocation selCircleIdxAlloc = VK_NULL_HANDLE;
|
||||
int selCircleVertCount = 0;
|
||||
void initSelectionCircle();
|
||||
void renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection, VkCommandBuffer overrideCmd = VK_NULL_HANDLE);
|
||||
glm::vec3 selCirclePos{0.0f};
|
||||
glm::vec3 selCircleColor{1.0f, 0.0f, 0.0f};
|
||||
float selCircleRadius = 1.5f;
|
||||
bool selCircleVisible = false;
|
||||
|
||||
// Fullscreen color overlay (underwater tint)
|
||||
VkPipeline overlayPipeline = VK_NULL_HANDLE;
|
||||
VkPipelineLayout overlayPipelineLayout = VK_NULL_HANDLE;
|
||||
void initOverlayPipeline();
|
||||
void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE);
|
||||
// Selection circle + overlay rendering (owned by OverlaySystem)
|
||||
std::unique_ptr<OverlaySystem> overlaySystem_;
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "audio/spell_sound_manager.hpp"
|
||||
#include "audio/movement_sound_manager.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "game/zone_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -86,5 +87,162 @@ void AudioCoordinator::shutdown() {
|
|||
LOG_INFO("AudioCoordinator shutdown complete");
|
||||
}
|
||||
|
||||
void AudioCoordinator::playZoneMusic(const std::string& music) {
|
||||
if (music.empty() || !musicManager_) return;
|
||||
if (music.rfind("file:", 0) == 0) {
|
||||
musicManager_->crossfadeToFile(music.substr(5));
|
||||
} else {
|
||||
musicManager_->crossfadeTo(music);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioCoordinator::updateZoneAudio(const ZoneAudioContext& ctx) {
|
||||
float deltaTime = ctx.deltaTime;
|
||||
if (musicSwitchCooldown_ > 0.0f) {
|
||||
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime);
|
||||
}
|
||||
|
||||
// ── Ambient weather audio sync ──
|
||||
if (ambientSoundManager_) {
|
||||
bool isBlacksmith = (ctx.insideWmoId == 96048);
|
||||
|
||||
// Map visual weather type to ambient sound weather type
|
||||
AmbientSoundManager::WeatherType audioWeatherType = AmbientSoundManager::WeatherType::NONE;
|
||||
if (ctx.weatherType == 1) { // RAIN
|
||||
if (ctx.weatherIntensity < 0.33f) audioWeatherType = AmbientSoundManager::WeatherType::RAIN_LIGHT;
|
||||
else if (ctx.weatherIntensity < 0.66f) audioWeatherType = AmbientSoundManager::WeatherType::RAIN_MEDIUM;
|
||||
else audioWeatherType = AmbientSoundManager::WeatherType::RAIN_HEAVY;
|
||||
} else if (ctx.weatherType == 2) { // SNOW
|
||||
if (ctx.weatherIntensity < 0.33f) audioWeatherType = AmbientSoundManager::WeatherType::SNOW_LIGHT;
|
||||
else if (ctx.weatherIntensity < 0.66f) audioWeatherType = AmbientSoundManager::WeatherType::SNOW_MEDIUM;
|
||||
else audioWeatherType = AmbientSoundManager::WeatherType::SNOW_HEAVY;
|
||||
}
|
||||
ambientSoundManager_->setWeather(audioWeatherType);
|
||||
ambientSoundManager_->update(deltaTime, ctx.cameraPosition, ctx.insideWmo, ctx.isSwimming, isBlacksmith);
|
||||
}
|
||||
|
||||
// ── Zone detection and music transitions ──
|
||||
auto* zm = ctx.zoneManager;
|
||||
if (!zm || !musicManager_ || !ctx.hasTile) return;
|
||||
|
||||
uint32_t zoneId = (ctx.serverZoneId != 0)
|
||||
? ctx.serverZoneId
|
||||
: zm->getZoneId(ctx.tileX, ctx.tileY);
|
||||
|
||||
bool insideTavern = false;
|
||||
bool insideBlacksmith = false;
|
||||
std::string tavernMusic;
|
||||
|
||||
// WMO-based location overrides (taverns, blacksmiths, city zones)
|
||||
if (ctx.insideWmo) {
|
||||
uint32_t wmoModelId = ctx.insideWmoId;
|
||||
|
||||
// Stormwind WMO → force Stormwind City zone
|
||||
if (wmoModelId == 10047) zoneId = 1519;
|
||||
|
||||
// Log WMO transitions
|
||||
static uint32_t lastLoggedWmoId = 0;
|
||||
if (wmoModelId != lastLoggedWmoId) {
|
||||
LOG_INFO("Inside WMO model ID: ", wmoModelId);
|
||||
lastLoggedWmoId = wmoModelId;
|
||||
}
|
||||
|
||||
// Blacksmith detection (ambient forge sounds)
|
||||
if (wmoModelId == 96048) {
|
||||
insideBlacksmith = true;
|
||||
LOG_INFO("Detected blacksmith WMO ", wmoModelId);
|
||||
}
|
||||
|
||||
// Tavern / inn detection
|
||||
if (wmoModelId == 191 || wmoModelId == 71414 || wmoModelId == 190 ||
|
||||
wmoModelId == 220 || wmoModelId == 221 ||
|
||||
wmoModelId == 5392 || wmoModelId == 5393) {
|
||||
insideTavern = true;
|
||||
static const std::vector<std::string> tavernTracks = {
|
||||
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
|
||||
};
|
||||
static int tavernTrackIndex = 0;
|
||||
tavernMusic = tavernTracks[tavernTrackIndex++ % tavernTracks.size()];
|
||||
LOG_INFO("Detected tavern WMO ", wmoModelId, ", playing: ", tavernMusic);
|
||||
}
|
||||
}
|
||||
|
||||
// Tavern music transitions
|
||||
if (insideTavern) {
|
||||
if (!inTavern_ && !tavernMusic.empty()) {
|
||||
inTavern_ = true;
|
||||
LOG_INFO("Entered tavern");
|
||||
musicManager_->playMusic(tavernMusic, true);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
} else if (inTavern_) {
|
||||
inTavern_ = false;
|
||||
LOG_INFO("Exited tavern");
|
||||
auto* info = zm->getZoneInfo(currentZoneId_);
|
||||
if (info) {
|
||||
std::string music = zm->getRandomMusic(currentZoneId_);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blacksmith transitions (stop music, let ambience play)
|
||||
if (insideBlacksmith) {
|
||||
if (!inBlacksmith_) {
|
||||
inBlacksmith_ = true;
|
||||
LOG_INFO("Entered blacksmith - stopping music");
|
||||
musicManager_->stopMusic();
|
||||
}
|
||||
} else if (inBlacksmith_) {
|
||||
inBlacksmith_ = false;
|
||||
LOG_INFO("Exited blacksmith - restoring music");
|
||||
auto* info = zm->getZoneInfo(currentZoneId_);
|
||||
if (info) {
|
||||
std::string music = zm->getRandomMusic(currentZoneId_);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normal zone transitions
|
||||
if (!insideTavern && !insideBlacksmith && zoneId != currentZoneId_ && zoneId != 0) {
|
||||
currentZoneId_ = zoneId;
|
||||
auto* info = zm->getZoneInfo(zoneId);
|
||||
if (info) {
|
||||
currentZoneName_ = info->name;
|
||||
LOG_INFO("Entered zone: ", info->name);
|
||||
if (musicSwitchCooldown_ <= 0.0f) {
|
||||
std::string music = zm->getRandomMusic(zoneId);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ambientSoundManager_) {
|
||||
ambientSoundManager_->setZoneId(zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
musicManager_->update(deltaTime);
|
||||
|
||||
// When a track finishes, pick a new random track from the current zone
|
||||
if (!musicManager_->isPlaying() && !inTavern_ && !inBlacksmith_ &&
|
||||
currentZoneId_ != 0 && musicSwitchCooldown_ <= 0.0f) {
|
||||
std::string music = zm->getRandomMusic(currentZoneId_);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ bool AnimationCallbackHandler::updateCharge(float deltaTime) {
|
|||
dir *= glm::inversesqrt(dirLenSq);
|
||||
float yawDeg = glm::degrees(std::atan2(dir.x, dir.y));
|
||||
renderer_.setCharacterYaw(yawDeg);
|
||||
renderer_.emitChargeEffect(renderPos, dir);
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->emitChargeEffect(renderPos, dir);
|
||||
}
|
||||
|
||||
// Sync to game handler
|
||||
|
|
@ -69,8 +69,8 @@ bool AnimationCallbackHandler::updateCharge(float deltaTime) {
|
|||
// Charge complete
|
||||
if (t >= 1.0f) {
|
||||
chargeActive_ = false;
|
||||
renderer_.setCharging(false);
|
||||
renderer_.stopChargeEffect();
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->setCharging(false);
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->stopChargeEffect();
|
||||
renderer_.getCameraController()->setExternalFollow(false);
|
||||
renderer_.getCameraController()->setExternalMoving(false);
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ bool AnimationCallbackHandler::updateCharge(float deltaTime) {
|
|||
}
|
||||
}
|
||||
gameHandler_.startAutoAttack(chargeTargetGuid_);
|
||||
renderer_.triggerMeleeSwing();
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->triggerMeleeSwing();
|
||||
}
|
||||
|
||||
// Send movement heartbeat so server knows our new position
|
||||
|
|
@ -157,11 +157,11 @@ void AnimationCallbackHandler::setupCallbacks() {
|
|||
// Disable player input, play charge animation
|
||||
renderer_.getCameraController()->setExternalFollow(true);
|
||||
renderer_.getCameraController()->clearMovementInputs();
|
||||
renderer_.setCharging(true);
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->setCharging(true);
|
||||
|
||||
// Start charge visual effect (red haze + dust)
|
||||
glm::vec3 chargeDir = glm::normalize(endRender - startRender);
|
||||
renderer_.startChargeEffect(startRender, chargeDir);
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->startChargeEffect(startRender, chargeDir);
|
||||
|
||||
// Play charge whoosh sound (try multiple paths)
|
||||
auto& audio = audio::AudioEngine::instance();
|
||||
|
|
|
|||
|
|
@ -934,7 +934,7 @@ void Application::setState(AppState newState) {
|
|||
uint32_t oldInst = renderer->getCharacterInstanceId();
|
||||
if (oldInst > 0) {
|
||||
renderer->setCharacterFollow(0);
|
||||
renderer->clearMount();
|
||||
if (auto* ac = renderer->getAnimationController()) ac->clearMount();
|
||||
renderer->getCharacterRenderer()->removeInstance(oldInst);
|
||||
}
|
||||
}
|
||||
|
|
@ -975,11 +975,11 @@ void Application::setState(AppState newState) {
|
|||
if (renderer) {
|
||||
// Ranged auto-attack spells: Auto Shot (75), Shoot (5019), Throw (2764)
|
||||
if (spellId == 75 || spellId == 5019 || spellId == 2764) {
|
||||
renderer->triggerRangedShot();
|
||||
if (auto* ac = renderer->getAnimationController()) ac->triggerRangedShot();
|
||||
} else if (spellId != 0) {
|
||||
renderer->triggerSpecialAttack(spellId);
|
||||
if (auto* ac = renderer->getAnimationController()) ac->triggerSpecialAttack(spellId);
|
||||
} else {
|
||||
renderer->triggerMeleeSwing();
|
||||
if (auto* ac = renderer->getAnimationController()) ac->triggerMeleeSwing();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1118,7 +1118,7 @@ void Application::logoutToLogin() {
|
|||
if (auto* questMarkers = renderer->getQuestMarkerRenderer()) {
|
||||
questMarkers->clear();
|
||||
}
|
||||
renderer->clearMount();
|
||||
if (auto* ac = renderer->getAnimationController()) ac->clearMount();
|
||||
renderer->setCharacterFollow(0);
|
||||
if (auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr) {
|
||||
music->stopMusic(0.0f);
|
||||
|
|
@ -1346,13 +1346,13 @@ void Application::update(float deltaTime) {
|
|||
// Tilt the mount/character model to match flight direction
|
||||
// (taxi flight uses setTaxiOrientationCallback for this instead)
|
||||
if (gameHandler->isPlayerFlying() && gameHandler->isMounted()) {
|
||||
renderer->setMountPitchRoll(pitchRad, 0.0f);
|
||||
if (auto* ac = renderer->getAnimationController()) ac->setMountPitchRoll(pitchRad, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (gameHandler->isMounted()) {
|
||||
// Reset mount pitch when not flying
|
||||
renderer->setMountPitchRoll(0.0f, 0.0f);
|
||||
if (auto* ac = renderer->getAnimationController()) ac->setMountPitchRoll(0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1454,14 +1454,14 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
bool idleOrbit = renderer->getCameraController()->isIdleOrbit();
|
||||
if (idleOrbit && !idleYawned_ && renderer) {
|
||||
renderer->playEmote("yawn");
|
||||
if (auto* ac = renderer->getAnimationController()) ac->playEmote("yawn");
|
||||
idleYawned_ = true;
|
||||
} else if (!idleOrbit) {
|
||||
idleYawned_ = false;
|
||||
}
|
||||
}
|
||||
if (renderer) {
|
||||
renderer->setTaxiFlight(onTaxi);
|
||||
if (auto* ac = renderer->getAnimationController()) ac->setTaxiFlight(onTaxi);
|
||||
}
|
||||
if (renderer && renderer->getTerrainManager()) {
|
||||
renderer->getTerrainManager()->setStreamingEnabled(true);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
|
|
@ -50,7 +52,7 @@ void AudioCallbackHandler::setupCallbacks() {
|
|||
uiManager_->getGameScreen().toastManager().triggerDing(newLevel);
|
||||
}
|
||||
if (renderer_) {
|
||||
renderer_->triggerLevelUpEffect(renderer_->getCharacterPosition());
|
||||
if (auto* ac = renderer_->getAnimationController()) ac->triggerLevelUpEffect(renderer_->getCharacterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -108,7 +110,7 @@ void AudioCallbackHandler::setupCallbacks() {
|
|||
if (entity) {
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer_->triggerLevelUpEffect(renderPos);
|
||||
if (auto* ac = renderer_->getAnimationController()) ac->triggerLevelUpEffect(renderPos);
|
||||
}
|
||||
|
||||
// Show chat message if in group
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1230
src/core/entity_spawner_player.cpp
Normal file
1230
src/core/entity_spawner_player.cpp
Normal file
File diff suppressed because it is too large
Load diff
1597
src/core/entity_spawner_processing.cpp
Normal file
1597
src/core/entity_spawner_processing.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@
|
|||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
|
|
@ -37,7 +38,7 @@ void TransportCallbackHandler::setupCallbacks() {
|
|||
entitySpawner_.clearMountState();
|
||||
}
|
||||
entitySpawner_.setMountDisplayId(0);
|
||||
renderer_.clearMount();
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->clearMount();
|
||||
LOG_INFO("Dismounted");
|
||||
return;
|
||||
}
|
||||
|
|
@ -106,7 +107,7 @@ void TransportCallbackHandler::setupCallbacks() {
|
|||
renderer_.getCameraController()->setFacingYaw(yawDegrees);
|
||||
renderer_.setCharacterYaw(yawDegrees);
|
||||
// Set mount pitch and roll for realistic flight animation
|
||||
renderer_.setMountPitchRoll(pitch, roll);
|
||||
if (auto* ac = renderer_.getAnimationController()) ac->setMountPitchRoll(pitch, roll);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "core/world_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
|
|
@ -290,7 +291,7 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
if (auto* questMarkers = renderer_->getQuestMarkerRenderer()) {
|
||||
questMarkers->clear();
|
||||
}
|
||||
renderer_->clearMount();
|
||||
if (auto* ac = renderer_->getAnimationController()) ac->clearMount();
|
||||
}
|
||||
|
||||
// Clear application-level instance tracking (after renderer cleanup)
|
||||
|
|
@ -416,7 +417,7 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
uint32_t oldInst = renderer_->getCharacterInstanceId();
|
||||
if (oldInst > 0) {
|
||||
renderer_->setCharacterFollow(0);
|
||||
renderer_->clearMount();
|
||||
if (auto* ac = renderer_->getAnimationController()) ac->clearMount();
|
||||
renderer_->getCharacterRenderer()->removeInstance(oldInst);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "game/opcode_table.hpp"
|
||||
#include "network/world_socket.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -54,8 +55,8 @@ void ChatHandler::registerOpcodes(DispatchTable& table) {
|
|||
if (!packet.hasRemaining(12)) return;
|
||||
uint32_t emoteAnim = packet.readUInt32();
|
||||
uint64_t sourceGuid = packet.readUInt64();
|
||||
if (owner_.emoteAnimCallback_ && sourceGuid != 0)
|
||||
owner_.emoteAnimCallback_(sourceGuid, emoteAnim);
|
||||
if (owner_.emoteAnimCallbackRef() && sourceGuid != 0)
|
||||
owner_.emoteAnimCallbackRef()(sourceGuid, emoteAnim);
|
||||
};
|
||||
table[Opcode::SMSG_CHANNEL_NOTIFY] = [this](network::Packet& packet) {
|
||||
if (owner_.getState() == WorldState::IN_WORLD ||
|
||||
|
|
@ -125,7 +126,7 @@ void ChatHandler::registerOpcodes(DispatchTable& table) {
|
|||
if (!msg.empty()) {
|
||||
owner_.addUIError(msg);
|
||||
addSystemChatMessage(msg);
|
||||
owner_.areaTriggerMsgs_.push_back(msg);
|
||||
owner_.areaTriggerMsgsRef().push_back(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -155,15 +156,15 @@ void ChatHandler::sendChatMessage(ChatType type, const std::string& message, con
|
|||
ChatLanguage language = isHorde ? ChatLanguage::ORCISH : ChatLanguage::COMMON;
|
||||
|
||||
auto packet = MessageChatPacket::build(type, language, message, target);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
|
||||
// Add local echo so the player sees their own message immediately
|
||||
MessageChatData echo;
|
||||
echo.senderGuid = owner_.playerGuid;
|
||||
echo.senderGuid = owner_.getPlayerGuid();
|
||||
echo.language = language;
|
||||
echo.message = message;
|
||||
|
||||
auto nameIt = owner_.getPlayerNameCache().find(owner_.playerGuid);
|
||||
auto nameIt = owner_.getPlayerNameCache().find(owner_.getPlayerGuid());
|
||||
if (nameIt != owner_.getPlayerNameCache().end()) {
|
||||
echo.senderName = nameIt->second;
|
||||
}
|
||||
|
|
@ -186,7 +187,7 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
|||
LOG_DEBUG("Handling SMSG_MESSAGECHAT");
|
||||
|
||||
MessageChatData data;
|
||||
if (!owner_.packetParsers_->parseMessageChat(packet, data)) {
|
||||
if (!owner_.getPacketParsers()->parseMessageChat(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT, size=", packet.getSize());
|
||||
return;
|
||||
}
|
||||
|
|
@ -195,9 +196,9 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
|||
" '", data.senderName, "' msg='", data.message.substr(0, 60), "'");
|
||||
|
||||
// Skip server echo of our own messages (we already added a local echo)
|
||||
if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) {
|
||||
if (data.senderGuid == owner_.getPlayerGuid() && data.senderGuid != 0) {
|
||||
if (data.type == ChatType::WHISPER && !data.senderName.empty()) {
|
||||
owner_.lastWhisperSender_ = data.senderName;
|
||||
owner_.lastWhisperSenderRef() = data.senderName;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -284,29 +285,29 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
|||
// Always store GUID so getLastWhisperSender() can resolve the name
|
||||
// from the player name cache even if name wasn't available yet
|
||||
if (data.senderGuid != 0)
|
||||
owner_.lastWhisperSenderGuid_ = data.senderGuid;
|
||||
owner_.lastWhisperSenderGuidRef() = data.senderGuid;
|
||||
if (!data.senderName.empty())
|
||||
owner_.lastWhisperSender_ = data.senderName;
|
||||
owner_.lastWhisperSenderRef() = data.senderName;
|
||||
|
||||
if (!data.senderName.empty()) {
|
||||
// Only auto-reply once per sender per AFK/DND session to prevent loops
|
||||
if (owner_.afkStatus_ && afkAutoRepliedSenders_.insert(data.senderName).second) {
|
||||
std::string reply = owner_.afkMessage_.empty() ? "Away from Keyboard" : owner_.afkMessage_;
|
||||
if (owner_.afkStatusRef() && afkAutoRepliedSenders_.insert(data.senderName).second) {
|
||||
std::string reply = owner_.afkMessageRef().empty() ? "Away from Keyboard" : owner_.afkMessageRef();
|
||||
sendChatMessage(ChatType::WHISPER, "<AFK> " + reply, data.senderName);
|
||||
} else if (owner_.dndStatus_ && afkAutoRepliedSenders_.insert(data.senderName).second) {
|
||||
std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_;
|
||||
} else if (owner_.dndStatusRef() && afkAutoRepliedSenders_.insert(data.senderName).second) {
|
||||
std::string reply = owner_.dndMessageRef().empty() ? "Do Not Disturb" : owner_.dndMessageRef();
|
||||
sendChatMessage(ChatType::WHISPER, "<DND> " + reply, data.senderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger chat bubble for SAY/YELL messages from others
|
||||
if (owner_.chatBubbleCallback_ && data.senderGuid != 0) {
|
||||
if (owner_.chatBubbleCallbackRef() && data.senderGuid != 0) {
|
||||
if (data.type == ChatType::SAY || data.type == ChatType::YELL ||
|
||||
data.type == ChatType::MONSTER_SAY || data.type == ChatType::MONSTER_YELL ||
|
||||
data.type == ChatType::MONSTER_PARTY) {
|
||||
bool isYell = (data.type == ChatType::YELL || data.type == ChatType::MONSTER_YELL);
|
||||
owner_.chatBubbleCallback_(data.senderGuid, data.message, isYell);
|
||||
owner_.chatBubbleCallbackRef()(data.senderGuid, data.message, isYell);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,7 +329,7 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
|||
LOG_DEBUG("[", getChatTypeString(data.type), "] ", channelInfo, senderInfo, ": ", data.message);
|
||||
|
||||
// Detect addon messages
|
||||
if (owner_.addonEventCallback_ &&
|
||||
if (owner_.addonEventCallbackRef() &&
|
||||
data.type != ChatType::SAY && data.type != ChatType::YELL &&
|
||||
data.type != ChatType::EMOTE && data.type != ChatType::TEXT_EMOTE &&
|
||||
data.type != ChatType::MONSTER_SAY && data.type != ChatType::MONSTER_YELL) {
|
||||
|
|
@ -339,21 +340,21 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
|||
if (prefix.find(' ') == std::string::npos) {
|
||||
std::string body = data.message.substr(tabPos + 1);
|
||||
std::string channel = getChatTypeString(data.type);
|
||||
owner_.addonEventCallback_("CHAT_MSG_ADDON", {prefix, body, channel, data.senderName});
|
||||
owner_.addonEventCallbackRef()("CHAT_MSG_ADDON", {prefix, body, channel, data.senderName});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fire CHAT_MSG_* addon events
|
||||
if (owner_.addonChatCallback_) owner_.addonChatCallback_(data);
|
||||
if (owner_.addonEventCallback_) {
|
||||
if (owner_.addonChatCallbackRef()) owner_.addonChatCallbackRef()(data);
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
std::string eventName = "CHAT_MSG_";
|
||||
eventName += getChatTypeString(data.type);
|
||||
std::string lang = std::to_string(static_cast<int>(data.language));
|
||||
char guidBuf[32];
|
||||
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)data.senderGuid);
|
||||
owner_.addonEventCallback_(eventName, {
|
||||
owner_.addonEventCallbackRef()(eventName, {
|
||||
data.message,
|
||||
data.senderName,
|
||||
lang,
|
||||
|
|
@ -371,9 +372,9 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
|||
}
|
||||
|
||||
void ChatHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) {
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
auto packet = TextEmotePacket::build(textEmoteId, targetGuid);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
}
|
||||
|
||||
void ChatHandler::handleTextEmote(network::Packet& packet) {
|
||||
|
|
@ -384,7 +385,7 @@ void ChatHandler::handleTextEmote(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) {
|
||||
if (data.senderGuid == owner_.getPlayerGuid() && data.senderGuid != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +406,7 @@ void ChatHandler::handleTextEmote(network::Packet& packet) {
|
|||
}
|
||||
|
||||
const std::string* targetPtr = data.targetName.empty() ? nullptr : &data.targetName;
|
||||
std::string emoteText = rendering::Renderer::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr);
|
||||
std::string emoteText = rendering::AnimationController::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr);
|
||||
if (emoteText.empty()) {
|
||||
emoteText = data.targetName.empty()
|
||||
? senderName + " performs an emote."
|
||||
|
|
@ -421,29 +422,29 @@ void ChatHandler::handleTextEmote(network::Packet& packet) {
|
|||
|
||||
addLocalChatMessage(chatMsg);
|
||||
|
||||
uint32_t animId = rendering::Renderer::getEmoteAnimByDbcId(data.textEmoteId);
|
||||
if (animId != 0 && owner_.emoteAnimCallback_) {
|
||||
owner_.emoteAnimCallback_(data.senderGuid, animId);
|
||||
uint32_t animId = rendering::AnimationController::getEmoteAnimByDbcId(data.textEmoteId);
|
||||
if (animId != 0 && owner_.emoteAnimCallbackRef()) {
|
||||
owner_.emoteAnimCallbackRef()(data.senderGuid, animId);
|
||||
}
|
||||
|
||||
LOG_INFO("TEXT_EMOTE from ", senderName, " (emoteId=", data.textEmoteId, ", anim=", animId, ")");
|
||||
}
|
||||
|
||||
void ChatHandler::joinChannel(const std::string& channelName, const std::string& password) {
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
auto packet = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildJoinChannel(channelName, password)
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
auto packet = owner_.getPacketParsers()
|
||||
? owner_.getPacketParsers()->buildJoinChannel(channelName, password)
|
||||
: JoinChannelPacket::build(channelName, password);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
LOG_INFO("Requesting to join channel: ", channelName);
|
||||
}
|
||||
|
||||
void ChatHandler::leaveChannel(const std::string& channelName) {
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
auto packet = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildLeaveChannel(channelName)
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
auto packet = owner_.getPacketParsers()
|
||||
? owner_.getPacketParsers()->buildLeaveChannel(channelName)
|
||||
: LeaveChannelPacket::build(channelName);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
LOG_INFO("Requesting to leave channel: ", channelName);
|
||||
}
|
||||
|
||||
|
|
@ -601,9 +602,9 @@ void ChatHandler::addLocalChatMessage(const MessageChatData& msg) {
|
|||
if (chatHistory_.size() > maxChatHistory_) {
|
||||
chatHistory_.pop_front();
|
||||
}
|
||||
if (owner_.addonChatCallback_) owner_.addonChatCallback_(msg);
|
||||
if (owner_.addonChatCallbackRef()) owner_.addonChatCallbackRef()(msg);
|
||||
|
||||
if (owner_.addonEventCallback_) {
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
std::string eventName = "CHAT_MSG_";
|
||||
eventName += getChatTypeString(msg.type);
|
||||
const Character* ac = owner_.getActiveCharacter();
|
||||
|
|
@ -611,8 +612,8 @@ void ChatHandler::addLocalChatMessage(const MessageChatData& msg) {
|
|||
? (ac ? ac->name : std::string{}) : msg.senderName;
|
||||
char guidBuf[32];
|
||||
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX",
|
||||
(unsigned long long)(msg.senderGuid != 0 ? msg.senderGuid : owner_.playerGuid));
|
||||
owner_.addonEventCallback_(eventName, {
|
||||
(unsigned long long)(msg.senderGuid != 0 ? msg.senderGuid : owner_.getPlayerGuid()));
|
||||
owner_.addonEventCallbackRef()(eventName, {
|
||||
msg.message, senderName,
|
||||
std::to_string(static_cast<int>(msg.language)),
|
||||
msg.channelName, senderName, "", "0", "0", "", "0", "0", guidBuf
|
||||
|
|
@ -630,51 +631,51 @@ void ChatHandler::addSystemChatMessage(const std::string& message) {
|
|||
}
|
||||
|
||||
void ChatHandler::toggleAfk(const std::string& message) {
|
||||
owner_.afkStatus_ = !owner_.afkStatus_;
|
||||
owner_.afkMessage_ = message;
|
||||
owner_.afkStatusRef() = !owner_.afkStatusRef();
|
||||
owner_.afkMessageRef() = message;
|
||||
|
||||
if (owner_.afkStatus_) {
|
||||
if (owner_.afkStatusRef()) {
|
||||
if (message.empty()) {
|
||||
addSystemChatMessage("You are now AFK.");
|
||||
} else {
|
||||
addSystemChatMessage("You are now AFK: " + message);
|
||||
}
|
||||
// If DND was active, turn it off
|
||||
if (owner_.dndStatus_) {
|
||||
owner_.dndStatus_ = false;
|
||||
owner_.dndMessage_.clear();
|
||||
if (owner_.dndStatusRef()) {
|
||||
owner_.dndStatusRef() = false;
|
||||
owner_.dndMessageRef().clear();
|
||||
}
|
||||
} else {
|
||||
addSystemChatMessage("You are no longer AFK.");
|
||||
owner_.afkMessage_.clear();
|
||||
owner_.afkMessageRef().clear();
|
||||
afkAutoRepliedSenders_.clear();
|
||||
}
|
||||
|
||||
LOG_INFO("AFK status: ", owner_.afkStatus_, ", message: ", message);
|
||||
LOG_INFO("AFK status: ", owner_.afkStatusRef(), ", message: ", message);
|
||||
}
|
||||
|
||||
void ChatHandler::toggleDnd(const std::string& message) {
|
||||
owner_.dndStatus_ = !owner_.dndStatus_;
|
||||
owner_.dndMessage_ = message;
|
||||
owner_.dndStatusRef() = !owner_.dndStatusRef();
|
||||
owner_.dndMessageRef() = message;
|
||||
|
||||
if (owner_.dndStatus_) {
|
||||
if (owner_.dndStatusRef()) {
|
||||
if (message.empty()) {
|
||||
addSystemChatMessage("You are now DND (Do Not Disturb).");
|
||||
} else {
|
||||
addSystemChatMessage("You are now DND: " + message);
|
||||
}
|
||||
// If AFK was active, turn it off
|
||||
if (owner_.afkStatus_) {
|
||||
owner_.afkStatus_ = false;
|
||||
owner_.afkMessage_.clear();
|
||||
if (owner_.afkStatusRef()) {
|
||||
owner_.afkStatusRef() = false;
|
||||
owner_.afkMessageRef().clear();
|
||||
}
|
||||
} else {
|
||||
addSystemChatMessage("You are no longer DND.");
|
||||
owner_.dndMessage_.clear();
|
||||
owner_.dndMessageRef().clear();
|
||||
afkAutoRepliedSenders_.clear();
|
||||
}
|
||||
|
||||
LOG_INFO("DND status: ", owner_.dndStatus_, ", message: ", message);
|
||||
LOG_INFO("DND status: ", owner_.dndStatusRef(), ", message: ", message);
|
||||
}
|
||||
|
||||
void ChatHandler::replyToLastWhisper(const std::string& message) {
|
||||
|
|
@ -683,7 +684,7 @@ void ChatHandler::replyToLastWhisper(const std::string& message) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (owner_.lastWhisperSender_.empty()) {
|
||||
if (owner_.lastWhisperSenderRef().empty()) {
|
||||
addSystemChatMessage("No one has whispered you yet.");
|
||||
return;
|
||||
}
|
||||
|
|
@ -694,8 +695,8 @@ void ChatHandler::replyToLastWhisper(const std::string& message) {
|
|||
}
|
||||
|
||||
// Send whisper using the standard message chat function
|
||||
sendChatMessage(ChatType::WHISPER, message, owner_.lastWhisperSender_);
|
||||
LOG_INFO("Replied to ", owner_.lastWhisperSender_, ": ", message);
|
||||
sendChatMessage(ChatType::WHISPER, message, owner_.lastWhisperSenderRef());
|
||||
LOG_INFO("Replied to ", owner_.lastWhisperSenderRef(), ": ", message);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -743,13 +744,13 @@ void ChatHandler::submitGmTicket(const std::string& text) {
|
|||
// uint8 need_response (1 = yes)
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_CREATE));
|
||||
pkt.writeString(text);
|
||||
pkt.writeFloat(owner_.movementInfo.x);
|
||||
pkt.writeFloat(owner_.movementInfo.y);
|
||||
pkt.writeFloat(owner_.movementInfo.z);
|
||||
pkt.writeFloat(owner_.movementInfo.orientation);
|
||||
pkt.writeUInt32(owner_.currentMapId_);
|
||||
pkt.writeFloat(owner_.movementInfoRef().x);
|
||||
pkt.writeFloat(owner_.movementInfoRef().y);
|
||||
pkt.writeFloat(owner_.movementInfoRef().z);
|
||||
pkt.writeFloat(owner_.movementInfoRef().orientation);
|
||||
pkt.writeUInt32(owner_.currentMapIdRef());
|
||||
pkt.writeUInt8(1); // need_response = yes
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
LOG_INFO("Submitted GM ticket: '", text, "'");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
};
|
||||
table[Opcode::SMSG_THREAT_CLEAR] = [this](network::Packet& /*packet*/) {
|
||||
threatLists_.clear();
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("UNIT_THREAT_LIST_UPDATE", {});
|
||||
};
|
||||
table[Opcode::SMSG_THREAT_REMOVE] = [this](network::Packet& packet) {
|
||||
if (!packet.hasRemaining(1)) return;
|
||||
|
|
@ -67,10 +67,10 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
if (autoAttackRequested_ && autoAttackTarget_ != 0) {
|
||||
auto targetEntity = owner_.getEntityManager().getEntity(autoAttackTarget_);
|
||||
if (targetEntity) {
|
||||
float toTargetX = targetEntity->getX() - owner_.movementInfo.x;
|
||||
float toTargetY = targetEntity->getY() - owner_.movementInfo.y;
|
||||
float toTargetX = targetEntity->getX() - owner_.movementInfoRef().x;
|
||||
float toTargetY = targetEntity->getY() - owner_.movementInfoRef().y;
|
||||
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
|
||||
owner_.movementInfo.orientation = std::atan2(-toTargetY, toTargetX);
|
||||
owner_.movementInfoRef().orientation = std::atan2(-toTargetY, toTargetX);
|
||||
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
|
||||
}
|
||||
}
|
||||
|
|
@ -96,10 +96,10 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
if (!packet.hasRemaining(12)) return;
|
||||
uint64_t guid = packet.readUInt64();
|
||||
uint32_t reaction = packet.readUInt32();
|
||||
if (reaction == 2 && owner_.npcAggroCallback_) {
|
||||
if (reaction == 2 && owner_.npcAggroCallbackRef()) {
|
||||
auto entity = owner_.getEntityManager().getEntity(guid);
|
||||
if (entity)
|
||||
owner_.npcAggroCallback_(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
owner_.npcAggroCallbackRef()(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
}
|
||||
};
|
||||
table[Opcode::SMSG_SPELLNONMELEEDAMAGELOG] = [this](network::Packet& packet) { handleSpellDamageLog(packet); };
|
||||
|
|
@ -115,7 +115,7 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
uint32_t dmg = packet.readUInt32();
|
||||
uint32_t envAbs = packet.readUInt32();
|
||||
uint32_t envRes = packet.readUInt32();
|
||||
if (victimGuid == owner_.playerGuid) {
|
||||
if (victimGuid == owner_.getPlayerGuid()) {
|
||||
// Environmental damage: pass envType via powerType field for display differentiation
|
||||
if (dmg > 0)
|
||||
addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast<int32_t>(dmg), 0, false, envType, 0, victimGuid);
|
||||
|
|
@ -124,8 +124,8 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
if (envRes > 0)
|
||||
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(envRes), 0, false, 0, 0, victimGuid);
|
||||
// Drowning damage → play DROWN one-shot on player
|
||||
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallback_)
|
||||
owner_.emoteAnimCallback_(victimGuid, 131); // anim::DROWN
|
||||
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallbackRef())
|
||||
owner_.emoteAnimCallbackRef()(victimGuid, 131); // anim::DROWN
|
||||
}
|
||||
packet.skipAll();
|
||||
};
|
||||
|
|
@ -158,8 +158,8 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
std::sort(list.begin(), list.end(),
|
||||
[](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; });
|
||||
threatLists_[unitGuid] = std::move(list);
|
||||
if (owner_.addonEventCallback_)
|
||||
owner_.addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {});
|
||||
if (owner_.addonEventCallbackRef())
|
||||
owner_.addonEventCallbackRef()("UNIT_THREAT_LIST_UPDATE", {});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +198,7 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
|
||||
void CombatHandler::startAutoAttack(uint64_t targetGuid) {
|
||||
// Can't attack yourself
|
||||
if (targetGuid == owner_.playerGuid) return;
|
||||
if (targetGuid == owner_.getPlayerGuid()) return;
|
||||
if (targetGuid == 0) return;
|
||||
|
||||
// Dismount when entering combat
|
||||
|
|
@ -209,9 +209,9 @@ void CombatHandler::startAutoAttack(uint64_t targetGuid) {
|
|||
// Client-side melee range gate to avoid starting "swing forever" loops when
|
||||
// target is already clearly out of range.
|
||||
if (auto target = owner_.getEntityManager().getEntity(targetGuid)) {
|
||||
float dx = owner_.movementInfo.x - target->getLatestX();
|
||||
float dy = owner_.movementInfo.y - target->getLatestY();
|
||||
float dz = owner_.movementInfo.z - target->getLatestZ();
|
||||
float dx = owner_.movementInfoRef().x - target->getLatestX();
|
||||
float dy = owner_.movementInfoRef().y - target->getLatestY();
|
||||
float dz = owner_.movementInfoRef().z - target->getLatestZ();
|
||||
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist3d > 8.0f) {
|
||||
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
|
||||
|
|
@ -232,9 +232,9 @@ void CombatHandler::startAutoAttack(uint64_t targetGuid) {
|
|||
autoAttackOutOfRangeTime_ = 0.0f;
|
||||
autoAttackResendTimer_ = 0.0f;
|
||||
autoAttackFacingSyncTimer_ = 0.0f;
|
||||
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
|
||||
if (owner_.getState() == WorldState::IN_WORLD && owner_.getSocket()) {
|
||||
auto packet = AttackSwingPacket::build(targetGuid);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
}
|
||||
LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec);
|
||||
}
|
||||
|
|
@ -249,13 +249,13 @@ void CombatHandler::stopAutoAttack() {
|
|||
autoAttackOutOfRangeTime_ = 0.0f;
|
||||
autoAttackResendTimer_ = 0.0f;
|
||||
autoAttackFacingSyncTimer_ = 0.0f;
|
||||
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
|
||||
if (owner_.getState() == WorldState::IN_WORLD && owner_.getSocket()) {
|
||||
auto packet = AttackStopPacket::build();
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
}
|
||||
LOG_INFO("Stopping auto-attack");
|
||||
if (owner_.addonEventCallback_)
|
||||
owner_.addonEventCallback_("PLAYER_LEAVE_COMBAT", {});
|
||||
if (owner_.addonEventCallbackRef())
|
||||
owner_.addonEventCallbackRef()("PLAYER_LEAVE_COMBAT", {});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -292,9 +292,9 @@ void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, ui
|
|||
// preserve "unknown/no source" (e.g. environmental damage) instead of
|
||||
// backfilling from current target.
|
||||
uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid
|
||||
: ((dstGuid != 0) ? 0 : (isPlayerSource ? owner_.playerGuid : owner_.targetGuid));
|
||||
: ((dstGuid != 0) ? 0 : (isPlayerSource ? owner_.getPlayerGuid() : owner_.getTargetGuid()));
|
||||
uint64_t effectiveDst = (dstGuid != 0) ? dstGuid
|
||||
: (isPlayerSource ? owner_.targetGuid : owner_.playerGuid);
|
||||
: (isPlayerSource ? owner_.getTargetGuid() : owner_.getPlayerGuid());
|
||||
log.sourceName = owner_.lookupName(effectiveSrc);
|
||||
log.targetName = (effectiveDst != 0) ? owner_.lookupName(effectiveDst) : std::string{};
|
||||
if (combatLog_.size() >= MAX_COMBAT_LOG)
|
||||
|
|
@ -303,7 +303,7 @@ void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, ui
|
|||
|
||||
// Fire COMBAT_LOG_EVENT_UNFILTERED for Lua addons
|
||||
// Args: subevent, sourceGUID, sourceName, 0 (sourceFlags), destGUID, destName, 0 (destFlags), spellId, spellName, amount
|
||||
if (owner_.addonEventCallback_) {
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
static const char* kSubevents[] = {
|
||||
"SWING_DAMAGE", "SPELL_DAMAGE", "SPELL_HEAL", "SWING_MISSED", "SWING_MISSED",
|
||||
"SWING_MISSED", "SWING_MISSED", "SWING_MISSED", "SPELL_DAMAGE", "SPELL_HEAL",
|
||||
|
|
@ -320,7 +320,7 @@ void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, ui
|
|||
snprintf(dstBuf, sizeof(dstBuf), "0x%016llX", (unsigned long long)effectiveDst);
|
||||
std::string spellName = (spellId != 0) ? owner_.getSpellName(spellId) : std::string{};
|
||||
std::string timestamp = std::to_string(static_cast<double>(std::time(nullptr)));
|
||||
owner_.addonEventCallback_("COMBAT_LOG_EVENT_UNFILTERED", {
|
||||
owner_.addonEventCallbackRef()("COMBAT_LOG_EVENT_UNFILTERED", {
|
||||
timestamp, subevent,
|
||||
srcBuf, log.sourceName, "0",
|
||||
dstBuf, log.targetName, "0",
|
||||
|
|
@ -370,8 +370,8 @@ void CombatHandler::updateCombatText(float deltaTime) {
|
|||
// ============================================================
|
||||
|
||||
void CombatHandler::autoTargetAttacker(uint64_t attackerGuid) {
|
||||
if (attackerGuid == 0 || attackerGuid == owner_.playerGuid) return;
|
||||
if (owner_.targetGuid != 0) return;
|
||||
if (attackerGuid == 0 || attackerGuid == owner_.getPlayerGuid()) return;
|
||||
if (owner_.getTargetGuid() != 0) return;
|
||||
if (!owner_.getEntityManager().hasEntity(attackerGuid)) return;
|
||||
owner_.setTarget(attackerGuid);
|
||||
}
|
||||
|
|
@ -380,23 +380,23 @@ void CombatHandler::handleAttackStart(network::Packet& packet) {
|
|||
AttackStartData data;
|
||||
if (!AttackStartParser::parse(packet, data)) return;
|
||||
|
||||
if (data.attackerGuid == owner_.playerGuid) {
|
||||
if (data.attackerGuid == owner_.getPlayerGuid()) {
|
||||
autoAttackRequested_ = true;
|
||||
autoAttacking_ = true;
|
||||
autoAttackRetryPending_ = false;
|
||||
autoAttackTarget_ = data.victimGuid;
|
||||
if (owner_.addonEventCallback_)
|
||||
owner_.addonEventCallback_("PLAYER_ENTER_COMBAT", {});
|
||||
} else if (data.victimGuid == owner_.playerGuid && data.attackerGuid != 0) {
|
||||
if (owner_.addonEventCallbackRef())
|
||||
owner_.addonEventCallbackRef()("PLAYER_ENTER_COMBAT", {});
|
||||
} else if (data.victimGuid == owner_.getPlayerGuid() && data.attackerGuid != 0) {
|
||||
hostileAttackers_.insert(data.attackerGuid);
|
||||
autoTargetAttacker(data.attackerGuid);
|
||||
|
||||
// Play aggro sound when NPC attacks player
|
||||
if (owner_.npcAggroCallback_) {
|
||||
if (owner_.npcAggroCallbackRef()) {
|
||||
auto entity = owner_.getEntityManager().getEntity(data.attackerGuid);
|
||||
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
|
||||
owner_.npcAggroCallback_(data.attackerGuid, pos);
|
||||
owner_.npcAggroCallbackRef()(data.attackerGuid, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -421,32 +421,32 @@ void CombatHandler::handleAttackStop(network::Packet& packet) {
|
|||
if (!AttackStopParser::parse(packet, data)) return;
|
||||
|
||||
// Keep intent, but clear server-confirmed active state until ATTACKSTART resumes.
|
||||
if (data.attackerGuid == owner_.playerGuid) {
|
||||
if (data.attackerGuid == owner_.getPlayerGuid()) {
|
||||
autoAttacking_ = false;
|
||||
autoAttackRetryPending_ = autoAttackRequested_;
|
||||
autoAttackResendTimer_ = 0.0f;
|
||||
LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)");
|
||||
} else if (data.victimGuid == owner_.playerGuid) {
|
||||
} else if (data.victimGuid == owner_.getPlayerGuid()) {
|
||||
hostileAttackers_.erase(data.attackerGuid);
|
||||
}
|
||||
}
|
||||
|
||||
void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||
AttackerStateUpdateData data;
|
||||
if (!owner_.packetParsers_->parseAttackerStateUpdate(packet, data)) return;
|
||||
if (!owner_.getPacketParsers()->parseAttackerStateUpdate(packet, data)) return;
|
||||
|
||||
bool isPlayerAttacker = (data.attackerGuid == owner_.playerGuid);
|
||||
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
|
||||
bool isPlayerAttacker = (data.attackerGuid == owner_.getPlayerGuid());
|
||||
bool isPlayerTarget = (data.targetGuid == owner_.getPlayerGuid());
|
||||
if (!isPlayerAttacker && !isPlayerTarget) return; // Not our combat
|
||||
|
||||
if (isPlayerAttacker) {
|
||||
lastMeleeSwingMs_ = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(0);
|
||||
if (owner_.meleeSwingCallbackRef()) owner_.meleeSwingCallbackRef()(0);
|
||||
}
|
||||
if (!isPlayerAttacker && owner_.npcSwingCallback_) {
|
||||
owner_.npcSwingCallback_(data.attackerGuid);
|
||||
if (!isPlayerAttacker && owner_.npcSwingCallbackRef()) {
|
||||
owner_.npcSwingCallbackRef()(data.attackerGuid);
|
||||
}
|
||||
|
||||
if (isPlayerTarget && data.attackerGuid != 0) {
|
||||
|
|
@ -524,24 +524,24 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
|||
}
|
||||
|
||||
// Fire hit reaction animation on the victim
|
||||
if (owner_.hitReactionCallback_ && !data.isMiss()) {
|
||||
if (owner_.hitReactionCallbackRef() && !data.isMiss()) {
|
||||
using HR = GameHandler::HitReaction;
|
||||
HR reaction = HR::WOUND;
|
||||
if (data.victimState == 1) reaction = HR::DODGE;
|
||||
else if (data.victimState == 2) reaction = HR::PARRY;
|
||||
else if (data.victimState == 4) reaction = HR::BLOCK;
|
||||
else if (data.isCrit()) reaction = HR::CRIT_WOUND;
|
||||
owner_.hitReactionCallback_(data.targetGuid, reaction);
|
||||
owner_.hitReactionCallbackRef()(data.targetGuid, reaction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
|
||||
SpellDamageLogData data;
|
||||
if (!owner_.packetParsers_->parseSpellDamageLog(packet, data)) return;
|
||||
if (!owner_.getPacketParsers()->parseSpellDamageLog(packet, data)) return;
|
||||
|
||||
bool isPlayerSource = (data.attackerGuid == owner_.playerGuid);
|
||||
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
|
||||
bool isPlayerSource = (data.attackerGuid == owner_.getPlayerGuid());
|
||||
bool isPlayerTarget = (data.targetGuid == owner_.getPlayerGuid());
|
||||
if (!isPlayerSource && !isPlayerTarget) return; // Not our combat
|
||||
|
||||
if (isPlayerTarget && data.attackerGuid != 0) {
|
||||
|
|
@ -560,10 +560,10 @@ void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
|
|||
|
||||
void CombatHandler::handleSpellHealLog(network::Packet& packet) {
|
||||
SpellHealLogData data;
|
||||
if (!owner_.packetParsers_->parseSpellHealLog(packet, data)) return;
|
||||
if (!owner_.getPacketParsers()->parseSpellHealLog(packet, data)) return;
|
||||
|
||||
bool isPlayerSource = (data.casterGuid == owner_.playerGuid);
|
||||
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
|
||||
bool isPlayerSource = (data.casterGuid == owner_.getPlayerGuid());
|
||||
bool isPlayerTarget = (data.targetGuid == owner_.getPlayerGuid());
|
||||
if (!isPlayerSource && !isPlayerTarget) return; // Not our combat
|
||||
|
||||
auto type = data.isCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::HEAL;
|
||||
|
|
@ -608,9 +608,9 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
|
|||
const float targetX = targetEntity->getLatestX();
|
||||
const float targetY = targetEntity->getLatestY();
|
||||
const float targetZ = targetEntity->getLatestZ();
|
||||
float dx = owner_.movementInfo.x - targetX;
|
||||
float dy = owner_.movementInfo.y - targetY;
|
||||
float dz = owner_.movementInfo.z - targetZ;
|
||||
float dx = owner_.movementInfoRef().x - targetX;
|
||||
float dy = owner_.movementInfoRef().y - targetY;
|
||||
float dz = owner_.movementInfoRef().z - targetZ;
|
||||
float dist = std::sqrt(dx * dx + dy * dy);
|
||||
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
const bool classicLike = isPreWotlk();
|
||||
|
|
@ -652,7 +652,7 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
|
|||
autoAttackResendTimer_ = 0.0f;
|
||||
autoAttackRetryPending_ = false;
|
||||
auto pkt = AttackSwingPacket::build(autoAttackTarget_);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
}
|
||||
|
||||
// Keep server-facing aligned while trying to acquire melee.
|
||||
|
|
@ -661,16 +661,16 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
|
|||
if (allowPeriodicFacingSync &&
|
||||
autoAttackFacingSyncTimer_ >= facingSyncInterval) {
|
||||
autoAttackFacingSyncTimer_ = 0.0f;
|
||||
float toTargetX = targetX - owner_.movementInfo.x;
|
||||
float toTargetY = targetY - owner_.movementInfo.y;
|
||||
float toTargetX = targetX - owner_.movementInfoRef().x;
|
||||
float toTargetY = targetY - owner_.movementInfoRef().y;
|
||||
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
|
||||
float desired = std::atan2(-toTargetY, toTargetX);
|
||||
float diff = desired - owner_.movementInfo.orientation;
|
||||
float diff = desired - owner_.movementInfoRef().orientation;
|
||||
while (diff > static_cast<float>(M_PI)) diff -= 2.0f * static_cast<float>(M_PI);
|
||||
while (diff < -static_cast<float>(M_PI)) diff += 2.0f * static_cast<float>(M_PI);
|
||||
const float facingThreshold = classicLike ? 0.035f : 0.12f;
|
||||
if (std::abs(diff) > facingThreshold) {
|
||||
owner_.movementInfo.orientation = desired;
|
||||
owner_.movementInfoRef().orientation = desired;
|
||||
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
|
||||
}
|
||||
}
|
||||
|
|
@ -685,8 +685,8 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
|
|||
for (uint64_t attackerGuid : hostileAttackers_) {
|
||||
auto attacker = owner_.getEntityManager().getEntity(attackerGuid);
|
||||
if (!attacker) continue;
|
||||
float dx = owner_.movementInfo.x - attacker->getX();
|
||||
float dy = owner_.movementInfo.y - attacker->getY();
|
||||
float dx = owner_.movementInfoRef().x - attacker->getX();
|
||||
float dy = owner_.movementInfoRef().y - attacker->getY();
|
||||
if (std::abs(dx) < 0.01f && std::abs(dy) < 0.01f) continue;
|
||||
attacker->setOrientation(std::atan2(-dy, dx));
|
||||
}
|
||||
|
|
@ -758,7 +758,7 @@ void CombatHandler::handlePowerUpdate(network::Packet& packet) {
|
|||
auto unitId = owner_.guidToUnitId(guid);
|
||||
if (!unitId.empty()) {
|
||||
owner_.fireAddonEvent("UNIT_POWER", {unitId});
|
||||
if (guid == owner_.playerGuid) {
|
||||
if (guid == owner_.getPlayerGuid()) {
|
||||
owner_.fireAddonEvent("ACTIONBAR_UPDATE_USABLE", {});
|
||||
owner_.fireAddonEvent("SPELL_UPDATE_USABLE", {});
|
||||
}
|
||||
|
|
@ -771,10 +771,10 @@ void CombatHandler::handleUpdateComboPoints(network::Packet& packet) {
|
|||
if (!packet.hasRemaining(cpTbc ? 8u : 2u) ) return;
|
||||
uint64_t target = cpTbc ? packet.readUInt64() : packet.readPackedGuid();
|
||||
if (!packet.hasRemaining(1)) return;
|
||||
owner_.comboPoints_ = packet.readUInt8();
|
||||
owner_.comboTarget_ = target;
|
||||
owner_.comboPointsRef() = packet.readUInt8();
|
||||
owner_.comboTargetRef() = target;
|
||||
LOG_DEBUG("SMSG_UPDATE_COMBO_POINTS: target=0x", std::hex, target,
|
||||
std::dec, " points=", static_cast<int>(owner_.comboPoints_));
|
||||
std::dec, " points=", static_cast<int>(owner_.comboPointsRef()));
|
||||
owner_.fireAddonEvent("PLAYER_COMBO_POINTS", {});
|
||||
}
|
||||
|
||||
|
|
@ -787,7 +787,7 @@ void CombatHandler::handlePvpCredit(network::Packet& packet) {
|
|||
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
|
||||
owner_.addSystemChatMessage(msg);
|
||||
if (honor > 0) addCombatText(CombatTextEntry::HONOR_GAIN, static_cast<int32_t>(honor), 0, true);
|
||||
if (owner_.pvpHonorCallback_) owner_.pvpHonorCallback_(honor, victimGuid, rank);
|
||||
if (owner_.pvpHonorCallbackRef()) owner_.pvpHonorCallbackRef()(honor, victimGuid, rank);
|
||||
owner_.fireAddonEvent("CHAT_MSG_COMBAT_HONOR_GAIN", {msg});
|
||||
}
|
||||
}
|
||||
|
|
@ -805,8 +805,8 @@ void CombatHandler::handleProcResist(network::Packet& packet) {
|
|||
uint64_t victim = readPrGuid();
|
||||
if (!packet.hasRemaining(4)) return;
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
if (victim == owner_.playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim);
|
||||
else if (caster == owner_.playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim);
|
||||
if (victim == owner_.getPlayerGuid()) addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim);
|
||||
else if (caster == owner_.getPlayerGuid()) addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim);
|
||||
packet.skipAll();
|
||||
}
|
||||
|
||||
|
|
@ -846,10 +846,10 @@ void CombatHandler::handleSpellDamageShield(network::Packet& packet) {
|
|||
/*uint32_t absorbed =*/ packet.readUInt32();
|
||||
/*uint32_t school =*/ packet.readUInt32();
|
||||
// Show combat text: damage shield reflect
|
||||
if (casterGuid == owner_.playerGuid) {
|
||||
if (casterGuid == owner_.getPlayerGuid()) {
|
||||
// We have a damage shield that reflected damage
|
||||
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(damage), shieldSpellId, true, 0, casterGuid, victimGuid);
|
||||
} else if (victimGuid == owner_.playerGuid) {
|
||||
} else if (victimGuid == owner_.getPlayerGuid()) {
|
||||
// A damage shield hit us (e.g. target's Thorns)
|
||||
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(damage), shieldSpellId, false, 0, casterGuid, victimGuid);
|
||||
}
|
||||
|
|
@ -878,9 +878,9 @@ void CombatHandler::handleSpellOrDamageImmune(network::Packet& packet) {
|
|||
/*uint8_t saveType =*/ packet.readUInt8();
|
||||
// Show IMMUNE text when the player is the caster (we hit an immune target)
|
||||
// or the victim (we are immune)
|
||||
if (casterGuid == owner_.playerGuid || victimGuid == owner_.playerGuid) {
|
||||
if (casterGuid == owner_.getPlayerGuid() || victimGuid == owner_.getPlayerGuid()) {
|
||||
addCombatText(CombatTextEntry::IMMUNE, 0, immuneSpellId,
|
||||
casterGuid == owner_.playerGuid, 0, casterGuid, victimGuid);
|
||||
casterGuid == owner_.getPlayerGuid(), 0, casterGuid, victimGuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -916,9 +916,9 @@ void CombatHandler::handleResistLog(network::Packet& packet) {
|
|||
/*uint32_t targetRes =*/ packet.readUInt32();
|
||||
int32_t resistedAmount = static_cast<int32_t>(packet.readUInt32());
|
||||
// Show RESIST when the player is involved on either side.
|
||||
if (resistedAmount > 0 && victimGuid == owner_.playerGuid) {
|
||||
if (resistedAmount > 0 && victimGuid == owner_.getPlayerGuid()) {
|
||||
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid);
|
||||
} else if (resistedAmount > 0 && attackerGuid == owner_.playerGuid) {
|
||||
} else if (resistedAmount > 0 && attackerGuid == owner_.getPlayerGuid()) {
|
||||
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid);
|
||||
}
|
||||
packet.skipAll();
|
||||
|
|
@ -985,10 +985,10 @@ void CombatHandler::handlePetCastFailed(network::Packet& packet) {
|
|||
|
||||
void CombatHandler::handlePetBroken(network::Packet& packet) {
|
||||
// Pet bond broken (died or forcibly dismissed) — clear pet state
|
||||
owner_.petGuid_ = 0;
|
||||
owner_.petSpellList_.clear();
|
||||
owner_.petAutocastSpells_.clear();
|
||||
memset(owner_.petActionSlots_, 0, sizeof(owner_.petActionSlots_));
|
||||
owner_.petGuidRef() = 0;
|
||||
owner_.petSpellListRef().clear();
|
||||
owner_.petAutocastSpellsRef().clear();
|
||||
memset(owner_.petActionSlotsRef(), 0, sizeof(owner_.petActionSlotsRef()));
|
||||
owner_.addSystemChatMessage("Your pet has died.");
|
||||
LOG_INFO("SMSG_PET_BROKEN: pet bond broken");
|
||||
packet.skipAll();
|
||||
|
|
@ -997,7 +997,7 @@ void CombatHandler::handlePetBroken(network::Packet& packet) {
|
|||
void CombatHandler::handlePetLearnedSpell(network::Packet& packet) {
|
||||
if (packet.hasRemaining(4)) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
owner_.petSpellList_.push_back(spellId);
|
||||
owner_.petSpellListRef().push_back(spellId);
|
||||
const std::string& sname = owner_.getSpellName(spellId);
|
||||
owner_.addSystemChatMessage("Your pet has learned " + (sname.empty() ? "a new ability." : sname + "."));
|
||||
LOG_DEBUG("SMSG_PET_LEARNED_SPELL: spellId=", spellId);
|
||||
|
|
@ -1009,10 +1009,10 @@ void CombatHandler::handlePetLearnedSpell(network::Packet& packet) {
|
|||
void CombatHandler::handlePetUnlearnedSpell(network::Packet& packet) {
|
||||
if (packet.hasRemaining(4)) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
owner_.petSpellList_.erase(
|
||||
std::remove(owner_.petSpellList_.begin(), owner_.petSpellList_.end(), spellId),
|
||||
owner_.petSpellList_.end());
|
||||
owner_.petAutocastSpells_.erase(spellId);
|
||||
owner_.petSpellListRef().erase(
|
||||
std::remove(owner_.petSpellListRef().begin(), owner_.petSpellListRef().end(), spellId),
|
||||
owner_.petSpellListRef().end());
|
||||
owner_.petAutocastSpellsRef().erase(spellId);
|
||||
LOG_DEBUG("SMSG_PET_UNLEARNED_SPELL: spellId=", spellId);
|
||||
}
|
||||
packet.skipAll();
|
||||
|
|
@ -1024,11 +1024,11 @@ void CombatHandler::handlePetMode(network::Packet& packet) {
|
|||
if (packet.hasRemaining(12)) {
|
||||
uint64_t modeGuid = packet.readUInt64();
|
||||
uint32_t mode = packet.readUInt32();
|
||||
if (modeGuid == owner_.petGuid_) {
|
||||
owner_.petCommand_ = static_cast<uint8_t>(mode & 0xFF);
|
||||
owner_.petReact_ = static_cast<uint8_t>((mode >> 8) & 0xFF);
|
||||
LOG_DEBUG("SMSG_PET_MODE: command=", static_cast<int>(owner_.petCommand_),
|
||||
" react=", static_cast<int>(owner_.petReact_));
|
||||
if (modeGuid == owner_.petGuidRef()) {
|
||||
owner_.petCommandRef() = static_cast<uint8_t>(mode & 0xFF);
|
||||
owner_.petReactRef() = static_cast<uint8_t>((mode >> 8) & 0xFF);
|
||||
LOG_DEBUG("SMSG_PET_MODE: command=", static_cast<int>(owner_.petCommandRef()),
|
||||
" react=", static_cast<int>(owner_.petReactRef()));
|
||||
}
|
||||
}
|
||||
packet.skipAll();
|
||||
|
|
@ -1054,18 +1054,18 @@ void CombatHandler::handleResurrectFailed(network::Packet& packet) {
|
|||
// ============================================================
|
||||
|
||||
void CombatHandler::setTarget(uint64_t guid) {
|
||||
if (guid == owner_.targetGuid) return;
|
||||
if (guid == owner_.getTargetGuid()) return;
|
||||
|
||||
// Save previous target
|
||||
if (owner_.targetGuid != 0) {
|
||||
owner_.lastTargetGuid = owner_.targetGuid;
|
||||
if (owner_.getTargetGuid() != 0) {
|
||||
owner_.lastTargetGuidRef() = owner_.getTargetGuid();
|
||||
}
|
||||
|
||||
owner_.targetGuid = guid;
|
||||
owner_.setTargetGuidRaw(guid);
|
||||
|
||||
// Clear stale aura data from the previous target so the buff bar shows
|
||||
// an empty state until the server sends SMSG_AURA_UPDATE_ALL for the new target.
|
||||
if (owner_.spellHandler_) for (auto& slot : owner_.spellHandler_->targetAuras_) slot = AuraSlot{};
|
||||
if (owner_.getSpellHandler()) for (auto& slot : owner_.getSpellHandler()->targetAuras_) slot = AuraSlot{};
|
||||
|
||||
// Clear previous target's cast bar on target change
|
||||
// (the new target's cast state is naturally fetched from spellHandler_->unitCastStates_ by GUID)
|
||||
|
|
@ -1073,7 +1073,7 @@ void CombatHandler::setTarget(uint64_t guid) {
|
|||
// Inform server of target selection
|
||||
if (owner_.isInWorld()) {
|
||||
auto packet = SetSelectionPacket::build(guid);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
}
|
||||
|
||||
if (guid != 0) {
|
||||
|
|
@ -1083,27 +1083,27 @@ void CombatHandler::setTarget(uint64_t guid) {
|
|||
}
|
||||
|
||||
void CombatHandler::clearTarget() {
|
||||
if (owner_.targetGuid != 0) {
|
||||
if (owner_.getTargetGuid() != 0) {
|
||||
LOG_INFO("Target cleared");
|
||||
// Zero the GUID before firing the event so callbacks/addons that query
|
||||
// the current target see null (consistent with setTarget which updates
|
||||
// targetGuid before the event).
|
||||
owner_.targetGuid = 0;
|
||||
owner_.setTargetGuidRaw(0);
|
||||
owner_.fireAddonEvent("PLAYER_TARGET_CHANGED", {});
|
||||
} else {
|
||||
owner_.targetGuid = 0;
|
||||
owner_.setTargetGuidRaw(0);
|
||||
}
|
||||
owner_.tabCycleIndex = -1;
|
||||
owner_.tabCycleStale = true;
|
||||
owner_.tabCycleIndexRef() = -1;
|
||||
owner_.tabCycleStaleRef() = true;
|
||||
}
|
||||
|
||||
std::shared_ptr<Entity> CombatHandler::getTarget() const {
|
||||
if (owner_.targetGuid == 0) return nullptr;
|
||||
return owner_.getEntityManager().getEntity(owner_.targetGuid);
|
||||
if (owner_.getTargetGuid() == 0) return nullptr;
|
||||
return owner_.getEntityManager().getEntity(owner_.getTargetGuid());
|
||||
}
|
||||
|
||||
void CombatHandler::setFocus(uint64_t guid) {
|
||||
owner_.focusGuid = guid;
|
||||
owner_.focusGuidRef() = guid;
|
||||
owner_.fireAddonEvent("PLAYER_FOCUS_CHANGED", {});
|
||||
if (guid != 0) {
|
||||
auto entity = owner_.getEntityManager().getEntity(guid);
|
||||
|
|
@ -1122,36 +1122,36 @@ void CombatHandler::setFocus(uint64_t guid) {
|
|||
}
|
||||
|
||||
void CombatHandler::clearFocus() {
|
||||
if (owner_.focusGuid != 0) {
|
||||
if (owner_.focusGuidRef() != 0) {
|
||||
owner_.addSystemChatMessage("Focus cleared.");
|
||||
LOG_INFO("Focus cleared");
|
||||
}
|
||||
owner_.focusGuid = 0;
|
||||
owner_.focusGuidRef() = 0;
|
||||
owner_.fireAddonEvent("PLAYER_FOCUS_CHANGED", {});
|
||||
}
|
||||
|
||||
std::shared_ptr<Entity> CombatHandler::getFocus() const {
|
||||
if (owner_.focusGuid == 0) return nullptr;
|
||||
return owner_.getEntityManager().getEntity(owner_.focusGuid);
|
||||
if (owner_.focusGuidRef() == 0) return nullptr;
|
||||
return owner_.getEntityManager().getEntity(owner_.focusGuidRef());
|
||||
}
|
||||
|
||||
void CombatHandler::setMouseoverGuid(uint64_t guid) {
|
||||
if (owner_.mouseoverGuid_ != guid) {
|
||||
owner_.mouseoverGuid_ = guid;
|
||||
if (owner_.mouseoverGuidRef() != guid) {
|
||||
owner_.mouseoverGuidRef() = guid;
|
||||
owner_.fireAddonEvent("UPDATE_MOUSEOVER_UNIT", {});
|
||||
}
|
||||
}
|
||||
|
||||
void CombatHandler::targetLastTarget() {
|
||||
if (owner_.lastTargetGuid == 0) {
|
||||
if (owner_.lastTargetGuidRef() == 0) {
|
||||
owner_.addSystemChatMessage("No previous target.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Swap current and last target
|
||||
uint64_t temp = owner_.targetGuid;
|
||||
setTarget(owner_.lastTargetGuid);
|
||||
owner_.lastTargetGuid = temp;
|
||||
uint64_t temp = owner_.getTargetGuid();
|
||||
setTarget(owner_.lastTargetGuidRef());
|
||||
owner_.lastTargetGuidRef() = temp;
|
||||
}
|
||||
|
||||
void CombatHandler::targetEnemy(bool reverse) {
|
||||
|
|
@ -1162,7 +1162,7 @@ void CombatHandler::targetEnemy(bool reverse) {
|
|||
for (const auto& [guid, entity] : entities) {
|
||||
if (entity->getType() == ObjectType::UNIT) {
|
||||
auto unit = std::dynamic_pointer_cast<Unit>(entity);
|
||||
if (unit && guid != owner_.playerGuid && unit->isHostile()) {
|
||||
if (unit && guid != owner_.getPlayerGuid() && unit->isHostile()) {
|
||||
hostiles.push_back(guid);
|
||||
}
|
||||
}
|
||||
|
|
@ -1174,7 +1174,7 @@ void CombatHandler::targetEnemy(bool reverse) {
|
|||
}
|
||||
|
||||
// Find current target in list
|
||||
auto it = std::find(hostiles.begin(), hostiles.end(), owner_.targetGuid);
|
||||
auto it = std::find(hostiles.begin(), hostiles.end(), owner_.getTargetGuid());
|
||||
|
||||
if (it == hostiles.end()) {
|
||||
// Not currently targeting a hostile, target first one
|
||||
|
|
@ -1204,7 +1204,7 @@ void CombatHandler::targetFriend(bool reverse) {
|
|||
auto& entities = owner_.getEntityManager().getEntities();
|
||||
|
||||
for (const auto& [guid, entity] : entities) {
|
||||
if (entity->getType() == ObjectType::PLAYER && guid != owner_.playerGuid) {
|
||||
if (entity->getType() == ObjectType::PLAYER && guid != owner_.getPlayerGuid()) {
|
||||
friendlies.push_back(guid);
|
||||
}
|
||||
}
|
||||
|
|
@ -1215,7 +1215,7 @@ void CombatHandler::targetFriend(bool reverse) {
|
|||
}
|
||||
|
||||
// Find current target in list
|
||||
auto it = std::find(friendlies.begin(), friendlies.end(), owner_.targetGuid);
|
||||
auto it = std::find(friendlies.begin(), friendlies.end(), owner_.getTargetGuid());
|
||||
|
||||
if (it == friendlies.end()) {
|
||||
// Not currently targeting a friend, target first one
|
||||
|
|
@ -1247,8 +1247,8 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
|||
auto* unit = dynamic_cast<Unit*>(e.get());
|
||||
if (!unit) return false;
|
||||
if (unit->getHealth() == 0) {
|
||||
auto lootIt = owner_.localLootState_.find(guid);
|
||||
if (lootIt == owner_.localLootState_.end() || lootIt->second.data.items.empty()) {
|
||||
auto lootIt = owner_.localLootStateRef().find(guid);
|
||||
if (lootIt == owner_.localLootStateRef().end() || lootIt->second.data.items.empty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1260,9 +1260,9 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
|||
};
|
||||
|
||||
// Rebuild cycle list if stale (entity added/removed since last tab press).
|
||||
if (owner_.tabCycleStale) {
|
||||
owner_.tabCycleList.clear();
|
||||
owner_.tabCycleIndex = -1;
|
||||
if (owner_.tabCycleStaleRef()) {
|
||||
owner_.tabCycleListRef().clear();
|
||||
owner_.tabCycleIndexRef() = -1;
|
||||
|
||||
struct EntityDist { uint64_t guid; float distance; };
|
||||
std::vector<EntityDist> sortable;
|
||||
|
|
@ -1270,7 +1270,7 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
|||
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
|
||||
if (guid == owner_.playerGuid) continue;
|
||||
if (guid == owner_.getPlayerGuid()) continue;
|
||||
if (!isValidTabTarget(entity)) continue;
|
||||
float dx = entity->getX() - playerX;
|
||||
float dy = entity->getY() - playerY;
|
||||
|
|
@ -1282,22 +1282,22 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
|||
[](const EntityDist& a, const EntityDist& b) { return a.distance < b.distance; });
|
||||
|
||||
for (const auto& ed : sortable) {
|
||||
owner_.tabCycleList.push_back(ed.guid);
|
||||
owner_.tabCycleListRef().push_back(ed.guid);
|
||||
}
|
||||
owner_.tabCycleStale = false;
|
||||
owner_.tabCycleStaleRef() = false;
|
||||
}
|
||||
|
||||
if (owner_.tabCycleList.empty()) {
|
||||
if (owner_.tabCycleListRef().empty()) {
|
||||
clearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
// Advance through the cycle, skipping any entry that has since died or
|
||||
// turned friendly (e.g. NPC killed between two tab presses).
|
||||
int tries = static_cast<int>(owner_.tabCycleList.size());
|
||||
int tries = static_cast<int>(owner_.tabCycleListRef().size());
|
||||
while (tries-- > 0) {
|
||||
owner_.tabCycleIndex = (owner_.tabCycleIndex + 1) % static_cast<int>(owner_.tabCycleList.size());
|
||||
uint64_t guid = owner_.tabCycleList[owner_.tabCycleIndex];
|
||||
owner_.tabCycleIndexRef() = (owner_.tabCycleIndexRef() + 1) % static_cast<int>(owner_.tabCycleListRef().size());
|
||||
uint64_t guid = owner_.tabCycleListRef()[owner_.tabCycleIndexRef()];
|
||||
auto entity = owner_.getEntityManager().getEntity(guid);
|
||||
if (isValidTabTarget(entity)) {
|
||||
setTarget(guid);
|
||||
|
|
@ -1306,17 +1306,17 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
|||
}
|
||||
|
||||
// All cached entries are stale — clear target and force a fresh rebuild next time.
|
||||
owner_.tabCycleStale = true;
|
||||
owner_.tabCycleStaleRef() = true;
|
||||
clearTarget();
|
||||
}
|
||||
|
||||
void CombatHandler::assistTarget() {
|
||||
if (owner_.state != WorldState::IN_WORLD) {
|
||||
if (owner_.getState() != WorldState::IN_WORLD) {
|
||||
LOG_WARNING("Cannot assist: not in world");
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner_.targetGuid == 0) {
|
||||
if (owner_.getTargetGuid() == 0) {
|
||||
owner_.addSystemChatMessage("You must target someone to assist.");
|
||||
return;
|
||||
}
|
||||
|
|
@ -1373,8 +1373,8 @@ void CombatHandler::togglePvp() {
|
|||
}
|
||||
|
||||
auto packet = TogglePvpPacket::build();
|
||||
owner_.socket->send(packet);
|
||||
auto entity = owner_.getEntityManager().getEntity(owner_.playerGuid);
|
||||
owner_.getSocket()->send(packet);
|
||||
auto entity = owner_.getEntityManager().getEntity(owner_.getPlayerGuid());
|
||||
bool currentlyPvp = false;
|
||||
if (entity) {
|
||||
// UNIT_FIELD_FLAGS (index 59), bit 0x1000 = UNIT_FLAG_PVP
|
||||
|
|
@ -1393,93 +1393,93 @@ void CombatHandler::togglePvp() {
|
|||
// ============================================================
|
||||
|
||||
void CombatHandler::releaseSpirit() {
|
||||
if (owner_.socket && owner_.state == WorldState::IN_WORLD) {
|
||||
if (owner_.getSocket() && owner_.getState() == WorldState::IN_WORLD) {
|
||||
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
if (owner_.repopPending_ && now - static_cast<int64_t>(owner_.lastRepopRequestMs_) < 1000) {
|
||||
if (owner_.repopPendingRef() && now - static_cast<int64_t>(owner_.lastRepopRequestMsRef()) < 1000) {
|
||||
return;
|
||||
}
|
||||
auto packet = RepopRequestPacket::build();
|
||||
owner_.socket->send(packet);
|
||||
owner_.selfResAvailable_ = false;
|
||||
owner_.repopPending_ = true;
|
||||
owner_.lastRepopRequestMs_ = static_cast<uint64_t>(now);
|
||||
owner_.getSocket()->send(packet);
|
||||
owner_.selfResAvailableRef() = false;
|
||||
owner_.repopPendingRef() = true;
|
||||
owner_.lastRepopRequestMsRef() = static_cast<uint64_t>(now);
|
||||
LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)");
|
||||
network::Packet cq(wireOpcode(Opcode::MSG_CORPSE_QUERY));
|
||||
owner_.socket->send(cq);
|
||||
owner_.getSocket()->send(cq);
|
||||
}
|
||||
}
|
||||
|
||||
bool CombatHandler::canReclaimCorpse() const {
|
||||
if (!owner_.releasedSpirit_ || owner_.corpseGuid_ == 0 || owner_.corpseMapId_ == 0) return false;
|
||||
if (owner_.currentMapId_ != owner_.corpseMapId_) return false;
|
||||
float dx = owner_.movementInfo.x - owner_.corpseY_;
|
||||
float dy = owner_.movementInfo.y - owner_.corpseX_;
|
||||
float dz = owner_.movementInfo.z - owner_.corpseZ_;
|
||||
if (!owner_.releasedSpiritRef() || owner_.corpseGuidRef() == 0 || owner_.corpseMapIdRef() == 0) return false;
|
||||
if (owner_.currentMapIdRef() != owner_.corpseMapIdRef()) return false;
|
||||
float dx = owner_.movementInfoRef().x - owner_.corpseYRef();
|
||||
float dy = owner_.movementInfoRef().y - owner_.corpseXRef();
|
||||
float dz = owner_.movementInfoRef().z - owner_.corpseZRef();
|
||||
return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f);
|
||||
}
|
||||
|
||||
float CombatHandler::getCorpseReclaimDelaySec() const {
|
||||
if (owner_.corpseReclaimAvailableMs_ == 0) return 0.0f;
|
||||
if (owner_.corpseReclaimAvailableMsRef() == 0) return 0.0f;
|
||||
auto nowMs = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
if (nowMs >= owner_.corpseReclaimAvailableMs_) return 0.0f;
|
||||
return static_cast<float>(owner_.corpseReclaimAvailableMs_ - nowMs) / 1000.0f;
|
||||
if (nowMs >= owner_.corpseReclaimAvailableMsRef()) return 0.0f;
|
||||
return static_cast<float>(owner_.corpseReclaimAvailableMsRef() - nowMs) / 1000.0f;
|
||||
}
|
||||
|
||||
void CombatHandler::reclaimCorpse() {
|
||||
if (!canReclaimCorpse() || !owner_.socket) return;
|
||||
if (owner_.corpseGuid_ == 0) {
|
||||
if (!canReclaimCorpse() || !owner_.getSocket()) return;
|
||||
if (owner_.corpseGuidRef() == 0) {
|
||||
LOG_WARNING("reclaimCorpse: corpse GUID not yet known (corpse object not received); cannot reclaim");
|
||||
return;
|
||||
}
|
||||
auto packet = ReclaimCorpsePacket::build(owner_.corpseGuid_);
|
||||
owner_.socket->send(packet);
|
||||
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, owner_.corpseGuid_, std::dec);
|
||||
auto packet = ReclaimCorpsePacket::build(owner_.corpseGuidRef());
|
||||
owner_.getSocket()->send(packet);
|
||||
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, owner_.corpseGuidRef(), std::dec);
|
||||
}
|
||||
|
||||
void CombatHandler::useSelfRes() {
|
||||
if (!owner_.selfResAvailable_ || !owner_.socket) return;
|
||||
if (!owner_.selfResAvailableRef() || !owner_.getSocket()) return;
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_SELF_RES));
|
||||
owner_.socket->send(pkt);
|
||||
owner_.selfResAvailable_ = false;
|
||||
owner_.getSocket()->send(pkt);
|
||||
owner_.selfResAvailableRef() = false;
|
||||
LOG_INFO("Sent CMSG_SELF_RES (Reincarnation / Twisting Nether)");
|
||||
}
|
||||
|
||||
void CombatHandler::activateSpiritHealer(uint64_t npcGuid) {
|
||||
if (!owner_.isInWorld()) return;
|
||||
owner_.pendingSpiritHealerGuid_ = npcGuid;
|
||||
owner_.pendingSpiritHealerGuidRef() = npcGuid;
|
||||
auto packet = SpiritHealerActivatePacket::build(npcGuid);
|
||||
owner_.socket->send(packet);
|
||||
owner_.resurrectPending_ = true;
|
||||
owner_.getSocket()->send(packet);
|
||||
owner_.resurrectPendingRef() = true;
|
||||
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x", std::hex, npcGuid, std::dec);
|
||||
}
|
||||
|
||||
void CombatHandler::acceptResurrect() {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !owner_.resurrectRequestPending_) return;
|
||||
if (owner_.resurrectIsSpiritHealer_) {
|
||||
auto activate = SpiritHealerActivatePacket::build(owner_.resurrectCasterGuid_);
|
||||
owner_.socket->send(activate);
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !owner_.resurrectRequestPendingRef()) return;
|
||||
if (owner_.resurrectIsSpiritHealerRef()) {
|
||||
auto activate = SpiritHealerActivatePacket::build(owner_.resurrectCasterGuidRef());
|
||||
owner_.getSocket()->send(activate);
|
||||
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x",
|
||||
std::hex, owner_.resurrectCasterGuid_, std::dec);
|
||||
std::hex, owner_.resurrectCasterGuidRef(), std::dec);
|
||||
} else {
|
||||
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuid_, true);
|
||||
owner_.socket->send(resp);
|
||||
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuidRef(), true);
|
||||
owner_.getSocket()->send(resp);
|
||||
LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (accept) for 0x",
|
||||
std::hex, owner_.resurrectCasterGuid_, std::dec);
|
||||
std::hex, owner_.resurrectCasterGuidRef(), std::dec);
|
||||
}
|
||||
owner_.resurrectRequestPending_ = false;
|
||||
owner_.resurrectPending_ = true;
|
||||
owner_.resurrectRequestPendingRef() = false;
|
||||
owner_.resurrectPendingRef() = true;
|
||||
}
|
||||
|
||||
void CombatHandler::declineResurrect() {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !owner_.resurrectRequestPending_) return;
|
||||
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuid_, false);
|
||||
owner_.socket->send(resp);
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !owner_.resurrectRequestPendingRef()) return;
|
||||
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuidRef(), false);
|
||||
owner_.getSocket()->send(resp);
|
||||
LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (decline) for 0x",
|
||||
std::hex, owner_.resurrectCasterGuid_, std::dec);
|
||||
owner_.resurrectRequestPending_ = false;
|
||||
std::hex, owner_.resurrectCasterGuidRef(), std::dec);
|
||||
owner_.resurrectRequestPendingRef() = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2468
src/game/game_handler_callbacks.cpp
Normal file
2468
src/game/game_handler_callbacks.cpp
Normal file
File diff suppressed because it is too large
Load diff
2937
src/game/game_handler_packets.cpp
Normal file
2937
src/game/game_handler_packets.cpp
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -338,7 +338,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
table[Opcode::SMSG_QUESTGIVER_STATUS] = [this](network::Packet& packet) {
|
||||
if (packet.hasRemaining(9)) {
|
||||
uint64_t npcGuid = packet.readUInt64();
|
||||
uint8_t status = owner_.packetParsers_->readQuestGiverStatus(packet);
|
||||
uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet);
|
||||
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
|
||||
}
|
||||
};
|
||||
|
|
@ -350,7 +350,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (!packet.hasRemaining(9)) break;
|
||||
uint64_t npcGuid = packet.readUInt64();
|
||||
uint8_t status = owner_.packetParsers_->readQuestGiverStatus(packet);
|
||||
uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet);
|
||||
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
|
||||
}
|
||||
};
|
||||
|
|
@ -466,8 +466,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
for (auto it = questLog_.begin(); it != questLog_.end(); ++it) {
|
||||
if (it->questId == questId) {
|
||||
// Fire toast callback before erasing
|
||||
if (owner_.questCompleteCallback_) {
|
||||
owner_.questCompleteCallback_(questId, it->title);
|
||||
if (owner_.questCompleteCallbackRef()) {
|
||||
owner_.questCompleteCallbackRef()(questId, it->title);
|
||||
}
|
||||
// Play quest-complete sound
|
||||
if (auto* ac = owner_.services().audioCoordinator) {
|
||||
|
|
@ -476,25 +476,25 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
}
|
||||
questLog_.erase(it);
|
||||
LOG_INFO(" Removed quest ", questId, " from quest log");
|
||||
if (owner_.addonEventCallback_)
|
||||
owner_.addonEventCallback_("QUEST_TURNED_IN", {std::to_string(questId)});
|
||||
if (owner_.addonEventCallbackRef())
|
||||
owner_.addonEventCallbackRef()("QUEST_TURNED_IN", {std::to_string(questId)});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (owner_.addonEventCallback_) {
|
||||
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
}
|
||||
// Re-query all nearby quest giver NPCs so markers refresh
|
||||
if (owner_.socket) {
|
||||
if (owner_.getSocket()) {
|
||||
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
|
||||
if (entity->getType() != ObjectType::UNIT) continue;
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
if (unit->getNpcFlags() & 0x02) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(guid);
|
||||
owner_.socket->send(qsPkt);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -548,13 +548,13 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
progressMsg += std::to_string(count) + "/" + std::to_string(reqCount);
|
||||
owner_.addSystemChatMessage(progressMsg);
|
||||
|
||||
if (owner_.questProgressCallback_) {
|
||||
owner_.questProgressCallback_(quest.title, creatureName, count, reqCount);
|
||||
if (owner_.questProgressCallbackRef()) {
|
||||
owner_.questProgressCallbackRef()(quest.title, creatureName, count, reqCount);
|
||||
}
|
||||
if (owner_.addonEventCallback_) {
|
||||
owner_.addonEventCallback_("QUEST_WATCH_UPDATE", {std::to_string(questId)});
|
||||
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
owner_.addonEventCallbackRef()("QUEST_WATCH_UPDATE", {std::to_string(questId)});
|
||||
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
}
|
||||
|
||||
LOG_INFO("Updated kill count for quest ", questId, ": ",
|
||||
|
|
@ -614,7 +614,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
}
|
||||
owner_.addSystemChatMessage("Quest item: " + buildItemLink(itemId, questItemQuality, itemLabel) + " (" + std::to_string(count) + ")");
|
||||
|
||||
if (owner_.questProgressCallback_ && updatedAny) {
|
||||
if (owner_.questProgressCallbackRef() && updatedAny) {
|
||||
for (const auto& quest : questLog_) {
|
||||
if (quest.complete) continue;
|
||||
if (quest.itemCounts.count(itemId) == 0) continue;
|
||||
|
|
@ -627,15 +627,15 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
}
|
||||
}
|
||||
if (required == 0) required = count;
|
||||
owner_.questProgressCallback_(quest.title, itemLabel, count, required);
|
||||
owner_.questProgressCallbackRef()(quest.title, itemLabel, count, required);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (owner_.addonEventCallback_ && updatedAny) {
|
||||
owner_.addonEventCallback_("QUEST_WATCH_UPDATE", {});
|
||||
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
if (owner_.addonEventCallbackRef() && updatedAny) {
|
||||
owner_.addonEventCallbackRef()("QUEST_WATCH_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
}
|
||||
LOG_INFO("Quest item update: itemId=", itemId, " count=", count,
|
||||
" trackedQuestsUpdated=", updatedAny);
|
||||
|
|
@ -690,12 +690,12 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
// WotLK uses this opcode as SMSG_SET_REST_START
|
||||
if (!isClassicLikeExpansion() && !isActiveExpansion("tbc")) {
|
||||
bool nowResting = (value != 0);
|
||||
if (nowResting != owner_.isResting_) {
|
||||
owner_.isResting_ = nowResting;
|
||||
owner_.addSystemChatMessage(owner_.isResting_ ? "You are now resting."
|
||||
if (nowResting != owner_.isRestingRef()) {
|
||||
owner_.isRestingRef() = nowResting;
|
||||
owner_.addSystemChatMessage(owner_.isRestingRef() ? "You are now resting."
|
||||
: "You are no longer resting.");
|
||||
if (owner_.addonEventCallback_)
|
||||
owner_.addonEventCallback_("PLAYER_UPDATE_RESTING", {});
|
||||
if (owner_.addonEventCallbackRef())
|
||||
owner_.addonEventCallbackRef()("PLAYER_UPDATE_RESTING", {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -740,10 +740,10 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
} else {
|
||||
owner_.addSystemChatMessage("Quest removed (ID " + std::to_string(questId) + ").");
|
||||
}
|
||||
if (owner_.addonEventCallback_) {
|
||||
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
owner_.addonEventCallback_("QUEST_REMOVED", {std::to_string(questId)});
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
owner_.addonEventCallbackRef()("QUEST_REMOVED", {std::to_string(questId)});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -758,7 +758,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
uint32_t questId = packet.readUInt32();
|
||||
packet.readUInt32(); // questMethod
|
||||
|
||||
const bool isClassicLayout = owner_.packetParsers_ && owner_.packetParsers_->questLogStride() <= 4;
|
||||
const bool isClassicLayout = owner_.getPacketParsers() && owner_.getPacketParsers()->questLogStride() <= 4;
|
||||
const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout);
|
||||
const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout);
|
||||
const QuestQueryRewards rwds = tryParseQuestRewards(packet.getData(), isClassicLayout);
|
||||
|
|
@ -880,7 +880,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
uint32_t questId = packet.readUInt32();
|
||||
owner_.completedQuests_.insert(questId);
|
||||
owner_.completedQuestsRef().insert(questId);
|
||||
}
|
||||
LOG_DEBUG("SMSG_QUERY_QUESTS_COMPLETED_RESPONSE: ", count, " completed quests");
|
||||
}
|
||||
|
|
@ -894,13 +894,13 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
void QuestHandler::selectGossipOption(uint32_t optionId) {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !gossipWindowOpen_) return;
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !gossipWindowOpen_) return;
|
||||
LOG_INFO("selectGossipOption: optionId=", optionId,
|
||||
" npcGuid=0x", std::hex, currentGossip_.npcGuid, std::dec,
|
||||
" menuId=", currentGossip_.menuId,
|
||||
" numOptions=", currentGossip_.options.size());
|
||||
auto packet = GossipSelectOptionPacket::build(currentGossip_.npcGuid, currentGossip_.menuId, optionId);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
|
||||
for (const auto& opt : currentGossip_.options) {
|
||||
if (opt.id != optionId) continue;
|
||||
|
|
@ -919,21 +919,21 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
|
|||
|
||||
if (opt.icon == 6) {
|
||||
auto pkt = BankerActivatePacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
sentBanker = true;
|
||||
LOG_INFO("Sent CMSG_BANKER_ACTIVATE (icon) for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
||||
if (!sentAuction && (text == "GOSSIP_OPTION_AUCTIONEER" || textLower.find("auction") != std::string::npos)) {
|
||||
auto pkt = AuctionHelloPacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
sentAuction = true;
|
||||
LOG_INFO("Sent MSG_AUCTION_HELLO for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
||||
if (!sentBanker && (text == "GOSSIP_OPTION_BANKER" || textLower.find("deposit box") != std::string::npos)) {
|
||||
auto pkt = BankerActivatePacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
sentBanker = true;
|
||||
LOG_INFO("Sent CMSG_BANKER_ACTIVATE (text) for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
|
@ -947,14 +947,14 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
|
|||
owner_.setVendorCanRepair(true);
|
||||
}
|
||||
auto pkt = ListInventoryPacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
LOG_DEBUG("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
||||
if (textLower.find("make this inn your home") != std::string::npos ||
|
||||
textLower.find("set your home") != std::string::npos) {
|
||||
auto bindPkt = BinderActivatePacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(bindPkt);
|
||||
owner_.getSocket()->send(bindPkt);
|
||||
LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
||||
|
|
@ -962,10 +962,10 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
|
|||
if (text == "GOSSIP_OPTION_STABLE" ||
|
||||
textLower.find("stable") != std::string::npos ||
|
||||
textLower.find("my pet") != std::string::npos) {
|
||||
owner_.stableMasterGuid_ = currentGossip_.npcGuid;
|
||||
owner_.stableWindowOpen_ = false;
|
||||
owner_.stableMasterGuidRef() = currentGossip_.npcGuid;
|
||||
owner_.stableWindowOpenRef() = false;
|
||||
auto listPkt = ListStabledPetsPacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(listPkt);
|
||||
owner_.getSocket()->send(listPkt);
|
||||
LOG_INFO("Sent MSG_LIST_STABLED_PETS (gossip) to npc=0x",
|
||||
std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
|
@ -974,7 +974,7 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
|
|||
}
|
||||
|
||||
void QuestHandler::selectGossipQuest(uint32_t questId) {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !gossipWindowOpen_) return;
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !gossipWindowOpen_) return;
|
||||
|
||||
const QuestLogEntry* activeQuest = nullptr;
|
||||
for (const auto& q : questLog_) {
|
||||
|
|
@ -986,11 +986,11 @@ void QuestHandler::selectGossipQuest(uint32_t questId) {
|
|||
|
||||
// Validate against server-auth quest slot fields
|
||||
auto questInServerLogSlots = [&](uint32_t qid) -> bool {
|
||||
if (qid == 0 || owner_.lastPlayerFields_.empty()) return false;
|
||||
if (qid == 0 || owner_.lastPlayerFieldsRef().empty()) return false;
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
|
||||
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
|
||||
const uint16_t ufQuestEnd = ufQuestStart + 25 * qStride;
|
||||
for (const auto& [key, val] : owner_.lastPlayerFields_) {
|
||||
for (const auto& [key, val] : owner_.lastPlayerFieldsRef()) {
|
||||
if (key < ufQuestStart || key >= ufQuestEnd) continue;
|
||||
if ((key - ufQuestStart) % qStride != 0) continue;
|
||||
if (val == qid) return true;
|
||||
|
|
@ -1015,37 +1015,37 @@ void QuestHandler::selectGossipQuest(uint32_t questId) {
|
|||
pendingTurnInNpcGuid_ = currentGossip_.npcGuid;
|
||||
pendingTurnInRewardRequest_ = activeQuest ? activeQuest->complete : false;
|
||||
auto packet = QuestgiverCompleteQuestPacket::build(currentGossip_.npcGuid, questId);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
} else {
|
||||
pendingTurnInQuestId_ = 0;
|
||||
pendingTurnInNpcGuid_ = 0;
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
auto packet = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildQueryQuestPacket(currentGossip_.npcGuid, questId)
|
||||
auto packet = owner_.getPacketParsers()
|
||||
? owner_.getPacketParsers()->buildQueryQuestPacket(currentGossip_.npcGuid, questId)
|
||||
: QuestgiverQueryQuestPacket::build(currentGossip_.npcGuid, questId);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
}
|
||||
|
||||
gossipWindowOpen_ = false;
|
||||
}
|
||||
|
||||
bool QuestHandler::requestQuestQuery(uint32_t questId, bool force) {
|
||||
if (questId == 0 || owner_.state != WorldState::IN_WORLD || !owner_.socket) return false;
|
||||
if (questId == 0 || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return false;
|
||||
if (!force && pendingQuestQueryIds_.count(questId)) return false;
|
||||
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
|
||||
pkt.writeUInt32(questId);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
pendingQuestQueryIds_.insert(questId);
|
||||
|
||||
// WotLK supports CMSG_QUEST_POI_QUERY to get objective map locations.
|
||||
if (owner_.packetParsers_ && owner_.packetParsers_->questLogStride() == 5) {
|
||||
if (owner_.getPacketParsers() && owner_.getPacketParsers()->questLogStride() == 5) {
|
||||
const uint32_t wirePoiQuery = wireOpcode(Opcode::CMSG_QUEST_POI_QUERY);
|
||||
if (wirePoiQuery != 0xFFFF) {
|
||||
network::Packet poiPkt(static_cast<uint16_t>(wirePoiQuery));
|
||||
poiPkt.writeUInt32(1); // count = 1
|
||||
poiPkt.writeUInt32(questId);
|
||||
owner_.socket->send(poiPkt);
|
||||
owner_.getSocket()->send(poiPkt);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1060,7 +1060,7 @@ void QuestHandler::setQuestTracked(uint32_t questId, bool tracked) {
|
|||
}
|
||||
|
||||
void QuestHandler::acceptQuest() {
|
||||
if (!questDetailsOpen_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
if (!questDetailsOpen_ || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
const uint32_t questId = currentQuestDetails_.questId;
|
||||
if (questId == 0) return;
|
||||
uint64_t npcGuid = currentQuestDetails_.npcGuid;
|
||||
|
|
@ -1087,10 +1087,10 @@ void QuestHandler::acceptQuest() {
|
|||
std::erase_if(questLog_, [&](const QuestLogEntry& q) { return q.questId == questId; });
|
||||
}
|
||||
|
||||
network::Packet packet = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildAcceptQuestPacket(npcGuid, questId)
|
||||
network::Packet packet = owner_.getPacketParsers()
|
||||
? owner_.getPacketParsers()->buildAcceptQuestPacket(npcGuid, questId)
|
||||
: QuestgiverAcceptQuestPacket::build(npcGuid, questId);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
pendingQuestAcceptTimeouts_[questId] = 5.0f;
|
||||
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
|
||||
|
||||
|
|
@ -1108,7 +1108,7 @@ void QuestHandler::acceptQuest() {
|
|||
if (npcGuid) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
owner_.socket->send(qsPkt);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1120,12 +1120,12 @@ void QuestHandler::declineQuest() {
|
|||
|
||||
void QuestHandler::closeGossip() {
|
||||
gossipWindowOpen_ = false;
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_CLOSED", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_CLOSED", {});
|
||||
currentGossip_ = GossipMessageData{};
|
||||
}
|
||||
|
||||
void QuestHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
if (itemGuid == 0 || questId == 0) {
|
||||
owner_.addSystemChatMessage("Cannot start quest right now.");
|
||||
return;
|
||||
|
|
@ -1133,23 +1133,23 @@ void QuestHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) {
|
|||
// Send CMSG_QUESTGIVER_QUERY_QUEST with the item GUID as the "questgiver."
|
||||
// The server responds with SMSG_QUESTGIVER_QUEST_DETAILS which handleQuestDetails()
|
||||
// picks up and opens the Accept/Decline dialog.
|
||||
auto queryPkt = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildQueryQuestPacket(itemGuid, questId)
|
||||
auto queryPkt = owner_.getPacketParsers()
|
||||
? owner_.getPacketParsers()->buildQueryQuestPacket(itemGuid, questId)
|
||||
: QuestgiverQueryQuestPacket::build(itemGuid, questId);
|
||||
owner_.socket->send(queryPkt);
|
||||
owner_.getSocket()->send(queryPkt);
|
||||
LOG_INFO("offerQuestFromItem: itemGuid=0x", std::hex, itemGuid, std::dec,
|
||||
" questId=", questId);
|
||||
}
|
||||
|
||||
void QuestHandler::completeQuest() {
|
||||
if (!questRequestItemsOpen_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
if (!questRequestItemsOpen_ || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
pendingTurnInQuestId_ = currentQuestRequestItems_.questId;
|
||||
pendingTurnInNpcGuid_ = currentQuestRequestItems_.npcGuid;
|
||||
pendingTurnInRewardRequest_ = currentQuestRequestItems_.isCompletable();
|
||||
|
||||
auto packet = QuestgiverCompleteQuestPacket::build(
|
||||
currentQuestRequestItems_.npcGuid, currentQuestRequestItems_.questId);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
questRequestItemsOpen_ = false;
|
||||
currentQuestRequestItems_ = QuestRequestItemsData{};
|
||||
}
|
||||
|
|
@ -1161,13 +1161,13 @@ void QuestHandler::closeQuestRequestItems() {
|
|||
}
|
||||
|
||||
void QuestHandler::chooseQuestReward(uint32_t rewardIndex) {
|
||||
if (!questOfferRewardOpen_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
if (!questOfferRewardOpen_ || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
|
||||
uint64_t npcGuid = currentQuestOfferReward_.npcGuid;
|
||||
LOG_INFO("Completing quest: questId=", currentQuestOfferReward_.questId,
|
||||
" npcGuid=", npcGuid, " rewardIndex=", rewardIndex);
|
||||
auto packet = QuestgiverChooseRewardPacket::build(
|
||||
npcGuid, currentQuestOfferReward_.questId, rewardIndex);
|
||||
owner_.socket->send(packet);
|
||||
owner_.getSocket()->send(packet);
|
||||
pendingTurnInQuestId_ = 0;
|
||||
pendingTurnInNpcGuid_ = 0;
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
|
|
@ -1178,7 +1178,7 @@ void QuestHandler::chooseQuestReward(uint32_t rewardIndex) {
|
|||
if (npcGuid) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
owner_.socket->send(qsPkt);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1205,10 +1205,10 @@ void QuestHandler::abandonQuest(uint32_t questId) {
|
|||
}
|
||||
|
||||
if (slotIndex >= 0 && slotIndex < 25) {
|
||||
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
|
||||
if (owner_.getState() == WorldState::IN_WORLD && owner_.getSocket()) {
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
|
||||
pkt.writeUInt8(static_cast<uint8_t>(slotIndex));
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Abandon quest failed: no quest-log slot found for questId=", questId);
|
||||
|
|
@ -1216,10 +1216,10 @@ void QuestHandler::abandonQuest(uint32_t questId) {
|
|||
|
||||
if (localIndex >= 0) {
|
||||
questLog_.erase(questLog_.begin() + static_cast<ptrdiff_t>(localIndex));
|
||||
if (owner_.addonEventCallback_) {
|
||||
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
owner_.addonEventCallback_("QUEST_REMOVED", {std::to_string(questId)});
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
owner_.addonEventCallbackRef()("QUEST_REMOVED", {std::to_string(questId)});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1231,7 +1231,7 @@ void QuestHandler::abandonQuest(uint32_t questId) {
|
|||
}
|
||||
|
||||
void QuestHandler::shareQuestWithParty(uint32_t questId) {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) {
|
||||
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) {
|
||||
owner_.addSystemChatMessage("Cannot share quest: not in world.");
|
||||
return;
|
||||
}
|
||||
|
|
@ -1241,7 +1241,7 @@ void QuestHandler::shareQuestWithParty(uint32_t questId) {
|
|||
}
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_PUSHQUESTTOPARTY));
|
||||
pkt.writeUInt32(questId);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
// Local feedback: find quest title
|
||||
for (const auto& q : questLog_) {
|
||||
if (q.questId == questId && !q.title.empty()) {
|
||||
|
|
@ -1253,11 +1253,11 @@ void QuestHandler::shareQuestWithParty(uint32_t questId) {
|
|||
}
|
||||
|
||||
void QuestHandler::acceptSharedQuest() {
|
||||
if (!pendingSharedQuest_ || !owner_.socket) return;
|
||||
if (!pendingSharedQuest_ || !owner_.getSocket()) return;
|
||||
pendingSharedQuest_ = false;
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_CONFIRM_ACCEPT));
|
||||
pkt.writeUInt32(sharedQuestId_);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
owner_.addSystemChatMessage("Accepted: " + sharedQuestTitle_);
|
||||
}
|
||||
|
||||
|
|
@ -1278,13 +1278,13 @@ bool QuestHandler::hasQuestInLog(uint32_t questId) const {
|
|||
}
|
||||
|
||||
int QuestHandler::findQuestLogSlotIndexFromServer(uint32_t questId) const {
|
||||
if (questId == 0 || owner_.lastPlayerFields_.empty()) return -1;
|
||||
if (questId == 0 || owner_.lastPlayerFieldsRef().empty()) return -1;
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
|
||||
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
|
||||
for (uint16_t slot = 0; slot < 25; ++slot) {
|
||||
const uint16_t idField = ufQuestStart + slot * qStride;
|
||||
auto it = owner_.lastPlayerFields_.find(idField);
|
||||
if (it != owner_.lastPlayerFields_.end() && it->second == questId) {
|
||||
auto it = owner_.lastPlayerFieldsRef().find(idField);
|
||||
if (it != owner_.lastPlayerFieldsRef().end() && it->second == questId) {
|
||||
return static_cast<int>(slot);
|
||||
}
|
||||
}
|
||||
|
|
@ -1298,18 +1298,18 @@ void QuestHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::stri
|
|||
entry.title = title.empty() ? ("Quest #" + std::to_string(questId)) : title;
|
||||
entry.objectives = objectives;
|
||||
questLog_.push_back(std::move(entry));
|
||||
if (owner_.addonEventCallback_) {
|
||||
owner_.addonEventCallback_("QUEST_ACCEPTED", {std::to_string(questId)});
|
||||
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
if (owner_.addonEventCallbackRef()) {
|
||||
owner_.addonEventCallbackRef()("QUEST_ACCEPTED", {std::to_string(questId)});
|
||||
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
|
||||
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
|
||||
}
|
||||
}
|
||||
|
||||
bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
|
||||
if (owner_.lastPlayerFields_.empty()) return false;
|
||||
if (owner_.lastPlayerFieldsRef().empty()) return false;
|
||||
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
|
||||
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
|
||||
|
||||
static constexpr uint32_t kQuestStatusComplete = 1;
|
||||
|
||||
|
|
@ -1318,15 +1318,15 @@ bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
|
|||
for (uint16_t slot = 0; slot < 25; ++slot) {
|
||||
const uint16_t idField = ufQuestStart + slot * qStride;
|
||||
const uint16_t stateField = ufQuestStart + slot * qStride + 1;
|
||||
auto it = owner_.lastPlayerFields_.find(idField);
|
||||
if (it == owner_.lastPlayerFields_.end()) continue;
|
||||
auto it = owner_.lastPlayerFieldsRef().find(idField);
|
||||
if (it == owner_.lastPlayerFieldsRef().end()) continue;
|
||||
uint32_t questId = it->second;
|
||||
if (questId == 0) continue;
|
||||
|
||||
bool complete = false;
|
||||
if (qStride >= 2) {
|
||||
auto stateIt = owner_.lastPlayerFields_.find(stateField);
|
||||
if (stateIt != owner_.lastPlayerFields_.end()) {
|
||||
auto stateIt = owner_.lastPlayerFieldsRef().find(stateField);
|
||||
if (stateIt != owner_.lastPlayerFieldsRef().end()) {
|
||||
uint32_t state = stateIt->second & 0xFF;
|
||||
complete = (state == kQuestStatusComplete);
|
||||
}
|
||||
|
|
@ -1378,7 +1378,7 @@ void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
|
|||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
if (ufQuestStart == 0xFFFF || questLog_.empty()) return;
|
||||
|
||||
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
|
||||
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
|
||||
if (qStride < 2) return;
|
||||
|
||||
static constexpr uint32_t kQuestStatusComplete = 1;
|
||||
|
|
@ -1407,12 +1407,12 @@ void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
|
|||
}
|
||||
|
||||
void QuestHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) {
|
||||
if (owner_.lastPlayerFields_.empty()) return;
|
||||
if (owner_.lastPlayerFieldsRef().empty()) return;
|
||||
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
if (ufQuestStart == 0xFFFF) return;
|
||||
|
||||
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
|
||||
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
|
||||
if (qStride < 3) return;
|
||||
|
||||
int slot = findQuestLogSlotIndexFromServer(quest.questId);
|
||||
|
|
@ -1423,14 +1423,14 @@ void QuestHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) {
|
|||
? static_cast<uint16_t>(countField1 + 1)
|
||||
: static_cast<uint16_t>(0xFFFF);
|
||||
|
||||
auto f1It = owner_.lastPlayerFields_.find(countField1);
|
||||
if (f1It == owner_.lastPlayerFields_.end()) return;
|
||||
auto f1It = owner_.lastPlayerFieldsRef().find(countField1);
|
||||
if (f1It == owner_.lastPlayerFieldsRef().end()) return;
|
||||
const uint32_t packed1 = f1It->second;
|
||||
|
||||
uint32_t packed2 = 0;
|
||||
if (countField2 != 0xFFFF) {
|
||||
auto f2It = owner_.lastPlayerFields_.find(countField2);
|
||||
if (f2It != owner_.lastPlayerFields_.end()) packed2 = f2It->second;
|
||||
auto f2It = owner_.lastPlayerFieldsRef().find(countField2);
|
||||
if (f2It != owner_.lastPlayerFieldsRef().end()) packed2 = f2It->second;
|
||||
}
|
||||
|
||||
auto unpack6 = [](uint32_t word, int idx) -> uint8_t {
|
||||
|
|
@ -1474,7 +1474,7 @@ void QuestHandler::clearPendingQuestAccept(uint32_t questId) {
|
|||
}
|
||||
|
||||
void QuestHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason) {
|
||||
if (questId == 0 || !owner_.socket || owner_.state != WorldState::IN_WORLD) return;
|
||||
if (questId == 0 || !owner_.getSocket() || owner_.getState() != WorldState::IN_WORLD) return;
|
||||
|
||||
LOG_INFO("Quest accept resync: questId=", questId, " reason=", reason ? reason : "unknown");
|
||||
requestQuestQuery(questId, true);
|
||||
|
|
@ -1482,12 +1482,12 @@ void QuestHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid,
|
|||
if (npcGuid != 0) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
owner_.socket->send(qsPkt);
|
||||
owner_.getSocket()->send(qsPkt);
|
||||
|
||||
auto queryPkt = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildQueryQuestPacket(npcGuid, questId)
|
||||
auto queryPkt = owner_.getPacketParsers()
|
||||
? owner_.getPacketParsers()->buildQueryQuestPacket(npcGuid, questId)
|
||||
: QuestgiverQueryQuestPacket::build(npcGuid, questId);
|
||||
owner_.socket->send(queryPkt);
|
||||
owner_.getSocket()->send(queryPkt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1496,23 +1496,23 @@ void QuestHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid,
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
void QuestHandler::handleGossipMessage(network::Packet& packet) {
|
||||
bool ok = owner_.packetParsers_ ? owner_.packetParsers_->parseGossipMessage(packet, currentGossip_)
|
||||
bool ok = owner_.getPacketParsers() ? owner_.getPacketParsers()->parseGossipMessage(packet, currentGossip_)
|
||||
: GossipMessageParser::parse(packet, currentGossip_);
|
||||
if (!ok) return;
|
||||
if (questDetailsOpen_) return; // Don't reopen gossip while viewing quest
|
||||
gossipWindowOpen_ = true;
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_SHOW", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_SHOW", {});
|
||||
owner_.closeVendor(); // Close vendor if gossip opens
|
||||
|
||||
// Classify gossip quests and update quest log + overhead NPC markers.
|
||||
classifyGossipQuests(true);
|
||||
|
||||
// Play NPC greeting voice
|
||||
if (owner_.npcGreetingCallback_ && currentGossip_.npcGuid != 0) {
|
||||
if (owner_.npcGreetingCallbackRef() && currentGossip_.npcGuid != 0) {
|
||||
auto entity = owner_.getEntityManager().getEntity(currentGossip_.npcGuid);
|
||||
if (entity) {
|
||||
glm::vec3 npcPos(entity->getX(), entity->getY(), entity->getZ());
|
||||
owner_.npcGreetingCallback_(currentGossip_.npcGuid, npcPos);
|
||||
owner_.npcGreetingCallbackRef()(currentGossip_.npcGuid, npcPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1563,7 +1563,7 @@ void QuestHandler::handleQuestgiverQuestList(network::Packet& packet) {
|
|||
|
||||
currentGossip_ = std::move(data);
|
||||
gossipWindowOpen_ = true;
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_SHOW", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_SHOW", {});
|
||||
owner_.closeVendor();
|
||||
|
||||
classifyGossipQuests(false);
|
||||
|
|
@ -1617,16 +1617,16 @@ void QuestHandler::handleGossipComplete(network::Packet& packet) {
|
|||
(void)packet;
|
||||
|
||||
// Play farewell sound before closing
|
||||
if (owner_.npcFarewellCallback_ && currentGossip_.npcGuid != 0) {
|
||||
if (owner_.npcFarewellCallbackRef() && currentGossip_.npcGuid != 0) {
|
||||
auto entity = owner_.getEntityManager().getEntity(currentGossip_.npcGuid);
|
||||
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
|
||||
owner_.npcFarewellCallback_(currentGossip_.npcGuid, pos);
|
||||
owner_.npcFarewellCallbackRef()(currentGossip_.npcGuid, pos);
|
||||
}
|
||||
}
|
||||
|
||||
gossipWindowOpen_ = false;
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_CLOSED", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_CLOSED", {});
|
||||
currentGossip_ = GossipMessageData{};
|
||||
}
|
||||
|
||||
|
|
@ -1687,7 +1687,7 @@ void QuestHandler::handleQuestPoiQueryResponse(network::Packet& packet) {
|
|||
sumY += static_cast<float>(py);
|
||||
}
|
||||
// Skip POIs for maps other than the player's current map.
|
||||
if (mapId != owner_.currentMapId_) continue;
|
||||
if (mapId != owner_.currentMapIdRef()) continue;
|
||||
GossipPoi poi;
|
||||
poi.x = sumX / static_cast<float>(pointCount);
|
||||
poi.y = sumY / static_cast<float>(pointCount);
|
||||
|
|
@ -1704,7 +1704,7 @@ void QuestHandler::handleQuestPoiQueryResponse(network::Packet& packet) {
|
|||
|
||||
void QuestHandler::handleQuestDetails(network::Packet& packet) {
|
||||
QuestDetailsData data;
|
||||
bool ok = owner_.packetParsers_ ? owner_.packetParsers_->parseQuestDetails(packet, data)
|
||||
bool ok = owner_.getPacketParsers() ? owner_.getPacketParsers()->parseQuestDetails(packet, data)
|
||||
: QuestDetailsParser::parse(packet, data);
|
||||
if (!ok) {
|
||||
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_QUEST_DETAILS");
|
||||
|
|
@ -1727,7 +1727,7 @@ void QuestHandler::handleQuestDetails(network::Packet& packet) {
|
|||
// Delay opening the window slightly to allow item queries to complete
|
||||
questDetailsOpenTime_ = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
|
||||
gossipWindowOpen_ = false;
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("QUEST_DETAIL", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("QUEST_DETAIL", {});
|
||||
}
|
||||
|
||||
void QuestHandler::handleQuestRequestItems(network::Packet& packet) {
|
||||
|
|
@ -1742,9 +1742,9 @@ void QuestHandler::handleQuestRequestItems(network::Packet& packet) {
|
|||
data.questId == pendingTurnInQuestId_ &&
|
||||
data.npcGuid == pendingTurnInNpcGuid_ &&
|
||||
data.isCompletable() &&
|
||||
owner_.socket) {
|
||||
owner_.getSocket()) {
|
||||
auto rewardReq = QuestgiverRequestRewardPacket::build(data.npcGuid, data.questId);
|
||||
owner_.socket->send(rewardReq);
|
||||
owner_.getSocket()->send(rewardReq);
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
}
|
||||
|
||||
|
|
@ -1809,7 +1809,7 @@ void QuestHandler::handleQuestOfferReward(network::Packet& packet) {
|
|||
gossipWindowOpen_ = false;
|
||||
questDetailsOpen_ = false;
|
||||
questDetailsOpenTime_ = std::chrono::steady_clock::time_point{};
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("QUEST_COMPLETE", {});
|
||||
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("QUEST_COMPLETE", {});
|
||||
|
||||
// Query item names for reward items
|
||||
for (const auto& item : data.choiceRewards)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -193,8 +193,8 @@ void WardenHandler::update(float deltaTime) {
|
|||
for (uint8_t byte : encrypted) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
if (owner_.socket && owner_.socket->isConnected()) {
|
||||
owner_.socket->send(response);
|
||||
if (owner_.getSocket() && owner_.getSocket()->isConnected()) {
|
||||
owner_.getSocket()->send(response);
|
||||
LOG_WARNING("Warden: Sent async CHEAT_CHECKS_RESULT (", plaintext.size(), " bytes plaintext)");
|
||||
}
|
||||
}
|
||||
|
|
@ -202,11 +202,11 @@ void WardenHandler::update(float deltaTime) {
|
|||
}
|
||||
|
||||
// Post-gate visibility
|
||||
if (wardenGateSeen_ && owner_.socket && owner_.socket->isConnected()) {
|
||||
if (wardenGateSeen_ && owner_.getSocket() && owner_.getSocket()->isConnected()) {
|
||||
wardenGateElapsed_ += deltaTime;
|
||||
if (wardenGateElapsed_ >= wardenGateNextStatusLog_) {
|
||||
LOG_DEBUG("Warden gate status: elapsed=", wardenGateElapsed_,
|
||||
"s connected=", owner_.socket->isConnected() ? "yes" : "no",
|
||||
"s connected=", owner_.getSocket()->isConnected() ? "yes" : "no",
|
||||
" packetsAfterGate=", wardenPacketsAfterGate_);
|
||||
wardenGateNextStatusLog_ += 30.0f;
|
||||
}
|
||||
|
|
@ -302,12 +302,12 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
|
|||
// Initialize Warden crypto from session key on first packet
|
||||
if (!wardenCrypto_) {
|
||||
wardenCrypto_ = std::make_unique<WardenCrypto>();
|
||||
if (owner_.sessionKey.size() != 40) {
|
||||
LOG_ERROR("Warden: No valid session key (size=", owner_.sessionKey.size(), "), cannot init crypto");
|
||||
if (owner_.getSessionKey().size() != 40) {
|
||||
LOG_ERROR("Warden: No valid session key (size=", owner_.getSessionKey().size(), "), cannot init crypto");
|
||||
wardenCrypto_.reset();
|
||||
return;
|
||||
}
|
||||
if (!wardenCrypto_->initFromSessionKey(owner_.sessionKey)) {
|
||||
if (!wardenCrypto_->initFromSessionKey(owner_.getSessionKey())) {
|
||||
LOG_ERROR("Warden: Failed to initialize crypto from session key");
|
||||
wardenCrypto_.reset();
|
||||
return;
|
||||
|
|
@ -348,8 +348,8 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
|
|||
for (uint8_t byte : encrypted) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
if (owner_.socket && owner_.socket->isConnected()) {
|
||||
owner_.socket->send(response);
|
||||
if (owner_.getSocket() && owner_.getSocket()->isConnected()) {
|
||||
owner_.getSocket()->send(response);
|
||||
LOG_DEBUG("Warden: Sent response (", plaintext.size(), " bytes plaintext)");
|
||||
}
|
||||
};
|
||||
|
|
@ -447,12 +447,12 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
|
|||
wardenLoadedModule_->setCallbackDependencies(
|
||||
wardenCrypto_.get(),
|
||||
[this](const uint8_t* data, size_t len) {
|
||||
if (!wardenCrypto_ || !owner_.socket) return;
|
||||
if (!wardenCrypto_ || !owner_.getSocket()) return;
|
||||
std::vector<uint8_t> plaintext(data, data + len);
|
||||
auto encrypted = wardenCrypto_->encrypt(plaintext);
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_WARDEN_DATA));
|
||||
for (uint8_t b : encrypted) pkt.writeUInt8(b);
|
||||
owner_.socket->send(pkt);
|
||||
owner_.getSocket()->send(pkt);
|
||||
LOG_DEBUG("Warden: Module sendPacket callback sent ", len, " bytes");
|
||||
});
|
||||
if (wardenLoadedModule_->load(wardenModuleData_, wardenModuleHash_, wardenModuleKey_)) { // codeql[cpp/weak-cryptographic-algorithm]
|
||||
|
|
@ -533,7 +533,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
|
|||
for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; }
|
||||
|
||||
bool isTurtle = isActiveExpansion("turtle");
|
||||
bool isClassic = (owner_.build <= 6005) && !isTurtle;
|
||||
bool isClassic = (owner_.getBuild() <= 6005) && !isTurtle;
|
||||
|
||||
if (!isTurtle && !isClassic) {
|
||||
// WotLK/TBC: don't respond to HASH_REQUEST without a valid CR match.
|
||||
|
|
@ -619,7 +619,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
|
|||
// Ensure wardenMemory_ is loaded on main thread before launching async task
|
||||
if (!wardenMemory_) {
|
||||
wardenMemory_ = std::make_unique<WardenMemory>();
|
||||
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.build), isActiveExpansion("turtle"))) {
|
||||
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.getBuild()), isActiveExpansion("turtle"))) {
|
||||
LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK");
|
||||
}
|
||||
}
|
||||
|
|
@ -1054,7 +1054,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
|
|||
// Lazy-load WoW.exe PE image on first MEM_CHECK
|
||||
if (!wardenMemory_) {
|
||||
wardenMemory_ = std::make_unique<WardenMemory>();
|
||||
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.build), isActiveExpansion("turtle"))) {
|
||||
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.getBuild()), isActiveExpansion("turtle"))) {
|
||||
LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
666
src/game/world_packets_economy.cpp
Normal file
666
src/game/world_packets_economy.cpp
Normal file
|
|
@ -0,0 +1,666 @@
|
|||
#include "game/world_packets.hpp"
|
||||
#include "game/packet_parsers.hpp"
|
||||
#include "game/opcodes.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <zlib.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data) {
|
||||
// Minimum: windowInfo(4) + npcGuid(8) + nearestNode(4) + at least 1 mask uint32(4)
|
||||
size_t remaining = packet.getRemainingSize();
|
||||
if (remaining < 4 + 8 + 4 + 4) {
|
||||
LOG_ERROR("ShowTaxiNodesParser: packet too short (", remaining, " bytes)");
|
||||
return false;
|
||||
}
|
||||
data.windowInfo = packet.readUInt32();
|
||||
data.npcGuid = packet.readUInt64();
|
||||
data.nearestNode = packet.readUInt32();
|
||||
// Read as many mask uint32s as available (Classic/Vanilla=4, WotLK=12)
|
||||
size_t maskBytes = packet.getRemainingSize();
|
||||
uint32_t maskCount = static_cast<uint32_t>(maskBytes / 4);
|
||||
if (maskCount > TLK_TAXI_MASK_SIZE) maskCount = TLK_TAXI_MASK_SIZE;
|
||||
for (uint32_t i = 0; i < maskCount; ++i) {
|
||||
data.nodeMask[i] = packet.readUInt32();
|
||||
}
|
||||
LOG_INFO("ShowTaxiNodes: window=", data.windowInfo, " npc=0x", std::hex, data.npcGuid, std::dec,
|
||||
" nearest=", data.nearestNode, " maskSlots=", maskCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyData& data) {
|
||||
size_t remaining = packet.getRemainingSize();
|
||||
if (remaining >= 4) {
|
||||
data.result = packet.readUInt32();
|
||||
} else if (remaining >= 1) {
|
||||
data.result = packet.readUInt8();
|
||||
} else {
|
||||
LOG_ERROR("ActivateTaxiReplyParser: packet too short");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("ActivateTaxiReply: result=", data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXIEXPRESS));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(totalCost);
|
||||
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
|
||||
for (uint32_t nodeId : pathNodes) {
|
||||
packet.writeUInt32(nodeId);
|
||||
}
|
||||
LOG_INFO("ActivateTaxiExpress: npc=0x", std::hex, npcGuid, std::dec,
|
||||
" cost=", totalCost, " nodes=", pathNodes.size());
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXI));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(srcNode);
|
||||
packet.writeUInt32(destNode);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GameObjectUsePacket::build(uint64_t guid) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJ_USE));
|
||||
packet.writeUInt64(guid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mail System
|
||||
// ============================================================
|
||||
|
||||
network::Packet GetMailListPacket::build(uint64_t mailboxGuid) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GET_MAIL_LIST));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint64_t money, uint64_t cod,
|
||||
const std::vector<uint64_t>& itemGuids) {
|
||||
// WotLK 3.3.5a format
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeString(recipient);
|
||||
packet.writeString(subject);
|
||||
packet.writeString(body);
|
||||
packet.writeUInt32(0); // stationery
|
||||
packet.writeUInt32(0); // unknown
|
||||
uint8_t attachCount = static_cast<uint8_t>(itemGuids.size());
|
||||
packet.writeUInt8(attachCount);
|
||||
for (uint8_t i = 0; i < attachCount; ++i) {
|
||||
packet.writeUInt8(i); // attachment slot index
|
||||
packet.writeUInt64(itemGuids[i]);
|
||||
}
|
||||
packet.writeUInt64(money);
|
||||
packet.writeUInt64(cod);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_MONEY));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
// WotLK expects attachment item GUID low, not attachment slot index.
|
||||
packet.writeUInt32(itemGuidLow);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailDeletePacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
packet.writeUInt32(mailTemplateId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_MARK_AS_READ));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PacketParsers::parseMailList — WotLK 3.3.5a format (base/default)
|
||||
// ============================================================================
|
||||
bool PacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) {
|
||||
size_t remaining = packet.getRemainingSize();
|
||||
if (remaining < 5) return false;
|
||||
|
||||
uint32_t totalCount = packet.readUInt32();
|
||||
uint8_t shownCount = packet.readUInt8();
|
||||
(void)totalCount;
|
||||
|
||||
LOG_INFO("SMSG_MAIL_LIST_RESULT (WotLK): total=", totalCount, " shown=", static_cast<int>(shownCount));
|
||||
|
||||
inbox.clear();
|
||||
inbox.reserve(shownCount);
|
||||
|
||||
for (uint8_t i = 0; i < shownCount; ++i) {
|
||||
remaining = packet.getRemainingSize();
|
||||
if (remaining < 2) break;
|
||||
|
||||
uint16_t msgSize = packet.readUInt16();
|
||||
size_t startPos = packet.getReadPos();
|
||||
|
||||
MailMessage msg;
|
||||
if (remaining < static_cast<size_t>(msgSize) + 2) {
|
||||
LOG_WARNING("Mail entry ", i, " truncated");
|
||||
break;
|
||||
}
|
||||
|
||||
msg.messageId = packet.readUInt32();
|
||||
msg.messageType = packet.readUInt8();
|
||||
|
||||
switch (msg.messageType) {
|
||||
case 0: msg.senderGuid = packet.readUInt64(); break;
|
||||
case 2: case 3: case 4: case 5:
|
||||
msg.senderEntry = packet.readUInt32(); break;
|
||||
default: msg.senderEntry = packet.readUInt32(); break;
|
||||
}
|
||||
|
||||
msg.cod = packet.readUInt64();
|
||||
packet.readUInt32(); // item text id
|
||||
packet.readUInt32(); // unknown
|
||||
msg.stationeryId = packet.readUInt32();
|
||||
msg.money = packet.readUInt64();
|
||||
msg.flags = packet.readUInt32();
|
||||
msg.expirationTime = packet.readFloat();
|
||||
msg.mailTemplateId = packet.readUInt32();
|
||||
msg.subject = packet.readString();
|
||||
// WotLK 3.3.5a always includes body text in SMSG_MAIL_LIST_RESULT.
|
||||
// mailTemplateId != 0 still carries a (possibly empty) body string.
|
||||
msg.body = packet.readString();
|
||||
|
||||
uint8_t attachCount = packet.readUInt8();
|
||||
msg.attachments.reserve(attachCount);
|
||||
for (uint8_t j = 0; j < attachCount; ++j) {
|
||||
MailAttachment att;
|
||||
att.slot = packet.readUInt8();
|
||||
att.itemGuidLow = packet.readUInt32();
|
||||
att.itemId = packet.readUInt32();
|
||||
for (int e = 0; e < 7; ++e) {
|
||||
uint32_t enchId = packet.readUInt32();
|
||||
packet.readUInt32(); // duration
|
||||
packet.readUInt32(); // charges
|
||||
if (e == 0) att.enchantId = enchId;
|
||||
}
|
||||
att.randomPropertyId = packet.readUInt32();
|
||||
att.randomSuffix = packet.readUInt32();
|
||||
att.stackCount = packet.readUInt32();
|
||||
att.chargesOrDurability = packet.readUInt32();
|
||||
att.maxDurability = packet.readUInt32();
|
||||
packet.readUInt32(); // durability/current durability
|
||||
packet.readUInt8(); // unknown WotLK trailing byte per attachment
|
||||
msg.attachments.push_back(att);
|
||||
}
|
||||
|
||||
msg.read = (msg.flags & 0x01) != 0;
|
||||
inbox.push_back(std::move(msg));
|
||||
|
||||
// Skip unread bytes
|
||||
size_t consumed = packet.getReadPos() - startPos;
|
||||
if (consumed < msgSize) {
|
||||
size_t skip = msgSize - consumed;
|
||||
for (size_t s = 0; s < skip && packet.hasData(); ++s)
|
||||
packet.readUInt8();
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Parsed ", inbox.size(), " mail messages");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Bank System
|
||||
// ============================================================
|
||||
|
||||
network::Packet BankerActivatePacket::build(uint64_t guid) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_BANKER_ACTIVATE));
|
||||
p.writeUInt64(guid);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet BuyBankSlotPacket::build(uint64_t guid) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_BUY_BANK_SLOT));
|
||||
p.writeUInt64(guid);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AutoBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUTOBANK_ITEM));
|
||||
p.writeUInt8(srcBag);
|
||||
p.writeUInt8(srcSlot);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AutoStoreBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUTOSTORE_BANK_ITEM));
|
||||
p.writeUInt8(srcBag);
|
||||
p.writeUInt8(srcSlot);
|
||||
return p;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Guild Bank System
|
||||
// ============================================================
|
||||
|
||||
network::Packet GuildBankerActivatePacket::build(uint64_t guid) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANKER_ACTIVATE));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt8(0); // full slots update
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet GuildBankQueryTabPacket::build(uint64_t guid, uint8_t tabId, bool fullUpdate) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_QUERY_TAB));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt8(tabId);
|
||||
p.writeUInt8(fullUpdate ? 1 : 0);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet GuildBankBuyTabPacket::build(uint64_t guid, uint8_t tabId) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_BUY_TAB));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt8(tabId);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet GuildBankDepositMoneyPacket::build(uint64_t guid, uint32_t amount) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_DEPOSIT_MONEY));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt32(amount);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet GuildBankWithdrawMoneyPacket::build(uint64_t guid, uint32_t amount) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_WITHDRAW_MONEY));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt32(amount);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet GuildBankSwapItemsPacket::buildBankToInventory(
|
||||
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
|
||||
uint8_t destBag, uint8_t destSlot, uint32_t splitCount)
|
||||
{
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt8(0); // bankToCharacter = false -> bank source
|
||||
p.writeUInt8(tabId);
|
||||
p.writeUInt8(bankSlot);
|
||||
p.writeUInt32(0); // itemEntry (unused client side)
|
||||
p.writeUInt8(0); // autoStore = false
|
||||
if (splitCount > 0) {
|
||||
p.writeUInt8(splitCount);
|
||||
}
|
||||
p.writeUInt8(destBag);
|
||||
p.writeUInt8(destSlot);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet GuildBankSwapItemsPacket::buildInventoryToBank(
|
||||
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
|
||||
uint8_t srcBag, uint8_t srcSlot, uint32_t splitCount)
|
||||
{
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt8(1); // bankToCharacter = true -> char to bank
|
||||
p.writeUInt8(tabId);
|
||||
p.writeUInt8(bankSlot);
|
||||
p.writeUInt32(0); // itemEntry
|
||||
p.writeUInt8(0); // autoStore
|
||||
if (splitCount > 0) {
|
||||
p.writeUInt8(splitCount);
|
||||
}
|
||||
p.writeUInt8(srcBag);
|
||||
p.writeUInt8(srcSlot);
|
||||
return p;
|
||||
}
|
||||
|
||||
bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) {
|
||||
if (!packet.hasRemaining(14)) return false;
|
||||
|
||||
data.money = packet.readUInt64();
|
||||
data.tabId = packet.readUInt8();
|
||||
data.withdrawAmount = static_cast<int32_t>(packet.readUInt32());
|
||||
uint8_t fullUpdate = packet.readUInt8();
|
||||
|
||||
if (fullUpdate) {
|
||||
if (!packet.hasRemaining(1)) {
|
||||
LOG_WARNING("GuildBankListParser: truncated before tabCount");
|
||||
data.tabs.clear();
|
||||
} else {
|
||||
uint8_t tabCount = packet.readUInt8();
|
||||
// Cap at 8 (normal guild bank tab limit in WoW)
|
||||
if (tabCount > 8) {
|
||||
LOG_WARNING("GuildBankListParser: tabCount capped (requested=", static_cast<int>(tabCount), ")");
|
||||
tabCount = 8;
|
||||
}
|
||||
data.tabs.resize(tabCount);
|
||||
for (uint8_t i = 0; i < tabCount; ++i) {
|
||||
// Validate before reading strings
|
||||
if (!packet.hasData()) {
|
||||
LOG_WARNING("GuildBankListParser: truncated tab at index ", static_cast<int>(i));
|
||||
break;
|
||||
}
|
||||
data.tabs[i].tabName = packet.readString();
|
||||
if (!packet.hasData()) {
|
||||
data.tabs[i].tabIcon.clear();
|
||||
} else {
|
||||
data.tabs[i].tabIcon = packet.readString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!packet.hasRemaining(1)) {
|
||||
LOG_WARNING("GuildBankListParser: truncated before numSlots");
|
||||
data.tabItems.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t numSlots = packet.readUInt8();
|
||||
data.tabItems.clear();
|
||||
for (uint8_t i = 0; i < numSlots; ++i) {
|
||||
// Validate minimum bytes before reading slot (slotId(1) + itemEntry(4) = 5)
|
||||
if (!packet.hasRemaining(5)) {
|
||||
LOG_WARNING("GuildBankListParser: truncated slot at index ", static_cast<int>(i));
|
||||
break;
|
||||
}
|
||||
GuildBankItemSlot slot;
|
||||
slot.slotId = packet.readUInt8();
|
||||
slot.itemEntry = packet.readUInt32();
|
||||
if (slot.itemEntry != 0) {
|
||||
// Validate before reading enchant mask
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
// Enchant info
|
||||
uint32_t enchantMask = packet.readUInt32();
|
||||
for (int bit = 0; bit < 10; ++bit) {
|
||||
if (enchantMask & (1u << bit)) {
|
||||
if (!packet.hasRemaining(12)) {
|
||||
LOG_WARNING("GuildBankListParser: truncated enchant data");
|
||||
break;
|
||||
}
|
||||
uint32_t enchId = packet.readUInt32();
|
||||
uint32_t enchDur = packet.readUInt32();
|
||||
uint32_t enchCharges = packet.readUInt32();
|
||||
if (bit == 0) slot.enchantId = enchId;
|
||||
(void)enchDur; (void)enchCharges;
|
||||
}
|
||||
}
|
||||
// Validate before reading remaining item fields
|
||||
if (!packet.hasRemaining(12)) {
|
||||
LOG_WARNING("GuildBankListParser: truncated item fields");
|
||||
break;
|
||||
}
|
||||
slot.stackCount = packet.readUInt32();
|
||||
/*spare=*/ packet.readUInt32();
|
||||
slot.randomPropertyId = packet.readUInt32();
|
||||
if (slot.randomPropertyId) {
|
||||
if (!packet.hasRemaining(4)) {
|
||||
LOG_WARNING("GuildBankListParser: truncated suffix factor");
|
||||
break;
|
||||
}
|
||||
/*suffixFactor=*/ packet.readUInt32();
|
||||
}
|
||||
}
|
||||
data.tabItems.push_back(slot);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Auction House System
|
||||
// ============================================================
|
||||
|
||||
network::Packet AuctionHelloPacket::build(uint64_t guid) {
|
||||
network::Packet p(wireOpcode(Opcode::MSG_AUCTION_HELLO));
|
||||
p.writeUInt64(guid);
|
||||
return p;
|
||||
}
|
||||
|
||||
bool AuctionHelloParser::parse(network::Packet& packet, AuctionHelloData& data) {
|
||||
size_t remaining = packet.getRemainingSize();
|
||||
if (remaining < 12) {
|
||||
LOG_WARNING("AuctionHelloParser: too small, remaining=", remaining);
|
||||
return false;
|
||||
}
|
||||
data.auctioneerGuid = packet.readUInt64();
|
||||
data.auctionHouseId = packet.readUInt32();
|
||||
// WotLK has an extra uint8 enabled field; Vanilla does not
|
||||
if (packet.hasData()) {
|
||||
data.enabled = packet.readUInt8();
|
||||
} else {
|
||||
data.enabled = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet AuctionListItemsPacket::build(
|
||||
uint64_t guid, uint32_t offset,
|
||||
const std::string& searchName,
|
||||
uint8_t levelMin, uint8_t levelMax,
|
||||
uint32_t invTypeMask, uint32_t itemClass,
|
||||
uint32_t itemSubClass, uint32_t quality,
|
||||
uint8_t usableOnly, uint8_t exactMatch)
|
||||
{
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_ITEMS));
|
||||
p.writeUInt64(guid);
|
||||
p.writeUInt32(offset);
|
||||
p.writeString(searchName);
|
||||
p.writeUInt8(levelMin);
|
||||
p.writeUInt8(levelMax);
|
||||
p.writeUInt32(invTypeMask);
|
||||
p.writeUInt32(itemClass);
|
||||
p.writeUInt32(itemSubClass);
|
||||
p.writeUInt32(quality);
|
||||
p.writeUInt8(usableOnly);
|
||||
p.writeUInt8(0); // getAll (0 = normal search)
|
||||
p.writeUInt8(exactMatch);
|
||||
// Sort columns (0 = none)
|
||||
p.writeUInt8(0);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AuctionSellItemPacket::build(
|
||||
uint64_t auctioneerGuid, uint64_t itemGuid,
|
||||
uint32_t stackCount, uint32_t bid,
|
||||
uint32_t buyout, uint32_t duration,
|
||||
bool preWotlk)
|
||||
{
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_SELL_ITEM));
|
||||
p.writeUInt64(auctioneerGuid);
|
||||
if (!preWotlk) {
|
||||
// WotLK: itemCount(4) + per-item [guid(8) + stackCount(4)]
|
||||
p.writeUInt32(1);
|
||||
p.writeUInt64(itemGuid);
|
||||
p.writeUInt32(stackCount);
|
||||
} else {
|
||||
// Classic/TBC: just itemGuid, no count fields
|
||||
p.writeUInt64(itemGuid);
|
||||
}
|
||||
p.writeUInt32(bid);
|
||||
p.writeUInt32(buyout);
|
||||
p.writeUInt32(duration);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AuctionPlaceBidPacket::build(uint64_t auctioneerGuid, uint32_t auctionId, uint32_t amount) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_PLACE_BID));
|
||||
p.writeUInt64(auctioneerGuid);
|
||||
p.writeUInt32(auctionId);
|
||||
p.writeUInt32(amount);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AuctionRemoveItemPacket::build(uint64_t auctioneerGuid, uint32_t auctionId) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_REMOVE_ITEM));
|
||||
p.writeUInt64(auctioneerGuid);
|
||||
p.writeUInt32(auctionId);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AuctionListOwnerItemsPacket::build(uint64_t auctioneerGuid, uint32_t offset) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_OWNER_ITEMS));
|
||||
p.writeUInt64(auctioneerGuid);
|
||||
p.writeUInt32(offset);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet AuctionListBidderItemsPacket::build(
|
||||
uint64_t auctioneerGuid, uint32_t offset,
|
||||
const std::vector<uint32_t>& outbiddedIds)
|
||||
{
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_BIDDER_ITEMS));
|
||||
p.writeUInt64(auctioneerGuid);
|
||||
p.writeUInt32(offset);
|
||||
p.writeUInt32(static_cast<uint32_t>(outbiddedIds.size()));
|
||||
for (uint32_t id : outbiddedIds)
|
||||
p.writeUInt32(id);
|
||||
return p;
|
||||
}
|
||||
|
||||
bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data, int numEnchantSlots) {
|
||||
// Per-entry fixed size: auctionId(4) + itemEntry(4) + enchantSlots×3×4 +
|
||||
// randProp(4) + suffix(4) + stack(4) + charges(4) + flags(4) +
|
||||
// ownerGuid(8) + startBid(4) + outbid(4) + buyout(4) + expire(4) +
|
||||
// bidderGuid(8) + curBid(4)
|
||||
// Classic: numEnchantSlots=1 → 80 bytes/entry
|
||||
// TBC/WotLK: numEnchantSlots=3 → 104 bytes/entry
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
|
||||
uint32_t count = packet.readUInt32();
|
||||
// Cap auction count to prevent unbounded memory allocation
|
||||
const uint32_t MAX_AUCTION_RESULTS = 256;
|
||||
if (count > MAX_AUCTION_RESULTS) {
|
||||
LOG_WARNING("AuctionListResultParser: count capped (requested=", count, ")");
|
||||
count = MAX_AUCTION_RESULTS;
|
||||
}
|
||||
|
||||
data.auctions.clear();
|
||||
data.auctions.reserve(count);
|
||||
|
||||
const size_t minPerEntry = static_cast<size_t>(8 + numEnchantSlots * 12 + 28 + 8 + 8);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (!packet.hasRemaining(minPerEntry)) break;
|
||||
AuctionEntry e;
|
||||
e.auctionId = packet.readUInt32();
|
||||
e.itemEntry = packet.readUInt32();
|
||||
// First enchant slot always present
|
||||
e.enchantId = packet.readUInt32();
|
||||
packet.readUInt32(); // enchant1 duration
|
||||
packet.readUInt32(); // enchant1 charges
|
||||
// Extra enchant slots for TBC/WotLK
|
||||
for (int s = 1; s < numEnchantSlots; ++s) {
|
||||
packet.readUInt32(); // enchant N id
|
||||
packet.readUInt32(); // enchant N duration
|
||||
packet.readUInt32(); // enchant N charges
|
||||
}
|
||||
e.randomPropertyId = packet.readUInt32();
|
||||
e.suffixFactor = packet.readUInt32();
|
||||
e.stackCount = packet.readUInt32();
|
||||
packet.readUInt32(); // item charges
|
||||
packet.readUInt32(); // item flags (unused)
|
||||
e.ownerGuid = packet.readUInt64();
|
||||
e.startBid = packet.readUInt32();
|
||||
e.minBidIncrement = packet.readUInt32();
|
||||
e.buyoutPrice = packet.readUInt32();
|
||||
e.timeLeftMs = packet.readUInt32();
|
||||
e.bidderGuid = packet.readUInt64();
|
||||
e.currentBid = packet.readUInt32();
|
||||
data.auctions.push_back(e);
|
||||
}
|
||||
|
||||
if (packet.hasRemaining(8)) {
|
||||
data.totalCount = packet.readUInt32();
|
||||
data.searchDelay = packet.readUInt32();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandResult& data) {
|
||||
if (!packet.hasRemaining(12)) return false;
|
||||
data.auctionId = packet.readUInt32();
|
||||
data.action = packet.readUInt32();
|
||||
data.errorCode = packet.readUInt32();
|
||||
if (data.errorCode != 0 && data.action == 2 && packet.hasRemaining(4)) {
|
||||
data.bidError = packet.readUInt32();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pet Stable System
|
||||
// ============================================================
|
||||
|
||||
network::Packet ListStabledPetsPacket::build(uint64_t stableMasterGuid) {
|
||||
network::Packet p(wireOpcode(Opcode::MSG_LIST_STABLED_PETS));
|
||||
p.writeUInt64(stableMasterGuid);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet StablePetPacket::build(uint64_t stableMasterGuid, uint8_t slot) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_STABLE_PET));
|
||||
p.writeUInt64(stableMasterGuid);
|
||||
p.writeUInt8(slot);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet UnstablePetPacket::build(uint64_t stableMasterGuid, uint32_t petNumber) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_UNSTABLE_PET));
|
||||
p.writeUInt64(stableMasterGuid);
|
||||
p.writeUInt32(petNumber);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet PetRenamePacket::build(uint64_t petGuid, const std::string& name, uint8_t isDeclined) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_PET_RENAME));
|
||||
p.writeUInt64(petGuid);
|
||||
p.writeString(name); // null-terminated
|
||||
p.writeUInt8(isDeclined);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet SetTitlePacket::build(int32_t titleBit) {
|
||||
// CMSG_SET_TITLE: int32 titleBit (-1 = remove active title)
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_SET_TITLE));
|
||||
p.writeUInt32(static_cast<uint32_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
|
||||
1246
src/game/world_packets_entity.cpp
Normal file
1246
src/game/world_packets_entity.cpp
Normal file
File diff suppressed because it is too large
Load diff
1214
src/game/world_packets_social.cpp
Normal file
1214
src/game/world_packets_social.cpp
Normal file
File diff suppressed because it is too large
Load diff
1420
src/game/world_packets_world.cpp
Normal file
1420
src/game/world_packets_world.cpp
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1271
src/rendering/m2_renderer_instance.cpp
Normal file
1271
src/rendering/m2_renderer_instance.cpp
Normal file
File diff suppressed because it is too large
Load diff
364
src/rendering/m2_renderer_internal.h
Normal file
364
src/rendering/m2_renderer_internal.h
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
// m2_renderer_internal.h — shared helpers for the m2_renderer split files.
|
||||
// All functions are inline to allow inclusion in multiple translation units.
|
||||
#pragma once
|
||||
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "core/profiler.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace m2_internal {
|
||||
|
||||
// ---- RNG helpers ----
|
||||
inline std::mt19937& rng() {
|
||||
static std::mt19937 gen(std::random_device{}());
|
||||
return gen;
|
||||
}
|
||||
inline uint32_t randRange(uint32_t maxExclusive) {
|
||||
if (maxExclusive == 0) return 0;
|
||||
return std::uniform_int_distribution<uint32_t>(0, maxExclusive - 1)(rng());
|
||||
}
|
||||
inline float randFloat(float lo, float hi) {
|
||||
return std::uniform_real_distribution<float>(lo, hi)(rng());
|
||||
}
|
||||
|
||||
// ---- Constants ----
|
||||
inline const auto kLavaAnimStart = std::chrono::steady_clock::now();
|
||||
inline constexpr uint32_t kParticleFlagRandomized = 0x40;
|
||||
inline constexpr uint32_t kParticleFlagTiled = 0x80;
|
||||
inline constexpr float kSmokeEmitInterval = 1.0f / 48.0f;
|
||||
|
||||
// ---- Geometry / collision helpers ----
|
||||
|
||||
inline float computeGroundDetailDownOffset(const M2ModelGPU& model, float scale) {
|
||||
const float pivotComp = glm::clamp(std::max(0.0f, model.boundMin.z * scale), 0.0f, 0.10f);
|
||||
const float terrainSink = 0.03f;
|
||||
return pivotComp + terrainSink;
|
||||
}
|
||||
|
||||
inline void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::vec3& outMax) {
|
||||
glm::vec3 center = (model.boundMin + model.boundMax) * 0.5f;
|
||||
glm::vec3 half = (model.boundMax - model.boundMin) * 0.5f;
|
||||
|
||||
if (model.collisionTreeTrunk) {
|
||||
float modelHoriz = std::max(model.boundMax.x - model.boundMin.x,
|
||||
model.boundMax.y - model.boundMin.y);
|
||||
float trunkHalf = std::clamp(modelHoriz * 0.05f, 0.5f, 5.0f);
|
||||
half.x = trunkHalf;
|
||||
half.y = trunkHalf;
|
||||
half.z = std::min(trunkHalf * 2.5f, 3.5f);
|
||||
center.z = model.boundMin.z + half.z;
|
||||
} else if (model.collisionNarrowVerticalProp) {
|
||||
half.x *= 0.30f;
|
||||
half.y *= 0.30f;
|
||||
half.z *= 0.96f;
|
||||
} else if (model.collisionSmallSolidProp) {
|
||||
half.x *= 1.00f;
|
||||
half.y *= 1.00f;
|
||||
half.z *= 1.00f;
|
||||
} else if (model.collisionSteppedLowPlatform) {
|
||||
half.x *= 0.98f;
|
||||
half.y *= 0.98f;
|
||||
half.z *= 0.52f;
|
||||
} else {
|
||||
half.x *= 0.66f;
|
||||
half.y *= 0.66f;
|
||||
half.z *= 0.76f;
|
||||
}
|
||||
|
||||
outMin = center - half;
|
||||
outMax = center + half;
|
||||
}
|
||||
|
||||
inline float getEffectiveCollisionTopLocal(const M2ModelGPU& model,
|
||||
const glm::vec3& localPos,
|
||||
const glm::vec3& localMin,
|
||||
const glm::vec3& localMax) {
|
||||
if (!model.collisionSteppedFountain && !model.collisionSteppedLowPlatform) {
|
||||
return localMax.z;
|
||||
}
|
||||
|
||||
glm::vec2 center((localMin.x + localMax.x) * 0.5f, (localMin.y + localMax.y) * 0.5f);
|
||||
glm::vec2 half((localMax.x - localMin.x) * 0.5f, (localMax.y - localMin.y) * 0.5f);
|
||||
if (half.x < 1e-4f || half.y < 1e-4f) {
|
||||
return localMax.z;
|
||||
}
|
||||
|
||||
float nx = (localPos.x - center.x) / half.x;
|
||||
float ny = (localPos.y - center.y) / half.y;
|
||||
float r = std::sqrt(nx * nx + ny * ny);
|
||||
|
||||
float h = localMax.z - localMin.z;
|
||||
if (model.collisionSteppedFountain) {
|
||||
if (r > 0.85f) return localMin.z + h * 0.18f;
|
||||
if (r > 0.65f) return localMin.z + h * 0.36f;
|
||||
if (r > 0.45f) return localMin.z + h * 0.54f;
|
||||
if (r > 0.28f) return localMin.z + h * 0.70f;
|
||||
if (r > 0.14f) return localMin.z + h * 0.84f;
|
||||
return localMin.z + h * 0.96f;
|
||||
}
|
||||
|
||||
float edge = std::max(std::abs(nx), std::abs(ny));
|
||||
if (edge > 0.92f) return localMin.z + h * 0.06f;
|
||||
if (edge > 0.72f) return localMin.z + h * 0.30f;
|
||||
return localMin.z + h * 0.62f;
|
||||
}
|
||||
|
||||
inline bool segmentIntersectsAABB(const glm::vec3& from, const glm::vec3& to,
|
||||
const glm::vec3& bmin, const glm::vec3& bmax,
|
||||
float& outEnterT) {
|
||||
glm::vec3 d = to - from;
|
||||
float tEnter = 0.0f;
|
||||
float tExit = 1.0f;
|
||||
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
if (std::abs(d[axis]) < 1e-6f) {
|
||||
if (from[axis] < bmin[axis] || from[axis] > bmax[axis]) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
float inv = 1.0f / d[axis];
|
||||
float t0 = (bmin[axis] - from[axis]) * inv;
|
||||
float t1 = (bmax[axis] - from[axis]) * inv;
|
||||
if (t0 > t1) std::swap(t0, t1);
|
||||
|
||||
tEnter = std::max(tEnter, t0);
|
||||
tExit = std::min(tExit, t1);
|
||||
if (tEnter > tExit) return false;
|
||||
}
|
||||
|
||||
outEnterT = tEnter;
|
||||
return tExit >= 0.0f && tEnter <= 1.0f;
|
||||
}
|
||||
|
||||
inline void transformAABB(const glm::mat4& modelMatrix,
|
||||
const glm::vec3& localMin,
|
||||
const glm::vec3& localMax,
|
||||
glm::vec3& outMin,
|
||||
glm::vec3& outMax) {
|
||||
const glm::vec3 corners[8] = {
|
||||
{localMin.x, localMin.y, localMin.z},
|
||||
{localMin.x, localMin.y, localMax.z},
|
||||
{localMin.x, localMax.y, localMin.z},
|
||||
{localMin.x, localMax.y, localMax.z},
|
||||
{localMax.x, localMin.y, localMin.z},
|
||||
{localMax.x, localMin.y, localMax.z},
|
||||
{localMax.x, localMax.y, localMin.z},
|
||||
{localMax.x, localMax.y, localMax.z}
|
||||
};
|
||||
|
||||
outMin = glm::vec3(std::numeric_limits<float>::max());
|
||||
outMax = glm::vec3(-std::numeric_limits<float>::max());
|
||||
for (const auto& c : corners) {
|
||||
glm::vec3 wc = glm::vec3(modelMatrix * glm::vec4(c, 1.0f));
|
||||
outMin = glm::min(outMin, wc);
|
||||
outMax = glm::max(outMax, wc);
|
||||
}
|
||||
}
|
||||
|
||||
inline float pointAABBDistanceSq(const glm::vec3& p, const glm::vec3& bmin, const glm::vec3& bmax) {
|
||||
glm::vec3 q = glm::clamp(p, bmin, bmax);
|
||||
glm::vec3 d = p - q;
|
||||
return glm::dot(d, d);
|
||||
}
|
||||
|
||||
// Möller–Trumbore ray-triangle intersection.
|
||||
// Returns distance along ray if hit, negative if miss.
|
||||
inline float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
||||
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
|
||||
constexpr float EPSILON = 1e-6f;
|
||||
glm::vec3 e1 = v1 - v0;
|
||||
glm::vec3 e2 = v2 - v0;
|
||||
glm::vec3 h = glm::cross(dir, e2);
|
||||
float a = glm::dot(e1, h);
|
||||
if (a > -EPSILON && a < EPSILON) return -1.0f;
|
||||
float f = 1.0f / a;
|
||||
glm::vec3 s = origin - v0;
|
||||
float u = f * glm::dot(s, h);
|
||||
if (u < 0.0f || u > 1.0f) return -1.0f;
|
||||
glm::vec3 q = glm::cross(s, e1);
|
||||
float v = f * glm::dot(dir, q);
|
||||
if (v < 0.0f || u + v > 1.0f) return -1.0f;
|
||||
float t = f * glm::dot(e2, q);
|
||||
return t > EPSILON ? t : -1.0f;
|
||||
}
|
||||
|
||||
// Closest point on triangle to a point (Ericson, Real-Time Collision Detection §5.1.5).
|
||||
inline glm::vec3 closestPointOnTriangle(const glm::vec3& p,
|
||||
const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
|
||||
glm::vec3 ab = b - a, ac = c - a, ap = p - a;
|
||||
float d1 = glm::dot(ab, ap), d2 = glm::dot(ac, ap);
|
||||
if (d1 <= 0.0f && d2 <= 0.0f) return a;
|
||||
glm::vec3 bp = p - b;
|
||||
float d3 = glm::dot(ab, bp), d4 = glm::dot(ac, bp);
|
||||
if (d3 >= 0.0f && d4 <= d3) return b;
|
||||
float vc = d1 * d4 - d3 * d2;
|
||||
if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) {
|
||||
float v = d1 / (d1 - d3);
|
||||
return a + v * ab;
|
||||
}
|
||||
glm::vec3 cp = p - c;
|
||||
float d5 = glm::dot(ab, cp), d6 = glm::dot(ac, cp);
|
||||
if (d6 >= 0.0f && d5 <= d6) return c;
|
||||
float vb = d5 * d2 - d1 * d6;
|
||||
if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) {
|
||||
float w = d2 / (d2 - d6);
|
||||
return a + w * ac;
|
||||
}
|
||||
float va = d3 * d6 - d5 * d4;
|
||||
if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f) {
|
||||
float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
||||
return b + w * (c - b);
|
||||
}
|
||||
float denom = 1.0f / (va + vb + vc);
|
||||
float v = vb * denom;
|
||||
float w = vc * denom;
|
||||
return a + ab * v + ac * w;
|
||||
}
|
||||
|
||||
// ---- Thread-local scratch buffers for collision queries ----
|
||||
inline thread_local std::vector<size_t> tl_m2_candidateScratch;
|
||||
inline thread_local std::unordered_set<uint32_t> tl_m2_candidateIdScratch;
|
||||
inline thread_local std::vector<uint32_t> tl_m2_collisionTriScratch;
|
||||
|
||||
// ---- Bone animation helpers ----
|
||||
|
||||
inline int findKeyframeIndex(const std::vector<uint32_t>& timestamps, float time) {
|
||||
if (timestamps.empty()) return -1;
|
||||
if (timestamps.size() == 1) return 0;
|
||||
auto it = std::upper_bound(timestamps.begin(), timestamps.end(), time,
|
||||
[](float t, uint32_t ts) { return t < static_cast<float>(ts); });
|
||||
if (it == timestamps.begin()) return 0;
|
||||
size_t idx = static_cast<size_t>(it - timestamps.begin()) - 1;
|
||||
return static_cast<int>(std::min(idx, timestamps.size() - 2));
|
||||
}
|
||||
|
||||
inline void resolveTrackTime(const pipeline::M2AnimationTrack& track,
|
||||
int seqIdx, float time,
|
||||
const std::vector<uint32_t>& globalSeqDurations,
|
||||
int& outSeqIdx, float& outTime) {
|
||||
if (track.globalSequence >= 0 &&
|
||||
static_cast<size_t>(track.globalSequence) < globalSeqDurations.size()) {
|
||||
outSeqIdx = 0;
|
||||
float dur = static_cast<float>(globalSeqDurations[track.globalSequence]);
|
||||
if (dur > 0.0f) {
|
||||
outTime = time;
|
||||
while (outTime >= dur) {
|
||||
outTime -= dur;
|
||||
}
|
||||
} else {
|
||||
outTime = 0.0f;
|
||||
}
|
||||
} else {
|
||||
outSeqIdx = seqIdx;
|
||||
outTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
inline glm::vec3 interpVec3(const pipeline::M2AnimationTrack& track,
|
||||
int seqIdx, float time, const glm::vec3& def,
|
||||
const std::vector<uint32_t>& globalSeqDurations) {
|
||||
if (!track.hasData()) return def;
|
||||
int si; float t;
|
||||
resolveTrackTime(track, seqIdx, time, globalSeqDurations, si, t);
|
||||
if (si < 0 || si >= static_cast<int>(track.sequences.size())) return def;
|
||||
const auto& keys = track.sequences[si];
|
||||
if (keys.timestamps.empty() || keys.vec3Values.empty()) return def;
|
||||
auto safe = [&](const glm::vec3& v) -> glm::vec3 {
|
||||
if (std::isnan(v.x) || std::isnan(v.y) || std::isnan(v.z)) return def;
|
||||
return v;
|
||||
};
|
||||
if (keys.vec3Values.size() == 1) return safe(keys.vec3Values[0]);
|
||||
int idx = findKeyframeIndex(keys.timestamps, t);
|
||||
if (idx < 0) return def;
|
||||
size_t i0 = static_cast<size_t>(idx);
|
||||
size_t i1 = std::min(i0 + 1, keys.vec3Values.size() - 1);
|
||||
if (i0 == i1) return safe(keys.vec3Values[i0]);
|
||||
float t0 = static_cast<float>(keys.timestamps[i0]);
|
||||
float t1 = static_cast<float>(keys.timestamps[i1]);
|
||||
float dur = t1 - t0;
|
||||
float frac = (dur > 0.0f) ? glm::clamp((t - t0) / dur, 0.0f, 1.0f) : 0.0f;
|
||||
return safe(glm::mix(keys.vec3Values[i0], keys.vec3Values[i1], frac));
|
||||
}
|
||||
|
||||
inline glm::quat interpQuat(const pipeline::M2AnimationTrack& track,
|
||||
int seqIdx, float time,
|
||||
const std::vector<uint32_t>& globalSeqDurations) {
|
||||
glm::quat identity(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
if (!track.hasData()) return identity;
|
||||
int si; float t;
|
||||
resolveTrackTime(track, seqIdx, time, globalSeqDurations, si, t);
|
||||
if (si < 0 || si >= static_cast<int>(track.sequences.size())) return identity;
|
||||
const auto& keys = track.sequences[si];
|
||||
if (keys.timestamps.empty() || keys.quatValues.empty()) return identity;
|
||||
auto safe = [&](const glm::quat& q) -> glm::quat {
|
||||
float lenSq = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;
|
||||
if (lenSq < 0.000001f || std::isnan(lenSq)) return identity;
|
||||
return q;
|
||||
};
|
||||
if (keys.quatValues.size() == 1) return safe(keys.quatValues[0]);
|
||||
int idx = findKeyframeIndex(keys.timestamps, t);
|
||||
if (idx < 0) return identity;
|
||||
size_t i0 = static_cast<size_t>(idx);
|
||||
size_t i1 = std::min(i0 + 1, keys.quatValues.size() - 1);
|
||||
if (i0 == i1) return safe(keys.quatValues[i0]);
|
||||
float t0 = static_cast<float>(keys.timestamps[i0]);
|
||||
float t1 = static_cast<float>(keys.timestamps[i1]);
|
||||
float dur = t1 - t0;
|
||||
float frac = (dur > 0.0f) ? glm::clamp((t - t0) / dur, 0.0f, 1.0f) : 0.0f;
|
||||
return glm::slerp(safe(keys.quatValues[i0]), safe(keys.quatValues[i1]), frac);
|
||||
}
|
||||
|
||||
inline void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) {
|
||||
ZoneScopedN("M2::computeBoneMatrices");
|
||||
size_t numBones = std::min(model.bones.size(), size_t(128));
|
||||
if (numBones == 0) return;
|
||||
instance.boneMatrices.resize(numBones);
|
||||
const auto& gsd = model.globalSequenceDurations;
|
||||
|
||||
for (size_t i = 0; i < numBones; i++) {
|
||||
const auto& bone = model.bones[i];
|
||||
glm::vec3 trans = interpVec3(bone.translation, instance.currentSequenceIndex, instance.animTime, glm::vec3(0.0f), gsd);
|
||||
glm::quat rot = interpQuat(bone.rotation, instance.currentSequenceIndex, instance.animTime, gsd);
|
||||
glm::vec3 scl = interpVec3(bone.scale, instance.currentSequenceIndex, instance.animTime, glm::vec3(1.0f), gsd);
|
||||
|
||||
if (scl.x < 0.001f) scl.x = 1.0f;
|
||||
if (scl.y < 0.001f) scl.y = 1.0f;
|
||||
if (scl.z < 0.001f) scl.z = 1.0f;
|
||||
|
||||
glm::mat4 local = glm::translate(glm::mat4(1.0f), bone.pivot);
|
||||
local = glm::translate(local, trans);
|
||||
local *= glm::toMat4(rot);
|
||||
local = glm::scale(local, scl);
|
||||
local = glm::translate(local, -bone.pivot);
|
||||
|
||||
if (bone.parentBone >= 0 && static_cast<size_t>(bone.parentBone) < numBones) {
|
||||
instance.boneMatrices[i] = instance.boneMatrices[bone.parentBone] * local;
|
||||
} else {
|
||||
instance.boneMatrices[i] = local;
|
||||
}
|
||||
}
|
||||
instance.bonesDirty[0] = instance.bonesDirty[1] = true;
|
||||
}
|
||||
|
||||
} // namespace m2_internal
|
||||
|
||||
// Pull all symbols into the rendering namespace so existing code compiles unchanged
|
||||
using namespace m2_internal;
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
618
src/rendering/m2_renderer_particles.cpp
Normal file
618
src/rendering/m2_renderer_particles.cpp
Normal file
|
|
@ -0,0 +1,618 @@
|
|||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/m2_renderer_internal.h"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/vk_buffer.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_pipeline.hpp"
|
||||
#include "rendering/vk_shader.hpp"
|
||||
#include "rendering/vk_utils.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// --- M2 Particle Emitter Helpers ---
|
||||
|
||||
float M2Renderer::interpFloat(const pipeline::M2AnimationTrack& track, float animTime,
|
||||
int seqIdx, const std::vector<pipeline::M2Sequence>& /*seqs*/,
|
||||
const std::vector<uint32_t>& globalSeqDurations) {
|
||||
if (!track.hasData()) return 0.0f;
|
||||
int si; float t;
|
||||
resolveTrackTime(track, seqIdx, animTime, globalSeqDurations, si, t);
|
||||
if (si < 0 || si >= static_cast<int>(track.sequences.size())) return 0.0f;
|
||||
const auto& keys = track.sequences[si];
|
||||
if (keys.timestamps.empty() || keys.floatValues.empty()) return 0.0f;
|
||||
if (keys.floatValues.size() == 1) return keys.floatValues[0];
|
||||
int idx = findKeyframeIndex(keys.timestamps, t);
|
||||
if (idx < 0) return 0.0f;
|
||||
size_t i0 = static_cast<size_t>(idx);
|
||||
size_t i1 = std::min(i0 + 1, keys.floatValues.size() - 1);
|
||||
if (i0 == i1) return keys.floatValues[i0];
|
||||
float t0 = static_cast<float>(keys.timestamps[i0]);
|
||||
float t1 = static_cast<float>(keys.timestamps[i1]);
|
||||
float dur = t1 - t0;
|
||||
float frac = (dur > 0.0f) ? glm::clamp((t - t0) / dur, 0.0f, 1.0f) : 0.0f;
|
||||
return glm::mix(keys.floatValues[i0], keys.floatValues[i1], frac);
|
||||
}
|
||||
|
||||
// Interpolate an M2 FBlock (particle lifetime curve) at a given life ratio [0..1].
|
||||
// FBlocks store per-lifetime keyframes for particle color, alpha, and scale.
|
||||
// NOTE: interpFBlockFloat and interpFBlockVec3 share identical interpolation logic —
|
||||
// if you fix a bug in one, update the other to match.
|
||||
float M2Renderer::interpFBlockFloat(const pipeline::M2FBlock& fb, float lifeRatio) {
|
||||
if (fb.floatValues.empty()) return 1.0f;
|
||||
if (fb.floatValues.size() == 1 || fb.timestamps.empty()) return fb.floatValues[0];
|
||||
lifeRatio = glm::clamp(lifeRatio, 0.0f, 1.0f);
|
||||
for (size_t i = 0; i < fb.timestamps.size() - 1; i++) {
|
||||
if (lifeRatio <= fb.timestamps[i + 1]) {
|
||||
float t0 = fb.timestamps[i];
|
||||
float t1 = fb.timestamps[i + 1];
|
||||
float dur = t1 - t0;
|
||||
float frac = (dur > 0.0f) ? (lifeRatio - t0) / dur : 0.0f;
|
||||
size_t v0 = std::min(i, fb.floatValues.size() - 1);
|
||||
size_t v1 = std::min(i + 1, fb.floatValues.size() - 1);
|
||||
return glm::mix(fb.floatValues[v0], fb.floatValues[v1], frac);
|
||||
}
|
||||
}
|
||||
return fb.floatValues.back();
|
||||
}
|
||||
|
||||
glm::vec3 M2Renderer::interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio) {
|
||||
if (fb.vec3Values.empty()) return glm::vec3(1.0f);
|
||||
if (fb.vec3Values.size() == 1 || fb.timestamps.empty()) return fb.vec3Values[0];
|
||||
lifeRatio = glm::clamp(lifeRatio, 0.0f, 1.0f);
|
||||
for (size_t i = 0; i < fb.timestamps.size() - 1; i++) {
|
||||
if (lifeRatio <= fb.timestamps[i + 1]) {
|
||||
float t0 = fb.timestamps[i];
|
||||
float t1 = fb.timestamps[i + 1];
|
||||
float dur = t1 - t0;
|
||||
float frac = (dur > 0.0f) ? (lifeRatio - t0) / dur : 0.0f;
|
||||
size_t v0 = std::min(i, fb.vec3Values.size() - 1);
|
||||
size_t v1 = std::min(i + 1, fb.vec3Values.size() - 1);
|
||||
return glm::mix(fb.vec3Values[v0], fb.vec3Values[v1], frac);
|
||||
}
|
||||
}
|
||||
return fb.vec3Values.back();
|
||||
}
|
||||
|
||||
std::vector<glm::vec3> M2Renderer::getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const {
|
||||
std::vector<glm::vec3> result;
|
||||
float maxDistSq = maxDist * maxDist;
|
||||
for (const auto& inst : instances) {
|
||||
if (!inst.cachedModel || !inst.cachedModel->isWaterVegetation) continue;
|
||||
glm::vec3 diff = inst.position - camPos;
|
||||
if (glm::dot(diff, diff) <= maxDistSq) {
|
||||
result.push_back(inst.position);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt) {
|
||||
if (inst.emitterAccumulators.size() != gpu.particleEmitters.size()) {
|
||||
inst.emitterAccumulators.resize(gpu.particleEmitters.size(), 0.0f);
|
||||
}
|
||||
|
||||
std::uniform_real_distribution<float> dist01(0.0f, 1.0f);
|
||||
std::uniform_real_distribution<float> distN(-1.0f, 1.0f);
|
||||
std::uniform_int_distribution<int> distTile;
|
||||
|
||||
for (size_t ei = 0; ei < gpu.particleEmitters.size(); ei++) {
|
||||
const auto& em = gpu.particleEmitters[ei];
|
||||
if (!em.enabled) continue;
|
||||
|
||||
float rate = interpFloat(em.emissionRate, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
float life = interpFloat(em.lifespan, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
if (rate <= 0.0f || life <= 0.0f) continue;
|
||||
|
||||
inst.emitterAccumulators[ei] += rate * dt;
|
||||
|
||||
while (inst.emitterAccumulators[ei] >= 1.0f && inst.particles.size() < MAX_M2_PARTICLES) {
|
||||
inst.emitterAccumulators[ei] -= 1.0f;
|
||||
|
||||
M2Particle p;
|
||||
p.emitterIndex = static_cast<int>(ei);
|
||||
p.life = 0.0f;
|
||||
p.maxLife = life;
|
||||
p.tileIndex = 0.0f;
|
||||
|
||||
// Position: emitter position transformed by bone matrix
|
||||
glm::vec3 localPos = em.position;
|
||||
glm::mat4 boneXform = glm::mat4(1.0f);
|
||||
if (em.bone < inst.boneMatrices.size()) {
|
||||
boneXform = inst.boneMatrices[em.bone];
|
||||
}
|
||||
glm::vec3 worldPos = glm::vec3(inst.modelMatrix * boneXform * glm::vec4(localPos, 1.0f));
|
||||
p.position = worldPos;
|
||||
|
||||
// Velocity: emission speed in upward direction + random spread
|
||||
float speed = interpFloat(em.emissionSpeed, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
float vRange = interpFloat(em.verticalRange, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
float hRange = interpFloat(em.horizontalRange, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
|
||||
// Base direction: up in model space, transformed to world
|
||||
glm::vec3 dir(0.0f, 0.0f, 1.0f);
|
||||
// Add random spread
|
||||
dir.x += distN(particleRng_) * hRange;
|
||||
dir.y += distN(particleRng_) * hRange;
|
||||
dir.z += distN(particleRng_) * vRange;
|
||||
float lenSq = glm::dot(dir, dir);
|
||||
if (lenSq > 0.001f * 0.001f) dir *= glm::inversesqrt(lenSq);
|
||||
|
||||
// Transform direction by bone + model orientation (rotation only)
|
||||
glm::mat3 rotMat = glm::mat3(inst.modelMatrix * boneXform);
|
||||
p.velocity = rotMat * dir * speed;
|
||||
|
||||
// When emission speed is ~0 and bone animation isn't loaded (.anim files),
|
||||
// particles pile up at the same position. Give them a drift so they
|
||||
// spread outward like a mist/spray effect instead of clustering.
|
||||
if (std::abs(speed) < 0.01f) {
|
||||
if (gpu.isFireflyEffect) {
|
||||
// Fireflies: gentle random drift in all directions
|
||||
p.velocity = rotMat * glm::vec3(
|
||||
distN(particleRng_) * 0.6f,
|
||||
distN(particleRng_) * 0.6f,
|
||||
distN(particleRng_) * 0.3f
|
||||
);
|
||||
} else {
|
||||
p.velocity = rotMat * glm::vec3(
|
||||
distN(particleRng_) * 1.0f,
|
||||
distN(particleRng_) * 1.0f,
|
||||
-dist01(particleRng_) * 0.5f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t tilesX = std::max<uint16_t>(em.textureCols, 1);
|
||||
const uint32_t tilesY = std::max<uint16_t>(em.textureRows, 1);
|
||||
const uint32_t totalTiles = tilesX * tilesY;
|
||||
if ((em.flags & kParticleFlagTiled) && totalTiles > 1) {
|
||||
if (em.flags & kParticleFlagRandomized) {
|
||||
distTile = std::uniform_int_distribution<int>(0, static_cast<int>(totalTiles - 1));
|
||||
p.tileIndex = static_cast<float>(distTile(particleRng_));
|
||||
} else {
|
||||
p.tileIndex = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
inst.particles.push_back(p);
|
||||
}
|
||||
// Cap accumulator to avoid bursts after lag
|
||||
if (inst.emitterAccumulators[ei] > 2.0f) {
|
||||
inst.emitterAccumulators[ei] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void M2Renderer::updateParticles(M2Instance& inst, float dt) {
|
||||
if (!inst.cachedModel) return;
|
||||
const auto& gpu = *inst.cachedModel;
|
||||
|
||||
for (size_t i = 0; i < inst.particles.size(); ) {
|
||||
auto& p = inst.particles[i];
|
||||
p.life += dt;
|
||||
if (p.life >= p.maxLife) {
|
||||
// Swap-and-pop removal
|
||||
inst.particles[i] = inst.particles.back();
|
||||
inst.particles.pop_back();
|
||||
continue;
|
||||
}
|
||||
// Apply gravity
|
||||
if (p.emitterIndex >= 0 && p.emitterIndex < static_cast<int>(gpu.particleEmitters.size())) {
|
||||
const auto& pem = gpu.particleEmitters[p.emitterIndex];
|
||||
float grav = interpFloat(pem.gravity,
|
||||
inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
// When M2 gravity is 0, apply default gravity so particles arc downward.
|
||||
// Many fountain M2s rely on bone animation (.anim files) we don't load yet.
|
||||
// Firefly/ambient glow particles intentionally have zero gravity — skip fallback.
|
||||
if (grav == 0.0f && !gpu.isFireflyEffect) {
|
||||
float emSpeed = interpFloat(pem.emissionSpeed,
|
||||
inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
if (std::abs(emSpeed) > 0.1f) {
|
||||
grav = 4.0f; // spray particles
|
||||
} else {
|
||||
grav = 1.5f; // mist/drift particles - gentler fall
|
||||
}
|
||||
}
|
||||
p.velocity.z -= grav * dt;
|
||||
}
|
||||
p.position += p.velocity * dt;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ribbon emitter simulation
|
||||
// ---------------------------------------------------------------------------
|
||||
void M2Renderer::updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt) {
|
||||
const auto& emitters = gpu.ribbonEmitters;
|
||||
if (emitters.empty()) return;
|
||||
|
||||
// Grow per-instance state arrays if needed
|
||||
if (inst.ribbonEdges.size() != emitters.size()) {
|
||||
inst.ribbonEdges.resize(emitters.size());
|
||||
}
|
||||
if (inst.ribbonEdgeAccumulators.size() != emitters.size()) {
|
||||
inst.ribbonEdgeAccumulators.resize(emitters.size(), 0.0f);
|
||||
}
|
||||
|
||||
for (size_t ri = 0; ri < emitters.size(); ri++) {
|
||||
const auto& em = emitters[ri];
|
||||
auto& edges = inst.ribbonEdges[ri];
|
||||
auto& accum = inst.ribbonEdgeAccumulators[ri];
|
||||
|
||||
// Determine bone world position for spine
|
||||
glm::vec3 spineWorld = inst.position;
|
||||
if (em.bone < inst.boneMatrices.size()) {
|
||||
glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f);
|
||||
spineWorld = glm::vec3(inst.modelMatrix * inst.boneMatrices[em.bone] * local);
|
||||
} else {
|
||||
glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f);
|
||||
spineWorld = glm::vec3(inst.modelMatrix * local);
|
||||
}
|
||||
|
||||
// Evaluate animated tracks (use first available sequence key, or fallback value)
|
||||
auto getFloatVal = [&](const pipeline::M2AnimationTrack& track, float fallback) -> float {
|
||||
for (const auto& seq : track.sequences) {
|
||||
if (!seq.floatValues.empty()) return seq.floatValues[0];
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
auto getVec3Val = [&](const pipeline::M2AnimationTrack& track, glm::vec3 fallback) -> glm::vec3 {
|
||||
for (const auto& seq : track.sequences) {
|
||||
if (!seq.vec3Values.empty()) return seq.vec3Values[0];
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
float visibility = getFloatVal(em.visibilityTrack, 1.0f);
|
||||
float heightAbove = getFloatVal(em.heightAboveTrack, 0.5f);
|
||||
float heightBelow = getFloatVal(em.heightBelowTrack, 0.5f);
|
||||
glm::vec3 color = getVec3Val(em.colorTrack, glm::vec3(1.0f));
|
||||
float alpha = getFloatVal(em.alphaTrack, 1.0f);
|
||||
|
||||
// Age existing edges and remove expired ones
|
||||
for (auto& e : edges) {
|
||||
e.age += dt;
|
||||
// Apply gravity
|
||||
if (em.gravity != 0.0f) {
|
||||
e.worldPos.z -= em.gravity * dt * dt * 0.5f;
|
||||
}
|
||||
}
|
||||
while (!edges.empty() && edges.front().age >= em.edgeLifetime) {
|
||||
edges.pop_front();
|
||||
}
|
||||
|
||||
// Emit new edges based on edgesPerSecond
|
||||
if (visibility > 0.5f) {
|
||||
accum += em.edgesPerSecond * dt;
|
||||
while (accum >= 1.0f) {
|
||||
accum -= 1.0f;
|
||||
M2Instance::RibbonEdge e;
|
||||
e.worldPos = spineWorld;
|
||||
e.color = color;
|
||||
e.alpha = alpha;
|
||||
e.heightAbove = heightAbove;
|
||||
e.heightBelow = heightBelow;
|
||||
e.age = 0.0f;
|
||||
edges.push_back(e);
|
||||
// Cap trail length
|
||||
if (edges.size() > 128) edges.pop_front();
|
||||
}
|
||||
} else {
|
||||
accum = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ribbon rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
void M2Renderer::renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||
if (!ribbonPipeline_ || !ribbonAdditivePipeline_ || !ribbonVB_ || !ribbonVBMapped_) return;
|
||||
|
||||
// Build camera right vector for billboard orientation
|
||||
// For ribbons we orient the quad strip along the spine with screen-space up.
|
||||
// Simple approach: use world-space Z=up for the ribbon cross direction.
|
||||
const glm::vec3 upWorld(0.0f, 0.0f, 1.0f);
|
||||
|
||||
float* dst = static_cast<float*>(ribbonVBMapped_);
|
||||
size_t written = 0;
|
||||
|
||||
ribbonDraws_.clear();
|
||||
auto& draws = ribbonDraws_;
|
||||
|
||||
for (const auto& inst : instances) {
|
||||
if (!inst.cachedModel) continue;
|
||||
const auto& gpu = *inst.cachedModel;
|
||||
if (gpu.ribbonEmitters.empty()) continue;
|
||||
|
||||
for (size_t ri = 0; ri < gpu.ribbonEmitters.size(); ri++) {
|
||||
if (ri >= inst.ribbonEdges.size()) continue;
|
||||
const auto& edges = inst.ribbonEdges[ri];
|
||||
if (edges.size() < 2) continue;
|
||||
|
||||
const auto& em = gpu.ribbonEmitters[ri];
|
||||
|
||||
// Select blend pipeline based on material blend mode
|
||||
bool additive = false;
|
||||
if (em.materialIndex < gpu.batches.size()) {
|
||||
additive = (gpu.batches[em.materialIndex].blendMode >= 3);
|
||||
}
|
||||
VkPipeline pipe = additive ? ribbonAdditivePipeline_ : ribbonPipeline_;
|
||||
|
||||
// Descriptor set for texture
|
||||
VkDescriptorSet texSet = (ri < gpu.ribbonTexSets.size())
|
||||
? gpu.ribbonTexSets[ri] : VK_NULL_HANDLE;
|
||||
if (!texSet) continue;
|
||||
|
||||
uint32_t firstVert = static_cast<uint32_t>(written);
|
||||
|
||||
// Emit triangle strip: 2 verts per edge (top + bottom)
|
||||
for (size_t ei = 0; ei < edges.size(); ei++) {
|
||||
if (written + 2 > MAX_RIBBON_VERTS) break;
|
||||
const auto& e = edges[ei];
|
||||
float t = (em.edgeLifetime > 0.0f)
|
||||
? 1.0f - (e.age / em.edgeLifetime) : 1.0f;
|
||||
float a = e.alpha * t;
|
||||
float u = static_cast<float>(ei) / static_cast<float>(edges.size() - 1);
|
||||
|
||||
// Top vertex (above spine along upWorld)
|
||||
glm::vec3 top = e.worldPos + upWorld * e.heightAbove;
|
||||
dst[written * 9 + 0] = top.x;
|
||||
dst[written * 9 + 1] = top.y;
|
||||
dst[written * 9 + 2] = top.z;
|
||||
dst[written * 9 + 3] = e.color.r;
|
||||
dst[written * 9 + 4] = e.color.g;
|
||||
dst[written * 9 + 5] = e.color.b;
|
||||
dst[written * 9 + 6] = a;
|
||||
dst[written * 9 + 7] = u;
|
||||
dst[written * 9 + 8] = 0.0f; // v = top
|
||||
written++;
|
||||
|
||||
// Bottom vertex (below spine)
|
||||
glm::vec3 bot = e.worldPos - upWorld * e.heightBelow;
|
||||
dst[written * 9 + 0] = bot.x;
|
||||
dst[written * 9 + 1] = bot.y;
|
||||
dst[written * 9 + 2] = bot.z;
|
||||
dst[written * 9 + 3] = e.color.r;
|
||||
dst[written * 9 + 4] = e.color.g;
|
||||
dst[written * 9 + 5] = e.color.b;
|
||||
dst[written * 9 + 6] = a;
|
||||
dst[written * 9 + 7] = u;
|
||||
dst[written * 9 + 8] = 1.0f; // v = bottom
|
||||
written++;
|
||||
}
|
||||
|
||||
uint32_t vertCount = static_cast<uint32_t>(written) - firstVert;
|
||||
if (vertCount >= 4) {
|
||||
draws.push_back({texSet, pipe, firstVert, vertCount});
|
||||
} else {
|
||||
// Rollback if too few verts
|
||||
written = firstVert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (draws.empty() || written == 0) return;
|
||||
|
||||
VkExtent2D ext = vkCtx_->getSwapchainExtent();
|
||||
VkViewport vp{};
|
||||
vp.x = 0; vp.y = 0;
|
||||
vp.width = static_cast<float>(ext.width);
|
||||
vp.height = static_cast<float>(ext.height);
|
||||
vp.minDepth = 0.0f; vp.maxDepth = 1.0f;
|
||||
VkRect2D sc{};
|
||||
sc.offset = {0, 0};
|
||||
sc.extent = ext;
|
||||
vkCmdSetViewport(cmd, 0, 1, &vp);
|
||||
vkCmdSetScissor(cmd, 0, 1, &sc);
|
||||
|
||||
VkPipeline lastPipe = VK_NULL_HANDLE;
|
||||
for (const auto& dc : draws) {
|
||||
if (dc.pipeline != lastPipe) {
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, dc.pipeline);
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
ribbonPipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
|
||||
lastPipe = dc.pipeline;
|
||||
}
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
ribbonPipelineLayout_, 1, 1, &dc.texSet, 0, nullptr);
|
||||
VkDeviceSize offset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &ribbonVB_, &offset);
|
||||
vkCmdDraw(cmd, dc.vertexCount, 1, dc.firstVertex, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||
if (!particlePipeline_ || !m2ParticleVB_) return;
|
||||
|
||||
// Collect all particles from all instances, grouped by texture+blend
|
||||
// Reuse persistent map — clear each group's vertex data but keep bucket structure.
|
||||
for (auto& [k, g] : particleGroups_) {
|
||||
g.vertexData.clear();
|
||||
g.preAllocSet = VK_NULL_HANDLE;
|
||||
}
|
||||
auto& groups = particleGroups_;
|
||||
|
||||
size_t totalParticles = 0;
|
||||
|
||||
for (auto& inst : instances) {
|
||||
if (inst.particles.empty()) continue;
|
||||
if (!inst.cachedModel) continue;
|
||||
const auto& gpu = *inst.cachedModel;
|
||||
|
||||
for (const auto& p : inst.particles) {
|
||||
if (p.emitterIndex < 0 || p.emitterIndex >= static_cast<int>(gpu.particleEmitters.size())) continue;
|
||||
const auto& em = gpu.particleEmitters[p.emitterIndex];
|
||||
|
||||
float lifeRatio = p.life / std::max(p.maxLife, 0.001f);
|
||||
glm::vec3 color = interpFBlockVec3(em.particleColor, lifeRatio);
|
||||
float alpha = std::min(interpFBlockFloat(em.particleAlpha, lifeRatio), 1.0f);
|
||||
float rawScale = interpFBlockFloat(em.particleScale, lifeRatio);
|
||||
|
||||
if (!gpu.isSpellEffect && !gpu.isFireflyEffect) {
|
||||
color = glm::mix(color, glm::vec3(1.0f), 0.7f);
|
||||
if (rawScale > 2.0f) alpha *= 0.02f;
|
||||
if (em.blendingType == 3 || em.blendingType == 4) alpha *= 0.05f;
|
||||
}
|
||||
float scale = (gpu.isSpellEffect || gpu.isFireflyEffect) ? rawScale : std::min(rawScale, 1.5f);
|
||||
|
||||
VkTexture* tex = whiteTexture_.get();
|
||||
if (p.emitterIndex < static_cast<int>(gpu.particleTextures.size())) {
|
||||
tex = gpu.particleTextures[p.emitterIndex];
|
||||
}
|
||||
|
||||
uint16_t tilesX = std::max<uint16_t>(em.textureCols, 1);
|
||||
uint16_t tilesY = std::max<uint16_t>(em.textureRows, 1);
|
||||
uint32_t totalTiles = static_cast<uint32_t>(tilesX) * static_cast<uint32_t>(tilesY);
|
||||
ParticleGroupKey key{tex, em.blendingType, tilesX, tilesY};
|
||||
auto& group = groups[key];
|
||||
group.texture = tex;
|
||||
group.blendType = em.blendingType;
|
||||
group.tilesX = tilesX;
|
||||
group.tilesY = tilesY;
|
||||
// Capture pre-allocated descriptor set on first insertion for this key
|
||||
if (group.preAllocSet == VK_NULL_HANDLE &&
|
||||
p.emitterIndex < static_cast<int>(gpu.particleTexSets.size())) {
|
||||
group.preAllocSet = gpu.particleTexSets[p.emitterIndex];
|
||||
}
|
||||
|
||||
group.vertexData.push_back(p.position.x);
|
||||
group.vertexData.push_back(p.position.y);
|
||||
group.vertexData.push_back(p.position.z);
|
||||
group.vertexData.push_back(color.r);
|
||||
group.vertexData.push_back(color.g);
|
||||
group.vertexData.push_back(color.b);
|
||||
group.vertexData.push_back(alpha);
|
||||
group.vertexData.push_back(scale);
|
||||
float tileIndex = p.tileIndex;
|
||||
if ((em.flags & kParticleFlagTiled) && totalTiles > 1) {
|
||||
float animSeconds = inst.animTime / 1000.0f;
|
||||
uint32_t animFrame = static_cast<uint32_t>(std::floor(animSeconds * totalTiles)) % totalTiles;
|
||||
tileIndex = p.tileIndex + static_cast<float>(animFrame);
|
||||
float tilesFloat = static_cast<float>(totalTiles);
|
||||
// Wrap tile index within totalTiles range
|
||||
while (tileIndex >= tilesFloat) {
|
||||
tileIndex -= tilesFloat;
|
||||
}
|
||||
}
|
||||
group.vertexData.push_back(tileIndex);
|
||||
totalParticles++;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalParticles == 0) return;
|
||||
|
||||
// Bind per-frame set (set 0) for particle pipeline
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
particlePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
|
||||
|
||||
VkDeviceSize vbOffset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &m2ParticleVB_, &vbOffset);
|
||||
|
||||
VkPipeline currentPipeline = VK_NULL_HANDLE;
|
||||
|
||||
for (auto& [key, group] : groups) {
|
||||
if (group.vertexData.empty()) continue;
|
||||
|
||||
uint8_t blendType = group.blendType;
|
||||
VkPipeline desiredPipeline = (blendType == 3 || blendType == 4)
|
||||
? particleAdditivePipeline_ : particlePipeline_;
|
||||
if (desiredPipeline != currentPipeline) {
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, desiredPipeline);
|
||||
currentPipeline = desiredPipeline;
|
||||
}
|
||||
|
||||
// Use pre-allocated stable descriptor set; fall back to per-frame alloc only if unavailable
|
||||
VkDescriptorSet texSet = group.preAllocSet;
|
||||
if (texSet == VK_NULL_HANDLE) {
|
||||
// Fallback: allocate per-frame (pool exhaustion risk — should not happen in practice)
|
||||
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
ai.descriptorPool = materialDescPool_;
|
||||
ai.descriptorSetCount = 1;
|
||||
ai.pSetLayouts = &particleTexLayout_;
|
||||
if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) {
|
||||
VkTexture* tex = group.texture ? group.texture : whiteTexture_.get();
|
||||
VkDescriptorImageInfo imgInfo = tex->descriptorInfo();
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = texSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr);
|
||||
}
|
||||
}
|
||||
if (texSet != VK_NULL_HANDLE) {
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
particlePipelineLayout_, 1, 1, &texSet, 0, nullptr);
|
||||
}
|
||||
|
||||
// Push constants: tileCount + alphaKey
|
||||
struct { float tileX, tileY; int alphaKey; } pc = {
|
||||
static_cast<float>(group.tilesX), static_cast<float>(group.tilesY),
|
||||
(blendType == 1) ? 1 : 0
|
||||
};
|
||||
vkCmdPushConstants(cmd, particlePipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0,
|
||||
sizeof(pc), &pc);
|
||||
|
||||
// Upload and draw in chunks
|
||||
size_t count = group.vertexData.size() / 9;
|
||||
size_t offset = 0;
|
||||
while (offset < count) {
|
||||
size_t batch = std::min(count - offset, MAX_M2_PARTICLES);
|
||||
memcpy(m2ParticleVBMapped_, &group.vertexData[offset * 9], batch * 9 * sizeof(float));
|
||||
vkCmdDraw(cmd, static_cast<uint32_t>(batch), 1, 0, 0);
|
||||
offset += batch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void M2Renderer::renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||
if (smokeParticles.empty() || !smokePipeline_ || !smokeVB_) return;
|
||||
|
||||
// Build vertex data: pos(3) + lifeRatio(1) + size(1) + isSpark(1) per particle
|
||||
size_t count = std::min(smokeParticles.size(), static_cast<size_t>(MAX_SMOKE_PARTICLES));
|
||||
float* dst = static_cast<float*>(smokeVBMapped_);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto& p = smokeParticles[i];
|
||||
*dst++ = p.position.x;
|
||||
*dst++ = p.position.y;
|
||||
*dst++ = p.position.z;
|
||||
*dst++ = p.life / p.maxLife;
|
||||
*dst++ = p.size;
|
||||
*dst++ = p.isSpark;
|
||||
}
|
||||
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, smokePipeline_);
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
smokePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
|
||||
|
||||
// Push constant: screenHeight
|
||||
float screenHeight = static_cast<float>(vkCtx_->getSwapchainExtent().height);
|
||||
vkCmdPushConstants(cmd, smokePipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0,
|
||||
sizeof(float), &screenHeight);
|
||||
|
||||
VkDeviceSize offset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &smokeVB_, &offset);
|
||||
vkCmdDraw(cmd, static_cast<uint32_t>(count), 1, 0, 0);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
1637
src/rendering/m2_renderer_render.cpp
Normal file
1637
src/rendering/m2_renderer_render.cpp
Normal file
File diff suppressed because it is too large
Load diff
235
src/rendering/overlay_system.cpp
Normal file
235
src/rendering/overlay_system.cpp
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
#include "rendering/overlay_system.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/vk_shader.hpp"
|
||||
#include "rendering/vk_pipeline.hpp"
|
||||
#include "rendering/vk_utils.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
OverlaySystem::OverlaySystem(VkContext* ctx)
|
||||
: vkCtx_(ctx) {}
|
||||
|
||||
OverlaySystem::~OverlaySystem() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void OverlaySystem::cleanup() {
|
||||
if (!vkCtx_) return;
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
if (selCirclePipeline_) { vkDestroyPipeline(device, selCirclePipeline_, nullptr); selCirclePipeline_ = VK_NULL_HANDLE; }
|
||||
if (selCirclePipelineLayout_) { vkDestroyPipelineLayout(device, selCirclePipelineLayout_, nullptr); selCirclePipelineLayout_ = VK_NULL_HANDLE; }
|
||||
if (selCircleVertBuf_) { vmaDestroyBuffer(vkCtx_->getAllocator(), selCircleVertBuf_, selCircleVertAlloc_); selCircleVertBuf_ = VK_NULL_HANDLE; selCircleVertAlloc_ = VK_NULL_HANDLE; }
|
||||
if (selCircleIdxBuf_) { vmaDestroyBuffer(vkCtx_->getAllocator(), selCircleIdxBuf_, selCircleIdxAlloc_); selCircleIdxBuf_ = VK_NULL_HANDLE; selCircleIdxAlloc_ = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline_) { vkDestroyPipeline(device, overlayPipeline_, nullptr); overlayPipeline_ = VK_NULL_HANDLE; }
|
||||
if (overlayPipelineLayout_) { vkDestroyPipelineLayout(device, overlayPipelineLayout_, nullptr); overlayPipelineLayout_ = VK_NULL_HANDLE; }
|
||||
}
|
||||
|
||||
void OverlaySystem::recreatePipelines() {
|
||||
if (!vkCtx_) return;
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
// Destroy only pipelines (keep geometry buffers)
|
||||
if (selCirclePipeline_) { vkDestroyPipeline(device, selCirclePipeline_, nullptr); selCirclePipeline_ = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline_) { vkDestroyPipeline(device, overlayPipeline_, nullptr); overlayPipeline_ = VK_NULL_HANDLE; }
|
||||
}
|
||||
|
||||
void OverlaySystem::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
|
||||
selCirclePos_ = pos;
|
||||
selCircleRadius_ = radius;
|
||||
selCircleColor_ = color;
|
||||
selCircleVisible_ = true;
|
||||
}
|
||||
|
||||
void OverlaySystem::clearSelectionCircle() {
|
||||
selCircleVisible_ = false;
|
||||
}
|
||||
|
||||
void OverlaySystem::initSelectionCircle() {
|
||||
if (selCirclePipeline_ != VK_NULL_HANDLE) return;
|
||||
if (!vkCtx_) return;
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
|
||||
// Load shaders
|
||||
VkShaderModule vertShader, fragShader;
|
||||
if (!vertShader.loadFromFile(device, "assets/shaders/selection_circle.vert.spv")) {
|
||||
LOG_ERROR("OverlaySystem: failed to load selection circle vertex shader");
|
||||
return;
|
||||
}
|
||||
if (!fragShader.loadFromFile(device, "assets/shaders/selection_circle.frag.spv")) {
|
||||
LOG_ERROR("OverlaySystem: failed to load selection circle fragment shader");
|
||||
vertShader.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pipeline layout: push constants only (mat4 mvp=64 + vec4 color=16), VERTEX|FRAGMENT
|
||||
VkPushConstantRange pcRange{};
|
||||
pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
pcRange.offset = 0;
|
||||
pcRange.size = 80;
|
||||
selCirclePipelineLayout_ = createPipelineLayout(device, {}, {pcRange});
|
||||
|
||||
// Vertex input: binding 0, stride 12, vec3 at location 0
|
||||
VkVertexInputBindingDescription vertBind{0, 12, VK_VERTEX_INPUT_RATE_VERTEX};
|
||||
VkVertexInputAttributeDescription vertAttr{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0};
|
||||
|
||||
// Build disc geometry as TRIANGLE_LIST (N=48 segments)
|
||||
constexpr int SEGMENTS = 48;
|
||||
std::vector<float> verts;
|
||||
verts.reserve((SEGMENTS + 1) * 3);
|
||||
// Center vertex
|
||||
verts.insert(verts.end(), {0.0f, 0.0f, 0.0f});
|
||||
// Ring vertices
|
||||
for (int i = 0; i <= SEGMENTS; ++i) {
|
||||
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
|
||||
verts.push_back(std::cos(angle));
|
||||
verts.push_back(std::sin(angle));
|
||||
verts.push_back(0.0f);
|
||||
}
|
||||
|
||||
// Build TRIANGLE_LIST indices
|
||||
std::vector<uint16_t> indices;
|
||||
indices.reserve(SEGMENTS * 3);
|
||||
for (int i = 0; i < SEGMENTS; ++i) {
|
||||
indices.push_back(0);
|
||||
indices.push_back(static_cast<uint16_t>(i + 1));
|
||||
indices.push_back(static_cast<uint16_t>(i + 2));
|
||||
}
|
||||
selCircleVertCount_ = SEGMENTS * 3;
|
||||
|
||||
// Upload vertex buffer
|
||||
if (selCircleVertBuf_ == VK_NULL_HANDLE) {
|
||||
AllocatedBuffer vbuf = uploadBuffer(*vkCtx_, verts.data(),
|
||||
verts.size() * sizeof(float), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||
selCircleVertBuf_ = vbuf.buffer;
|
||||
selCircleVertAlloc_ = vbuf.allocation;
|
||||
|
||||
AllocatedBuffer ibuf = uploadBuffer(*vkCtx_, indices.data(),
|
||||
indices.size() * sizeof(uint16_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||
selCircleIdxBuf_ = ibuf.buffer;
|
||||
selCircleIdxAlloc_ = ibuf.allocation;
|
||||
}
|
||||
|
||||
// Build pipeline
|
||||
selCirclePipeline_ = PipelineBuilder()
|
||||
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({vertBind}, {vertAttr})
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
||||
.setMultisample(vkCtx_->getMsaaSamples())
|
||||
.setLayout(selCirclePipelineLayout_)
|
||||
.setRenderPass(vkCtx_->getImGuiRenderPass())
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device, vkCtx_->getPipelineCache());
|
||||
|
||||
vertShader.destroy();
|
||||
fragShader.destroy();
|
||||
|
||||
if (!selCirclePipeline_) {
|
||||
LOG_ERROR("OverlaySystem: failed to build selection circle pipeline");
|
||||
}
|
||||
}
|
||||
|
||||
void OverlaySystem::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection,
|
||||
VkCommandBuffer cmd,
|
||||
HeightQuery2D terrainHeight,
|
||||
HeightQuery3D wmoHeight,
|
||||
HeightQuery3D m2Height) {
|
||||
if (!selCircleVisible_) return;
|
||||
initSelectionCircle();
|
||||
if (selCirclePipeline_ == VK_NULL_HANDLE || cmd == VK_NULL_HANDLE) return;
|
||||
|
||||
// Keep circle anchored near target foot Z.
|
||||
const float baseZ = selCirclePos_.z;
|
||||
float floorZ = baseZ;
|
||||
auto considerFloor = [&](std::optional<float> sample) {
|
||||
if (!sample) return;
|
||||
const float h = *sample;
|
||||
if (h < baseZ - 1.25f || h > baseZ + 0.85f) return;
|
||||
floorZ = std::max(floorZ, h);
|
||||
};
|
||||
|
||||
if (terrainHeight) considerFloor(terrainHeight(selCirclePos_.x, selCirclePos_.y));
|
||||
if (wmoHeight) considerFloor(wmoHeight(selCirclePos_.x, selCirclePos_.y, selCirclePos_.z + 3.0f));
|
||||
if (m2Height) considerFloor(m2Height(selCirclePos_.x, selCirclePos_.y, selCirclePos_.z + 2.0f));
|
||||
|
||||
glm::vec3 raisedPos = selCirclePos_;
|
||||
raisedPos.z = floorZ + 0.17f;
|
||||
glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos);
|
||||
model = glm::scale(model, glm::vec3(selCircleRadius_));
|
||||
|
||||
glm::mat4 mvp = projection * view * model;
|
||||
glm::vec4 color4(selCircleColor_, 1.0f);
|
||||
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, selCirclePipeline_);
|
||||
VkDeviceSize offset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &selCircleVertBuf_, &offset);
|
||||
vkCmdBindIndexBuffer(cmd, selCircleIdxBuf_, 0, VK_INDEX_TYPE_UINT16);
|
||||
vkCmdPushConstants(cmd, selCirclePipelineLayout_,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, 64, &mvp[0][0]);
|
||||
vkCmdPushConstants(cmd, selCirclePipelineLayout_,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
64, 16, &color4[0]);
|
||||
vkCmdDrawIndexed(cmd, static_cast<uint32_t>(selCircleVertCount_), 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
void OverlaySystem::initOverlayPipeline() {
|
||||
if (!vkCtx_) return;
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
|
||||
VkPushConstantRange pc{};
|
||||
pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
pc.offset = 0;
|
||||
pc.size = 16;
|
||||
|
||||
VkPipelineLayoutCreateInfo plCI{};
|
||||
plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
plCI.pushConstantRangeCount = 1;
|
||||
plCI.pPushConstantRanges = &pc;
|
||||
vkCreatePipelineLayout(device, &plCI, nullptr, &overlayPipelineLayout_);
|
||||
|
||||
VkShaderModule vertMod, fragMod;
|
||||
if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") ||
|
||||
!fragMod.loadFromFile(device, "assets/shaders/overlay.frag.spv")) {
|
||||
LOG_ERROR("OverlaySystem: failed to load overlay shaders");
|
||||
vertMod.destroy(); fragMod.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
overlayPipeline_ = PipelineBuilder()
|
||||
.setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({}, {})
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
||||
.setMultisample(vkCtx_->getMsaaSamples())
|
||||
.setLayout(overlayPipelineLayout_)
|
||||
.setRenderPass(vkCtx_->getImGuiRenderPass())
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device, vkCtx_->getPipelineCache());
|
||||
|
||||
vertMod.destroy(); fragMod.destroy();
|
||||
|
||||
if (overlayPipeline_) LOG_INFO("OverlaySystem: overlay pipeline initialized");
|
||||
}
|
||||
|
||||
void OverlaySystem::renderOverlay(const glm::vec4& color, VkCommandBuffer cmd) {
|
||||
if (!overlayPipeline_) initOverlayPipeline();
|
||||
if (!overlayPipeline_ || cmd == VK_NULL_HANDLE) return;
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline_);
|
||||
vkCmdPushConstants(cmd, overlayPipelineLayout_,
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, &color[0]);
|
||||
vkCmdDraw(cmd, 3, 1, 0, 0);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "rendering/performance_hud.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/post_process_pipeline.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/terrain_renderer.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
|
|
@ -198,38 +199,38 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
|
|||
}
|
||||
|
||||
// FSR info
|
||||
if (renderer->isFSREnabled()) {
|
||||
if (renderer->getPostProcessPipeline()->isFSREnabled()) {
|
||||
ImGui::TextColored(colors::kGreen, "FSR 1.0: ON");
|
||||
auto* ctx = renderer->getVkContext();
|
||||
if (ctx) {
|
||||
auto ext = ctx->getSwapchainExtent();
|
||||
float sf = renderer->getFSRScaleFactor();
|
||||
float sf = renderer->getPostProcessPipeline()->getFSRScaleFactor();
|
||||
uint32_t iw = static_cast<uint32_t>(ext.width * sf) & ~1u;
|
||||
uint32_t ih = static_cast<uint32_t>(ext.height * sf) & ~1u;
|
||||
ImGui::Text(" %ux%u -> %ux%u (%.0f%%)", iw, ih, ext.width, ext.height, sf * 100.0f);
|
||||
}
|
||||
}
|
||||
if (renderer->isFSR2Enabled()) {
|
||||
if (renderer->getPostProcessPipeline()->isFSR2Enabled()) {
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "FSR 3 Upscale: ON");
|
||||
ImGui::Text(" JitterSign=%.2f", renderer->getFSR2JitterSign());
|
||||
const bool fgEnabled = renderer->isAmdFsr3FramegenEnabled();
|
||||
const bool fgReady = renderer->isAmdFsr3FramegenRuntimeReady();
|
||||
const bool fgActive = renderer->isAmdFsr3FramegenRuntimeActive();
|
||||
ImGui::Text(" JitterSign=%.2f", renderer->getPostProcessPipeline()->getFSR2JitterSign());
|
||||
const bool fgEnabled = renderer->getPostProcessPipeline()->isAmdFsr3FramegenEnabled();
|
||||
const bool fgReady = renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeReady();
|
||||
const bool fgActive = renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeActive();
|
||||
const char* fgStatus = "Disabled";
|
||||
if (fgEnabled) {
|
||||
fgStatus = fgActive ? "Active" : (fgReady ? "Ready (waiting/fallback)" : "Unavailable");
|
||||
}
|
||||
ImGui::Text(" FSR3 FG: %s (%s)", fgStatus, renderer->getAmdFsr3FramegenRuntimePath());
|
||||
const std::string& fgErr = renderer->getAmdFsr3FramegenRuntimeError();
|
||||
ImGui::Text(" FSR3 FG: %s (%s)", fgStatus, renderer->getPostProcessPipeline()->getAmdFsr3FramegenRuntimePath());
|
||||
const std::string& fgErr = renderer->getPostProcessPipeline()->getAmdFsr3FramegenRuntimeError();
|
||||
if (!fgErr.empty()) {
|
||||
ImGui::TextWrapped(" FG Last Error: %s", fgErr.c_str());
|
||||
}
|
||||
ImGui::Text(" FG Dispatches: %zu", renderer->getAmdFsr3FramegenDispatchCount());
|
||||
ImGui::Text(" Upscale Dispatches: %zu", renderer->getAmdFsr3UpscaleDispatchCount());
|
||||
ImGui::Text(" FG Fallbacks: %zu", renderer->getAmdFsr3FallbackCount());
|
||||
ImGui::Text(" FG Dispatches: %zu", renderer->getPostProcessPipeline()->getAmdFsr3FramegenDispatchCount());
|
||||
ImGui::Text(" Upscale Dispatches: %zu", renderer->getPostProcessPipeline()->getAmdFsr3UpscaleDispatchCount());
|
||||
ImGui::Text(" FG Fallbacks: %zu", renderer->getPostProcessPipeline()->getAmdFsr3FallbackCount());
|
||||
}
|
||||
if (renderer->isFXAAEnabled()) {
|
||||
if (renderer->isFSR2Enabled()) {
|
||||
if (renderer->getPostProcessPipeline()->isFXAAEnabled()) {
|
||||
if (renderer->getPostProcessPipeline()->isFSR2Enabled()) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.8f, 1.0f), "FXAA: ON (FSR3+FXAA combined)");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.6f, 1.0f), "FXAA: ON");
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
#include "rendering/post_process_pipeline.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "rendering/render_graph.hpp"
|
||||
#include "rendering/overlay_system.hpp"
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_vulkan.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
|
@ -574,6 +575,9 @@ bool Renderer::initialize(core::Window* win) {
|
|||
|
||||
// Create render graph and register virtual resources
|
||||
renderGraph_ = std::make_unique<RenderGraph>();
|
||||
|
||||
// Create overlay system (selection circle + fullscreen overlay)
|
||||
overlaySystem_ = std::make_unique<OverlaySystem>(vkCtx);
|
||||
renderGraph_->registerResource("shadow_depth");
|
||||
renderGraph_->registerResource("reflection_texture");
|
||||
renderGraph_->registerResource("cull_visibility");
|
||||
|
|
@ -676,15 +680,10 @@ void Renderer::shutdown() {
|
|||
// Audio shutdown is handled by AudioCoordinator (owned by Application).
|
||||
audioCoordinator_ = nullptr;
|
||||
|
||||
// Cleanup Vulkan selection circle resources
|
||||
if (vkCtx) {
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; }
|
||||
if (selCirclePipelineLayout) { vkDestroyPipelineLayout(device, selCirclePipelineLayout, nullptr); selCirclePipelineLayout = VK_NULL_HANDLE; }
|
||||
if (selCircleVertBuf) { vmaDestroyBuffer(vkCtx->getAllocator(), selCircleVertBuf, selCircleVertAlloc); selCircleVertBuf = VK_NULL_HANDLE; selCircleVertAlloc = VK_NULL_HANDLE; }
|
||||
if (selCircleIdxBuf) { vmaDestroyBuffer(vkCtx->getAllocator(), selCircleIdxBuf, selCircleIdxAlloc); selCircleIdxBuf = VK_NULL_HANDLE; selCircleIdxAlloc = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; }
|
||||
if (overlayPipelineLayout) { vkDestroyPipelineLayout(device, overlayPipelineLayout, nullptr); overlayPipelineLayout = VK_NULL_HANDLE; }
|
||||
// Cleanup selection circle + overlay resources
|
||||
if (overlaySystem_) {
|
||||
overlaySystem_->cleanup();
|
||||
overlaySystem_.reset();
|
||||
}
|
||||
|
||||
// Shutdown post-process pipeline (FSR/FXAA/FSR2 resources) (§4.3)
|
||||
|
|
@ -800,9 +799,7 @@ void Renderer::applyMsaaChange() {
|
|||
if (minimap) minimap->recreatePipelines();
|
||||
|
||||
// Selection circle + overlay + FSR use lazy init, just destroy them
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; }
|
||||
if (overlaySystem_) overlaySystem_->recreatePipelines();
|
||||
if (postProcessPipeline_) postProcessPipeline_->destroyAllResources(); // Will be lazily recreated in beginFrame()
|
||||
|
||||
// Reinitialize ImGui Vulkan backend with new MSAA sample count
|
||||
|
|
@ -998,74 +995,6 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
|||
if (animationController_) animationController_->onCharacterFollow(instanceId);
|
||||
}
|
||||
|
||||
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) {
|
||||
if (animationController_) animationController_->setMounted(mountInstId, mountDisplayId, heightOffset, modelPath);
|
||||
}
|
||||
|
||||
void Renderer::clearMount() {
|
||||
if (animationController_) animationController_->clearMount();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Renderer::playEmote(const std::string& emoteName) {
|
||||
if (animationController_) animationController_->playEmote(emoteName);
|
||||
}
|
||||
|
||||
void Renderer::cancelEmote() {
|
||||
if (animationController_) animationController_->cancelEmote();
|
||||
}
|
||||
|
||||
bool Renderer::isEmoteActive() const {
|
||||
return animationController_ && animationController_->isEmoteActive();
|
||||
}
|
||||
|
||||
void Renderer::setInCombat(bool combat) {
|
||||
if (animationController_) animationController_->setInCombat(combat);
|
||||
}
|
||||
|
||||
void Renderer::setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose, bool isFist,
|
||||
bool isDagger, bool hasOffHand, bool hasShield) {
|
||||
if (animationController_) animationController_->setEquippedWeaponType(inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
|
||||
}
|
||||
|
||||
void Renderer::triggerSpecialAttack(uint32_t spellId) {
|
||||
if (animationController_) animationController_->triggerSpecialAttack(spellId);
|
||||
}
|
||||
|
||||
void Renderer::setEquippedRangedType(RangedWeaponType type) {
|
||||
if (animationController_) animationController_->setEquippedRangedType(type);
|
||||
}
|
||||
|
||||
void Renderer::triggerRangedShot() {
|
||||
if (animationController_) animationController_->triggerRangedShot();
|
||||
}
|
||||
|
||||
RangedWeaponType Renderer::getEquippedRangedType() const {
|
||||
return animationController_ ? animationController_->getEquippedRangedType()
|
||||
: RangedWeaponType::NONE;
|
||||
}
|
||||
|
||||
void Renderer::setCharging(bool c) {
|
||||
if (animationController_) animationController_->setCharging(c);
|
||||
}
|
||||
|
||||
bool Renderer::isCharging() const {
|
||||
return animationController_ && animationController_->isCharging();
|
||||
}
|
||||
|
||||
void Renderer::setTaxiFlight(bool taxi) {
|
||||
if (animationController_) animationController_->setTaxiFlight(taxi);
|
||||
}
|
||||
|
||||
void Renderer::setMountPitchRoll(float pitch, float roll) {
|
||||
if (animationController_) animationController_->setMountPitchRoll(pitch, roll);
|
||||
}
|
||||
|
||||
bool Renderer::isMounted() const {
|
||||
return animationController_ && animationController_->isMounted();
|
||||
}
|
||||
|
||||
bool Renderer::captureScreenshot(const std::string& outputPath) {
|
||||
if (!vkCtx) return false;
|
||||
|
||||
|
|
@ -1161,69 +1090,23 @@ bool Renderer::captureScreenshot(const std::string& outputPath) {
|
|||
return ok != 0;
|
||||
}
|
||||
|
||||
void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
|
||||
if (animationController_) animationController_->triggerLevelUpEffect(position);
|
||||
}
|
||||
|
||||
void Renderer::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
|
||||
if (animationController_) animationController_->startChargeEffect(position, direction);
|
||||
}
|
||||
|
||||
void Renderer::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
|
||||
if (animationController_) animationController_->emitChargeEffect(position, direction);
|
||||
}
|
||||
|
||||
void Renderer::stopChargeEffect() {
|
||||
if (animationController_) animationController_->stopChargeEffect();
|
||||
}
|
||||
|
||||
// ─── Spell Visual Effects — delegated to SpellVisualSystem (§4.4) ────────────
|
||||
|
||||
void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
||||
bool useImpactKit) {
|
||||
if (spellVisualSystem_) spellVisualSystem_->playSpellVisual(visualId, worldPosition, useImpactKit);
|
||||
}
|
||||
|
||||
void Renderer::triggerMeleeSwing() {
|
||||
if (animationController_) animationController_->triggerMeleeSwing();
|
||||
}
|
||||
|
||||
std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) {
|
||||
return AnimationController::getEmoteText(emoteName, targetName);
|
||||
}
|
||||
|
||||
uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) {
|
||||
return AnimationController::getEmoteDbcId(emoteName);
|
||||
}
|
||||
|
||||
std::string Renderer::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName,
|
||||
const std::string* targetName) {
|
||||
return AnimationController::getEmoteTextByDbcId(dbcId, senderName, targetName);
|
||||
}
|
||||
|
||||
uint32_t Renderer::getEmoteAnimByDbcId(uint32_t dbcId) {
|
||||
return AnimationController::getEmoteAnimByDbcId(dbcId);
|
||||
}
|
||||
|
||||
void Renderer::setTargetPosition(const glm::vec3* pos) {
|
||||
if (animationController_) animationController_->setTargetPosition(pos);
|
||||
}
|
||||
|
||||
void Renderer::resetCombatVisualState() {
|
||||
if (animationController_) animationController_->resetCombatVisualState();
|
||||
if (spellVisualSystem_) spellVisualSystem_->reset();
|
||||
}
|
||||
|
||||
bool Renderer::isMoving() const {
|
||||
return cameraController && cameraController->isMoving();
|
||||
const std::string& Renderer::getCurrentZoneName() const {
|
||||
static const std::string empty;
|
||||
return audioCoordinator_ ? audioCoordinator_->getCurrentZoneName() : empty;
|
||||
}
|
||||
|
||||
uint32_t Renderer::getCurrentZoneId() const {
|
||||
return audioCoordinator_ ? audioCoordinator_->getCurrentZoneId() : 0;
|
||||
}
|
||||
|
||||
void Renderer::update(float deltaTime) {
|
||||
ZoneScopedN("Renderer::update");
|
||||
globalTime += deltaTime;
|
||||
if (musicSwitchCooldown_ > 0.0f) {
|
||||
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime);
|
||||
}
|
||||
runDeferredWorldInitStep(deltaTime);
|
||||
|
||||
auto updateStart = std::chrono::steady_clock::now();
|
||||
|
|
@ -1281,7 +1164,7 @@ void Renderer::update(float deltaTime) {
|
|||
weather->setIntensity(wInt);
|
||||
} else {
|
||||
// No server weather — use zone-based weather configuration
|
||||
weather->updateZoneWeather(currentZoneId, deltaTime);
|
||||
weather->updateZoneWeather(getCurrentZoneId(), deltaTime);
|
||||
}
|
||||
weather->setEnabled(true);
|
||||
|
||||
|
|
@ -1291,7 +1174,7 @@ void Renderer::update(float deltaTime) {
|
|||
}
|
||||
} else if (weather) {
|
||||
// No game handler (single-player without network) — zone weather only
|
||||
weather->updateZoneWeather(currentZoneId, deltaTime);
|
||||
weather->updateZoneWeather(getCurrentZoneId(), deltaTime);
|
||||
weather->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1307,7 +1190,7 @@ void Renderer::update(float deltaTime) {
|
|||
} else if (cameraController->isMoving() || cameraController->isRightMouseHeld()) {
|
||||
characterYaw = cameraController->getFacingYaw();
|
||||
} else if (animationController_ && animationController_->isInCombat() &&
|
||||
animationController_->getTargetPosition() && !animationController_->isEmoteActive() && !isMounted()) {
|
||||
animationController_->getTargetPosition() && !animationController_->isEmoteActive() && !(animationController_ && animationController_->isMounted())) {
|
||||
glm::vec3 toTarget = *animationController_->getTargetPosition() - characterPosition;
|
||||
if (toTarget.x * toTarget.x + toTarget.y * toTarget.y > 0.01f) {
|
||||
float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x));
|
||||
|
|
@ -1369,7 +1252,7 @@ void Renderer::update(float deltaTime) {
|
|||
mountDust->update(deltaTime);
|
||||
|
||||
// Spawn dust when mounted and moving on ground
|
||||
if (isMounted() && camera && cameraController && !(animationController_ && animationController_->isTaxiFlight())) {
|
||||
if ((animationController_ && animationController_->isMounted()) && camera && cameraController && !(animationController_ && animationController_->isTaxiFlight())) {
|
||||
bool isMoving = cameraController->isMoving();
|
||||
bool onGround = cameraController->isGrounded();
|
||||
|
||||
|
|
@ -1434,45 +1317,31 @@ void Renderer::update(float deltaTime) {
|
|||
wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &insideWmoId);
|
||||
playerIndoors_ = insideWmo;
|
||||
|
||||
// Ambient environmental sounds: fireplaces, water, birds, etc.
|
||||
if (audioCoordinator_->getAmbientSoundManager() && camera && wmoRenderer && cameraController) {
|
||||
bool isIndoor = insideWmo;
|
||||
bool isSwimming = cameraController->isSwimming();
|
||||
|
||||
// Detect blacksmith buildings to play ambient forge/anvil sounds.
|
||||
// 96048 is the WMO group ID for the Goldshire blacksmith interior.
|
||||
// TODO: extend to other smithy WMO IDs (Ironforge, Orgrimmar, etc.)
|
||||
bool isBlacksmith = (insideWmoId == 96048);
|
||||
|
||||
// Sync weather audio with visual weather system
|
||||
// Ambient environmental sounds + zone/music transitions (delegated to AudioCoordinator)
|
||||
if (audioCoordinator_) {
|
||||
audio::ZoneAudioContext zctx;
|
||||
zctx.deltaTime = deltaTime;
|
||||
zctx.cameraPosition = camPos;
|
||||
zctx.isSwimming = cameraController ? cameraController->isSwimming() : false;
|
||||
zctx.insideWmo = insideWmo;
|
||||
zctx.insideWmoId = insideWmoId;
|
||||
if (weather) {
|
||||
auto weatherType = weather->getWeatherType();
|
||||
float intensity = weather->getIntensity();
|
||||
|
||||
audio::AmbientSoundManager::WeatherType audioWeatherType = audio::AmbientSoundManager::WeatherType::NONE;
|
||||
|
||||
if (weatherType == Weather::Type::RAIN) {
|
||||
if (intensity < 0.33f) {
|
||||
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_LIGHT;
|
||||
} else if (intensity < 0.66f) {
|
||||
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_MEDIUM;
|
||||
} else {
|
||||
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_HEAVY;
|
||||
auto wt = weather->getWeatherType();
|
||||
if (wt == Weather::Type::RAIN) zctx.weatherType = 1;
|
||||
else if (wt == Weather::Type::SNOW) zctx.weatherType = 2;
|
||||
else if (wt == Weather::Type::STORM) zctx.weatherType = 3;
|
||||
zctx.weatherIntensity = weather->getIntensity();
|
||||
}
|
||||
} else if (weatherType == Weather::Type::SNOW) {
|
||||
if (intensity < 0.33f) {
|
||||
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_LIGHT;
|
||||
} else if (intensity < 0.66f) {
|
||||
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_MEDIUM;
|
||||
} else {
|
||||
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_HEAVY;
|
||||
if (terrainManager) {
|
||||
auto tile = terrainManager->getCurrentTile();
|
||||
zctx.tileX = tile.x;
|
||||
zctx.tileY = tile.y;
|
||||
zctx.hasTile = true;
|
||||
}
|
||||
}
|
||||
|
||||
audioCoordinator_->getAmbientSoundManager()->setWeather(audioWeatherType);
|
||||
}
|
||||
|
||||
audioCoordinator_->getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
|
||||
const auto* gh2 = core::Application::getInstance().getGameHandler();
|
||||
zctx.serverZoneId = gh2 ? gh2->getWorldStateZoneId() : 0;
|
||||
zctx.zoneManager = zoneManager.get();
|
||||
audioCoordinator_->updateZoneAudio(zctx);
|
||||
}
|
||||
|
||||
// Wait for M2 doodad animation to finish (was launched earlier in parallel with character anim)
|
||||
|
|
@ -1481,154 +1350,6 @@ void Renderer::update(float deltaTime) {
|
|||
catch (const std::exception& e) { LOG_ERROR("M2 animation worker: ", e.what()); }
|
||||
}
|
||||
|
||||
// Helper: play zone music, dispatching local files (file: prefix) vs MPQ paths
|
||||
auto playZoneMusic = [&](const std::string& music) {
|
||||
if (music.empty()) return;
|
||||
if (music.rfind("file:", 0) == 0) {
|
||||
audioCoordinator_->getMusicManager()->crossfadeToFile(music.substr(5));
|
||||
} else {
|
||||
audioCoordinator_->getMusicManager()->crossfadeTo(music);
|
||||
}
|
||||
};
|
||||
|
||||
// Update zone detection and music
|
||||
if (zoneManager && audioCoordinator_->getMusicManager() && terrainManager && camera) {
|
||||
// Prefer server-authoritative zone ID (from SMSG_INIT_WORLD_STATES);
|
||||
// fall back to tile-based lookup for single-player / offline mode.
|
||||
const auto* gh = core::Application::getInstance().getGameHandler();
|
||||
uint32_t serverZoneId = gh ? gh->getWorldStateZoneId() : 0;
|
||||
auto tile = terrainManager->getCurrentTile();
|
||||
uint32_t zoneId = (serverZoneId != 0) ? serverZoneId : zoneManager->getZoneId(tile.x, tile.y);
|
||||
|
||||
bool insideTavern = false;
|
||||
bool insideBlacksmith = false;
|
||||
std::string tavernMusic;
|
||||
|
||||
// Override with WMO-based detection (e.g., inside Stormwind, taverns, blacksmiths)
|
||||
if (wmoRenderer) {
|
||||
uint32_t wmoModelId = insideWmoId;
|
||||
if (insideWmo) {
|
||||
// Check if inside Stormwind WMO (model ID 10047)
|
||||
if (wmoModelId == 10047) {
|
||||
zoneId = 1519; // Stormwind City
|
||||
}
|
||||
|
||||
// Detect taverns/inns/blacksmiths by WMO model ID
|
||||
// Log WMO ID for debugging
|
||||
static uint32_t lastLoggedWmoId = 0;
|
||||
if (wmoModelId != lastLoggedWmoId) {
|
||||
LOG_INFO("Inside WMO model ID: ", wmoModelId);
|
||||
lastLoggedWmoId = wmoModelId;
|
||||
}
|
||||
|
||||
// Detect blacksmith WMO for ambient forge sounds
|
||||
if (wmoModelId == 96048) { // Goldshire blacksmith interior
|
||||
insideBlacksmith = true;
|
||||
LOG_INFO("Detected blacksmith WMO ", wmoModelId);
|
||||
}
|
||||
|
||||
// These IDs represent typical Alliance and Horde inn buildings
|
||||
if (wmoModelId == 191 || // Goldshire inn (old ID)
|
||||
wmoModelId == 71414 || // Goldshire inn (actual)
|
||||
wmoModelId == 190 || // Small inn (common)
|
||||
wmoModelId == 220 || // Tavern building
|
||||
wmoModelId == 221 || // Large tavern
|
||||
wmoModelId == 5392 || // Horde inn
|
||||
wmoModelId == 5393) { // Another inn variant
|
||||
insideTavern = true;
|
||||
// WoW tavern music (cozy ambient tracks) - FIXED PATHS
|
||||
static const std::vector<std::string> tavernTracks = {
|
||||
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
|
||||
};
|
||||
// Rotate through tracks so the player doesn't always hear the same one.
|
||||
// Post-increment: first visit plays index 0, next plays 1, etc.
|
||||
static int tavernTrackIndex = 0;
|
||||
tavernMusic = tavernTracks[tavernTrackIndex++ % tavernTracks.size()];
|
||||
LOG_INFO("Detected tavern WMO ", wmoModelId, ", playing: ", tavernMusic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tavern music transitions
|
||||
if (insideTavern) {
|
||||
if (!inTavern_ && !tavernMusic.empty()) {
|
||||
inTavern_ = true;
|
||||
LOG_INFO("Entered tavern");
|
||||
audioCoordinator_->getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
} else if (inTavern_) {
|
||||
// Exited tavern - restore zone music with crossfade
|
||||
inTavern_ = false;
|
||||
LOG_INFO("Exited tavern");
|
||||
auto* info = zoneManager->getZoneInfo(currentZoneId);
|
||||
if (info) {
|
||||
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle blacksmith music (stop music when entering blacksmith, let ambience play)
|
||||
if (insideBlacksmith) {
|
||||
if (!inBlacksmith_) {
|
||||
inBlacksmith_ = true;
|
||||
LOG_INFO("Entered blacksmith - stopping music");
|
||||
audioCoordinator_->getMusicManager()->stopMusic();
|
||||
}
|
||||
} else if (inBlacksmith_) {
|
||||
// Exited blacksmith - restore zone music with crossfade
|
||||
inBlacksmith_ = false;
|
||||
LOG_INFO("Exited blacksmith - restoring music");
|
||||
auto* info = zoneManager->getZoneInfo(currentZoneId);
|
||||
if (info) {
|
||||
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle normal zone transitions (only if not in tavern or blacksmith)
|
||||
if (!insideTavern && !insideBlacksmith && zoneId != currentZoneId && zoneId != 0) {
|
||||
currentZoneId = zoneId;
|
||||
auto* info = zoneManager->getZoneInfo(zoneId);
|
||||
if (info) {
|
||||
currentZoneName = info->name;
|
||||
LOG_INFO("Entered zone: ", info->name);
|
||||
if (musicSwitchCooldown_ <= 0.0f) {
|
||||
std::string music = zoneManager->getRandomMusic(zoneId);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update ambient sound manager zone type
|
||||
if (audioCoordinator_->getAmbientSoundManager()) {
|
||||
audioCoordinator_->getAmbientSoundManager()->setZoneId(zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
audioCoordinator_->getMusicManager()->update(deltaTime);
|
||||
|
||||
// When a track finishes, pick a new random track from the current zone
|
||||
if (!audioCoordinator_->getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ &&
|
||||
currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) {
|
||||
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
||||
if (!music.empty()) {
|
||||
playZoneMusic(music);
|
||||
musicSwitchCooldown_ = 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update performance HUD
|
||||
if (performanceHUD) {
|
||||
performanceHUD->update(deltaTime);
|
||||
|
|
@ -1691,215 +1412,12 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) {
|
|||
deferredWorldInitCooldown_ = 0.12f;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Selection Circle
|
||||
// ============================================================
|
||||
|
||||
void Renderer::initSelectionCircle() {
|
||||
if (selCirclePipeline != VK_NULL_HANDLE) return;
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
|
||||
// Load shaders
|
||||
VkShaderModule vertShader, fragShader;
|
||||
if (!vertShader.loadFromFile(device, "assets/shaders/selection_circle.vert.spv")) {
|
||||
LOG_ERROR("initSelectionCircle: failed to load vertex shader");
|
||||
return;
|
||||
}
|
||||
if (!fragShader.loadFromFile(device, "assets/shaders/selection_circle.frag.spv")) {
|
||||
LOG_ERROR("initSelectionCircle: failed to load fragment shader");
|
||||
vertShader.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pipeline layout: push constants only (mat4 mvp=64 + vec4 color=16), VERTEX|FRAGMENT
|
||||
VkPushConstantRange pcRange{};
|
||||
pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
pcRange.offset = 0;
|
||||
pcRange.size = 80;
|
||||
selCirclePipelineLayout = createPipelineLayout(device, {}, {pcRange});
|
||||
|
||||
// Vertex input: binding 0, stride 12, vec3 at location 0
|
||||
VkVertexInputBindingDescription vertBind{0, 12, VK_VERTEX_INPUT_RATE_VERTEX};
|
||||
VkVertexInputAttributeDescription vertAttr{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0};
|
||||
|
||||
// Build disc geometry as TRIANGLE_LIST (replaces GL_TRIANGLE_FAN)
|
||||
// N=48 segments: center at origin + ring verts
|
||||
constexpr int SEGMENTS = 48;
|
||||
std::vector<float> verts;
|
||||
verts.reserve((SEGMENTS + 1) * 3);
|
||||
// Center vertex
|
||||
verts.insert(verts.end(), {0.0f, 0.0f, 0.0f});
|
||||
// Ring vertices
|
||||
for (int i = 0; i <= SEGMENTS; ++i) {
|
||||
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
|
||||
verts.push_back(std::cos(angle));
|
||||
verts.push_back(std::sin(angle));
|
||||
verts.push_back(0.0f);
|
||||
}
|
||||
|
||||
// Build TRIANGLE_LIST indices: N triangles (center=0, ring[i]=i+1, ring[i+1]=i+2)
|
||||
std::vector<uint16_t> indices;
|
||||
indices.reserve(SEGMENTS * 3);
|
||||
for (int i = 0; i < SEGMENTS; ++i) {
|
||||
indices.push_back(0);
|
||||
indices.push_back(static_cast<uint16_t>(i + 1));
|
||||
indices.push_back(static_cast<uint16_t>(i + 2));
|
||||
}
|
||||
selCircleVertCount = SEGMENTS * 3; // index count for drawing
|
||||
|
||||
// Upload vertex buffer
|
||||
AllocatedBuffer vbuf = uploadBuffer(*vkCtx, verts.data(),
|
||||
verts.size() * sizeof(float), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||
selCircleVertBuf = vbuf.buffer;
|
||||
selCircleVertAlloc = vbuf.allocation;
|
||||
|
||||
// Upload index buffer
|
||||
AllocatedBuffer ibuf = uploadBuffer(*vkCtx, indices.data(),
|
||||
indices.size() * sizeof(uint16_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||
selCircleIdxBuf = ibuf.buffer;
|
||||
selCircleIdxAlloc = ibuf.allocation;
|
||||
|
||||
// Build pipeline: alpha blend, no depth write/test, TRIANGLE_LIST, CULL_NONE
|
||||
selCirclePipeline = PipelineBuilder()
|
||||
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({vertBind}, {vertAttr})
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
||||
.setMultisample(vkCtx->getMsaaSamples())
|
||||
.setLayout(selCirclePipelineLayout)
|
||||
.setRenderPass(vkCtx->getImGuiRenderPass())
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device, vkCtx->getPipelineCache());
|
||||
|
||||
vertShader.destroy();
|
||||
fragShader.destroy();
|
||||
|
||||
if (!selCirclePipeline) {
|
||||
LOG_ERROR("initSelectionCircle: failed to build pipeline");
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
|
||||
selCirclePos = pos;
|
||||
selCircleRadius = radius;
|
||||
selCircleColor = color;
|
||||
selCircleVisible = true;
|
||||
if (overlaySystem_) overlaySystem_->setSelectionCircle(pos, radius, color);
|
||||
}
|
||||
|
||||
void Renderer::clearSelectionCircle() {
|
||||
selCircleVisible = false;
|
||||
}
|
||||
|
||||
void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection, VkCommandBuffer overrideCmd) {
|
||||
if (!selCircleVisible) return;
|
||||
initSelectionCircle();
|
||||
VkCommandBuffer cmd = (overrideCmd != VK_NULL_HANDLE) ? overrideCmd : currentCmd;
|
||||
if (selCirclePipeline == VK_NULL_HANDLE || cmd == VK_NULL_HANDLE) return;
|
||||
|
||||
// Keep circle anchored near target foot Z. Accept nearby floor probes only,
|
||||
// so distant upper/lower WMO planes don't yank the ring away from feet.
|
||||
const float baseZ = selCirclePos.z;
|
||||
float floorZ = baseZ;
|
||||
auto considerFloor = [&](std::optional<float> sample) {
|
||||
if (!sample) return;
|
||||
const float h = *sample;
|
||||
// Ignore unrelated floors/ceilings far from target feet.
|
||||
if (h < baseZ - 1.25f || h > baseZ + 0.85f) return;
|
||||
floorZ = std::max(floorZ, h);
|
||||
};
|
||||
|
||||
if (terrainManager) {
|
||||
considerFloor(terrainManager->getHeightAt(selCirclePos.x, selCirclePos.y));
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
considerFloor(wmoRenderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 3.0f));
|
||||
}
|
||||
if (m2Renderer) {
|
||||
considerFloor(m2Renderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 2.0f));
|
||||
}
|
||||
|
||||
glm::vec3 raisedPos = selCirclePos;
|
||||
raisedPos.z = floorZ + 0.17f;
|
||||
glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos);
|
||||
model = glm::scale(model, glm::vec3(selCircleRadius));
|
||||
|
||||
glm::mat4 mvp = projection * view * model;
|
||||
glm::vec4 color4(selCircleColor, 1.0f);
|
||||
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, selCirclePipeline);
|
||||
VkDeviceSize offset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &selCircleVertBuf, &offset);
|
||||
vkCmdBindIndexBuffer(cmd, selCircleIdxBuf, 0, VK_INDEX_TYPE_UINT16);
|
||||
// Push mvp (64 bytes) at offset 0
|
||||
vkCmdPushConstants(cmd, selCirclePipelineLayout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, 64, &mvp[0][0]);
|
||||
// Push color (16 bytes) at offset 64
|
||||
vkCmdPushConstants(cmd, selCirclePipelineLayout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
64, 16, &color4[0]);
|
||||
vkCmdDrawIndexed(cmd, static_cast<uint32_t>(selCircleVertCount), 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Fullscreen overlay pipeline (underwater tint, etc.)
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
void Renderer::initOverlayPipeline() {
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
|
||||
// Push constant: vec4 color (16 bytes), visible to both stages
|
||||
VkPushConstantRange pc{};
|
||||
pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
pc.offset = 0;
|
||||
pc.size = 16;
|
||||
|
||||
VkPipelineLayoutCreateInfo plCI{};
|
||||
plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
plCI.pushConstantRangeCount = 1;
|
||||
plCI.pPushConstantRanges = &pc;
|
||||
vkCreatePipelineLayout(device, &plCI, nullptr, &overlayPipelineLayout);
|
||||
|
||||
VkShaderModule vertMod, fragMod;
|
||||
if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") ||
|
||||
!fragMod.loadFromFile(device, "assets/shaders/overlay.frag.spv")) {
|
||||
LOG_ERROR("Renderer: failed to load overlay shaders");
|
||||
vertMod.destroy(); fragMod.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
overlayPipeline = PipelineBuilder()
|
||||
.setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({}, {}) // fullscreen triangle, no VBOs
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
||||
.setMultisample(vkCtx->getMsaaSamples())
|
||||
.setLayout(overlayPipelineLayout)
|
||||
.setRenderPass(vkCtx->getImGuiRenderPass())
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device, vkCtx->getPipelineCache());
|
||||
|
||||
vertMod.destroy(); fragMod.destroy();
|
||||
|
||||
if (overlayPipeline) LOG_INFO("Renderer: overlay pipeline initialized");
|
||||
}
|
||||
|
||||
void Renderer::renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd) {
|
||||
if (!overlayPipeline) initOverlayPipeline();
|
||||
VkCommandBuffer cmd = (overrideCmd != VK_NULL_HANDLE) ? overrideCmd : currentCmd;
|
||||
if (!overlayPipeline || cmd == VK_NULL_HANDLE) return;
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline);
|
||||
vkCmdPushConstants(cmd, overlayPipelineLayout,
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, &color[0]);
|
||||
vkCmdDraw(cmd, 3, 1, 0, 0); // fullscreen triangle
|
||||
if (overlaySystem_) overlaySystem_->clearSelectionCircle();
|
||||
}
|
||||
|
||||
// ========================= PostProcessPipeline delegation stubs (§4.3) =========================
|
||||
|
|
@ -1908,13 +1426,6 @@ PostProcessPipeline* Renderer::getPostProcessPipeline() const {
|
|||
return postProcessPipeline_.get();
|
||||
}
|
||||
|
||||
void Renderer::setFXAAEnabled(bool enabled) {
|
||||
if (postProcessPipeline_) postProcessPipeline_->setFXAAEnabled(enabled);
|
||||
}
|
||||
bool Renderer::isFXAAEnabled() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isFXAAEnabled();
|
||||
}
|
||||
|
||||
void Renderer::setFSREnabled(bool enabled) {
|
||||
if (!postProcessPipeline_) return;
|
||||
auto req = postProcessPipeline_->setFSREnabled(enabled);
|
||||
|
|
@ -1923,22 +1434,6 @@ void Renderer::setFSREnabled(bool enabled) {
|
|||
msaaChangePending_ = true;
|
||||
}
|
||||
}
|
||||
bool Renderer::isFSREnabled() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isFSREnabled();
|
||||
}
|
||||
void Renderer::setFSRQuality(float scaleFactor) {
|
||||
if (postProcessPipeline_) postProcessPipeline_->setFSRQuality(scaleFactor);
|
||||
}
|
||||
void Renderer::setFSRSharpness(float sharpness) {
|
||||
if (postProcessPipeline_) postProcessPipeline_->setFSRSharpness(sharpness);
|
||||
}
|
||||
float Renderer::getFSRScaleFactor() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getFSRScaleFactor() : 1.0f;
|
||||
}
|
||||
float Renderer::getFSRSharpness() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getFSRSharpness() : 0.0f;
|
||||
}
|
||||
|
||||
void Renderer::setFSR2Enabled(bool enabled) {
|
||||
if (!postProcessPipeline_) return;
|
||||
auto req = postProcessPipeline_->setFSR2Enabled(enabled, camera.get());
|
||||
|
|
@ -1952,63 +1447,6 @@ void Renderer::setFSR2Enabled(bool enabled) {
|
|||
pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
|
||||
}
|
||||
}
|
||||
bool Renderer::isFSR2Enabled() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isFSR2Enabled();
|
||||
}
|
||||
void Renderer::setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY) {
|
||||
if (postProcessPipeline_) postProcessPipeline_->setFSR2DebugTuning(jitterSign, motionVecScaleX, motionVecScaleY);
|
||||
}
|
||||
|
||||
void Renderer::setAmdFsr3FramegenEnabled(bool enabled) {
|
||||
if (postProcessPipeline_) postProcessPipeline_->setAmdFsr3FramegenEnabled(enabled);
|
||||
}
|
||||
bool Renderer::isAmdFsr3FramegenEnabled() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenEnabled();
|
||||
}
|
||||
float Renderer::getFSR2JitterSign() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getFSR2JitterSign() : 1.0f;
|
||||
}
|
||||
float Renderer::getFSR2MotionVecScaleX() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getFSR2MotionVecScaleX() : 1.0f;
|
||||
}
|
||||
float Renderer::getFSR2MotionVecScaleY() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getFSR2MotionVecScaleY() : 1.0f;
|
||||
}
|
||||
bool Renderer::isAmdFsr2SdkAvailable() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr2SdkAvailable();
|
||||
}
|
||||
bool Renderer::isAmdFsr3FramegenSdkAvailable() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenSdkAvailable();
|
||||
}
|
||||
bool Renderer::isAmdFsr3FramegenRuntimeActive() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenRuntimeActive();
|
||||
}
|
||||
bool Renderer::isAmdFsr3FramegenRuntimeReady() const {
|
||||
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenRuntimeReady();
|
||||
}
|
||||
const char* Renderer::getAmdFsr3FramegenRuntimePath() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenRuntimePath() : "";
|
||||
}
|
||||
const std::string& Renderer::getAmdFsr3FramegenRuntimeError() const {
|
||||
static const std::string empty;
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenRuntimeError() : empty;
|
||||
}
|
||||
size_t Renderer::getAmdFsr3UpscaleDispatchCount() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3UpscaleDispatchCount() : 0;
|
||||
}
|
||||
size_t Renderer::getAmdFsr3FramegenDispatchCount() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenDispatchCount() : 0;
|
||||
}
|
||||
size_t Renderer::getAmdFsr3FallbackCount() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FallbackCount() : 0;
|
||||
}
|
||||
void Renderer::setBrightness(float b) {
|
||||
if (postProcessPipeline_) postProcessPipeline_->setBrightness(b);
|
||||
}
|
||||
float Renderer::getBrightness() const {
|
||||
return postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f;
|
||||
}
|
||||
|
||||
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||
ZoneScopedN("Renderer::renderWorld");
|
||||
(void)world;
|
||||
|
|
@ -2132,7 +1570,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
{
|
||||
VkCommandBuffer cmd = beginSecondary(SEC_CHARS);
|
||||
setSecondaryViewportScissor(cmd);
|
||||
renderSelectionCircle(view, projection, cmd);
|
||||
if (overlaySystem_) {
|
||||
overlaySystem_->renderSelectionCircle(view, projection, cmd,
|
||||
terrainManager ? OverlaySystem::HeightQuery2D([&](float x, float y) { return terrainManager->getHeightAt(x, y); }) : OverlaySystem::HeightQuery2D{},
|
||||
wmoRenderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return wmoRenderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{},
|
||||
m2Renderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return m2Renderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{});
|
||||
}
|
||||
if (characterRenderer && camera && !skipChars) {
|
||||
characterRenderer->render(cmd, perFrameSet, *camera);
|
||||
}
|
||||
|
|
@ -2164,7 +1607,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
if (questMarkerRenderer && camera) questMarkerRenderer->render(cmd, perFrameSet, *camera);
|
||||
|
||||
// Underwater overlay + minimap
|
||||
if (overlayPipeline && waterRenderer && camera) {
|
||||
if (overlaySystem_ && waterRenderer && camera) {
|
||||
glm::vec3 camPos = camera->getPosition();
|
||||
auto waterH = waterRenderer->getNearestWaterHeightAt(camPos.x, camPos.y, camPos.z);
|
||||
constexpr float MIN_SUBMERSION_OVERLAY = 1.5f;
|
||||
|
|
@ -2179,21 +1622,21 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
glm::vec4 tint = canal
|
||||
? glm::vec4(0.01f, 0.04f, 0.10f, fogStrength)
|
||||
: glm::vec4(0.03f, 0.09f, 0.18f, fogStrength);
|
||||
renderOverlay(tint, cmd);
|
||||
if (overlaySystem_) overlaySystem_->renderOverlay(tint, cmd);
|
||||
}
|
||||
}
|
||||
// Ghost mode desaturation: cold blue-grey overlay when dead/ghost
|
||||
if (ghostMode_) {
|
||||
renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd);
|
||||
if (ghostMode_ && overlaySystem_) {
|
||||
overlaySystem_->renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd);
|
||||
}
|
||||
// Brightness overlay (applied before minimap so it doesn't affect UI)
|
||||
{
|
||||
if (overlaySystem_) {
|
||||
float br = postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f;
|
||||
if (br < 0.99f) {
|
||||
renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), cmd);
|
||||
overlaySystem_->renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), cmd);
|
||||
} else if (br > 1.01f) {
|
||||
float alpha = (br - 1.0f) / 1.0f;
|
||||
renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd);
|
||||
overlaySystem_->renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd);
|
||||
}
|
||||
}
|
||||
if (minimap && minimap->isEnabled() && camera && window) {
|
||||
|
|
@ -2277,7 +1720,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
std::chrono::steady_clock::now() - wmoStart).count();
|
||||
}
|
||||
|
||||
renderSelectionCircle(view, projection);
|
||||
if (overlaySystem_) {
|
||||
overlaySystem_->renderSelectionCircle(view, projection, currentCmd,
|
||||
terrainManager ? OverlaySystem::HeightQuery2D([&](float x, float y) { return terrainManager->getHeightAt(x, y); }) : OverlaySystem::HeightQuery2D{},
|
||||
wmoRenderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return wmoRenderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{},
|
||||
m2Renderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return m2Renderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{});
|
||||
}
|
||||
|
||||
if (characterRenderer && camera && !skipChars) {
|
||||
characterRenderer->prepareRender(frameIdx);
|
||||
|
|
@ -2312,7 +1760,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
// Underwater overlay and minimap — in the fallback path these run inline;
|
||||
// in the parallel path they were already recorded into SEC_POST above.
|
||||
if (!parallelRecordingEnabled_) {
|
||||
if (overlayPipeline && waterRenderer && camera) {
|
||||
if (overlaySystem_ && waterRenderer && camera) {
|
||||
glm::vec3 camPos = camera->getPosition();
|
||||
auto waterH = waterRenderer->getNearestWaterHeightAt(camPos.x, camPos.y, camPos.z);
|
||||
constexpr float MIN_SUBMERSION_OVERLAY = 1.5f;
|
||||
|
|
@ -2327,21 +1775,21 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
glm::vec4 tint = canal
|
||||
? glm::vec4(0.01f, 0.04f, 0.10f, fogStrength)
|
||||
: glm::vec4(0.03f, 0.09f, 0.18f, fogStrength);
|
||||
renderOverlay(tint);
|
||||
if (overlaySystem_) overlaySystem_->renderOverlay(tint, currentCmd);
|
||||
}
|
||||
}
|
||||
// Ghost mode desaturation: cold blue-grey overlay when dead/ghost
|
||||
if (ghostMode_) {
|
||||
renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f));
|
||||
if (ghostMode_ && overlaySystem_) {
|
||||
overlaySystem_->renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), currentCmd);
|
||||
}
|
||||
// Brightness overlay (applied before minimap so it doesn't affect UI)
|
||||
{
|
||||
if (overlaySystem_) {
|
||||
float br = postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f;
|
||||
if (br < 0.99f) {
|
||||
renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br));
|
||||
overlaySystem_->renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), currentCmd);
|
||||
} else if (br > 1.01f) {
|
||||
float alpha = (br - 1.0f) / 1.0f;
|
||||
renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha));
|
||||
overlaySystem_->renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), currentCmd);
|
||||
}
|
||||
}
|
||||
if (minimap && minimap->isEnabled() && camera && window) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
2803
src/ui/chat_panel_commands.cpp
Normal file
2803
src/ui/chat_panel_commands.cpp
Normal file
File diff suppressed because it is too large
Load diff
480
src/ui/chat_panel_utils.cpp
Normal file
480
src/ui/chat_panel_utils.cpp
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/ui_sound_manager.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include "core/coordinates.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
using namespace wowee::ui::colors;
|
||||
constexpr auto& kColorRed = kRed;
|
||||
constexpr auto& kColorBrightGreen= kBrightGreen;
|
||||
constexpr auto& kColorYellow = kYellow;
|
||||
} // namespace
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
const char* ChatPanel::getChatTypeName(game::ChatType type) const {
|
||||
switch (type) {
|
||||
case game::ChatType::SAY: return "Say";
|
||||
case game::ChatType::YELL: return "Yell";
|
||||
case game::ChatType::EMOTE: return "Emote";
|
||||
case game::ChatType::TEXT_EMOTE: return "Emote";
|
||||
case game::ChatType::PARTY: return "Party";
|
||||
case game::ChatType::GUILD: return "Guild";
|
||||
case game::ChatType::OFFICER: return "Officer";
|
||||
case game::ChatType::RAID: return "Raid";
|
||||
case game::ChatType::RAID_LEADER: return "Raid Leader";
|
||||
case game::ChatType::RAID_WARNING: return "Raid Warning";
|
||||
case game::ChatType::BATTLEGROUND: return "Battleground";
|
||||
case game::ChatType::BATTLEGROUND_LEADER: return "Battleground Leader";
|
||||
case game::ChatType::WHISPER: return "Whisper";
|
||||
case game::ChatType::WHISPER_INFORM: return "To";
|
||||
case game::ChatType::SYSTEM: return "System";
|
||||
case game::ChatType::MONSTER_SAY: return "Say";
|
||||
case game::ChatType::MONSTER_YELL: return "Yell";
|
||||
case game::ChatType::MONSTER_EMOTE: return "Emote";
|
||||
case game::ChatType::CHANNEL: return "Channel";
|
||||
case game::ChatType::ACHIEVEMENT: return "Achievement";
|
||||
case game::ChatType::DND: return "DND";
|
||||
case game::ChatType::AFK: return "AFK";
|
||||
case game::ChatType::BG_SYSTEM_NEUTRAL:
|
||||
case game::ChatType::BG_SYSTEM_ALLIANCE:
|
||||
case game::ChatType::BG_SYSTEM_HORDE: return "System";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ImVec4 ChatPanel::getChatTypeColor(game::ChatType type) const {
|
||||
switch (type) {
|
||||
case game::ChatType::SAY:
|
||||
return ui::colors::kWhite; // White
|
||||
case game::ChatType::YELL:
|
||||
return kColorRed; // Red
|
||||
case game::ChatType::EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
||||
case game::ChatType::TEXT_EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
||||
case game::ChatType::PARTY:
|
||||
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
|
||||
case game::ChatType::GUILD:
|
||||
return kColorBrightGreen; // Green
|
||||
case game::ChatType::OFFICER:
|
||||
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
|
||||
case game::ChatType::RAID:
|
||||
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
||||
case game::ChatType::RAID_LEADER:
|
||||
return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange
|
||||
case game::ChatType::RAID_WARNING:
|
||||
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
|
||||
case game::ChatType::BATTLEGROUND:
|
||||
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold
|
||||
case game::ChatType::BATTLEGROUND_LEADER:
|
||||
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
||||
case game::ChatType::WHISPER:
|
||||
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
||||
case game::ChatType::WHISPER_INFORM:
|
||||
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
||||
case game::ChatType::SYSTEM:
|
||||
return kColorYellow; // Yellow
|
||||
case game::ChatType::MONSTER_SAY:
|
||||
return ui::colors::kWhite; // White (same as SAY)
|
||||
case game::ChatType::MONSTER_YELL:
|
||||
return kColorRed; // Red (same as YELL)
|
||||
case game::ChatType::MONSTER_EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE)
|
||||
case game::ChatType::CHANNEL:
|
||||
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
|
||||
case game::ChatType::ACHIEVEMENT:
|
||||
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
|
||||
case game::ChatType::GUILD_ACHIEVEMENT:
|
||||
return colors::kWarmGold; // Gold
|
||||
case game::ChatType::SKILL:
|
||||
return colors::kCyan; // Cyan
|
||||
case game::ChatType::LOOT:
|
||||
return ImVec4(0.8f, 0.5f, 1.0f, 1.0f); // Light purple
|
||||
case game::ChatType::MONSTER_WHISPER:
|
||||
case game::ChatType::RAID_BOSS_WHISPER:
|
||||
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink (same as WHISPER)
|
||||
case game::ChatType::RAID_BOSS_EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE)
|
||||
case game::ChatType::MONSTER_PARTY:
|
||||
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue (same as PARTY)
|
||||
case game::ChatType::BG_SYSTEM_NEUTRAL:
|
||||
return colors::kWarmGold; // Gold
|
||||
case game::ChatType::BG_SYSTEM_ALLIANCE:
|
||||
return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue
|
||||
case game::ChatType::BG_SYSTEM_HORDE:
|
||||
return kColorRed; // Red
|
||||
case game::ChatType::AFK:
|
||||
case game::ChatType::DND:
|
||||
return ImVec4(0.85f, 0.85f, 0.85f, 0.8f); // Light gray
|
||||
default:
|
||||
return ui::colors::kLightGray; // Gray
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
||||
// Get player gender, pronouns, and name
|
||||
game::Gender gender = game::Gender::NONBINARY;
|
||||
std::string playerName = "Adventurer";
|
||||
const auto* character = gameHandler.getActiveCharacter();
|
||||
if (character) {
|
||||
gender = character->gender;
|
||||
if (!character->name.empty()) {
|
||||
playerName = character->name;
|
||||
}
|
||||
}
|
||||
game::Pronouns pronouns = game::Pronouns::forGender(gender);
|
||||
|
||||
std::string result = text;
|
||||
|
||||
// Helper to trim whitespace
|
||||
auto trim = [](std::string& s) {
|
||||
const char* ws = " \t\n\r";
|
||||
size_t start = s.find_first_not_of(ws);
|
||||
if (start == std::string::npos) { s.clear(); return; }
|
||||
size_t end = s.find_last_not_of(ws);
|
||||
s = s.substr(start, end - start + 1);
|
||||
};
|
||||
|
||||
// Replace $g/$G placeholders first.
|
||||
size_t pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
char marker = result[pos + 1];
|
||||
if (marker != 'g' && marker != 'G') { pos++; continue; }
|
||||
|
||||
size_t endPos = result.find(';', pos);
|
||||
if (endPos == std::string::npos) { pos += 2; continue; }
|
||||
|
||||
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
||||
|
||||
// Split by colons
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
size_t colonPos;
|
||||
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
|
||||
std::string part = placeholder.substr(start, colonPos - start);
|
||||
trim(part);
|
||||
parts.push_back(part);
|
||||
start = colonPos + 1;
|
||||
}
|
||||
// Add the last part
|
||||
std::string lastPart = placeholder.substr(start);
|
||||
trim(lastPart);
|
||||
parts.push_back(lastPart);
|
||||
|
||||
// Select appropriate text based on gender
|
||||
std::string replacement;
|
||||
if (parts.size() >= 3) {
|
||||
// Three options: male, female, nonbinary
|
||||
switch (gender) {
|
||||
case game::Gender::MALE:
|
||||
replacement = parts[0];
|
||||
break;
|
||||
case game::Gender::FEMALE:
|
||||
replacement = parts[1];
|
||||
break;
|
||||
case game::Gender::NONBINARY:
|
||||
replacement = parts[2];
|
||||
break;
|
||||
}
|
||||
} else if (parts.size() >= 2) {
|
||||
// Two options: male, female (use first for nonbinary)
|
||||
switch (gender) {
|
||||
case game::Gender::MALE:
|
||||
replacement = parts[0];
|
||||
break;
|
||||
case game::Gender::FEMALE:
|
||||
replacement = parts[1];
|
||||
break;
|
||||
case game::Gender::NONBINARY:
|
||||
// Default to gender-neutral: use the shorter/simpler option
|
||||
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Malformed placeholder
|
||||
pos = endPos + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.replace(pos, endPos - pos + 1, replacement);
|
||||
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
|
||||
// $p = subject pronoun (he/she/they)
|
||||
// $o = object pronoun (him/her/them)
|
||||
// $s = possessive adjective (his/her/their)
|
||||
// $S = possessive pronoun (his/hers/theirs)
|
||||
// $b/$B = line break
|
||||
pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
|
||||
char code = result[pos + 1];
|
||||
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;
|
||||
case 'S': replacement = pronouns.possessiveP; break;
|
||||
case 'b': case 'B': replacement = "\n"; break;
|
||||
case 'g': case 'G': pos++; continue;
|
||||
default: pos++; continue;
|
||||
}
|
||||
|
||||
result.replace(pos, 2, replacement);
|
||||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// WoW markup linebreak token.
|
||||
pos = 0;
|
||||
while ((pos = result.find("|n", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
pos = 0;
|
||||
while ((pos = result.find("|N", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChatPanel::renderBubbles(game::GameHandler& gameHandler) {
|
||||
if (chatBubbles_.empty()) return;
|
||||
|
||||
auto* renderer = services_.renderer;
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
if (!camera) return;
|
||||
|
||||
auto* window = services_.window;
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
// Get delta time from ImGui
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
|
||||
glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
||||
|
||||
// Update and render bubbles
|
||||
for (int i = static_cast<int>(chatBubbles_.size()) - 1; i >= 0; --i) {
|
||||
auto& bubble = chatBubbles_[i];
|
||||
bubble.timeRemaining -= dt;
|
||||
if (bubble.timeRemaining <= 0.0f) {
|
||||
chatBubbles_.erase(chatBubbles_.begin() + i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get entity position
|
||||
auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid);
|
||||
if (!entity) continue;
|
||||
|
||||
// Convert canonical → render coordinates, offset up by 2.5 units for bubble above head
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f);
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Project to screen
|
||||
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
||||
if (clipPos.w <= 0.0f) continue; // Behind camera
|
||||
|
||||
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
||||
float screenX = (ndc.x * 0.5f + 0.5f) * screenW;
|
||||
// Camera bakes the Vulkan Y-flip into the projection matrix:
|
||||
// NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection.
|
||||
float screenY = (ndc.y * 0.5f + 0.5f) * screenH;
|
||||
|
||||
// Skip if off-screen
|
||||
if (screenX < -200.0f || screenX > screenW + 200.0f ||
|
||||
screenY < -100.0f || screenY > screenH + 100.0f) continue;
|
||||
|
||||
// Fade alpha over last 2 seconds
|
||||
float alpha = 1.0f;
|
||||
if (bubble.timeRemaining < 2.0f) {
|
||||
alpha = bubble.timeRemaining / 2.0f;
|
||||
}
|
||||
|
||||
// Draw bubble window
|
||||
std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid);
|
||||
ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
|
||||
ImGui::SetNextWindowBgAlpha(0.7f * alpha);
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
|
||||
|
||||
ImGui::Begin(winId.c_str(), nullptr, flags);
|
||||
|
||||
ImVec4 textColor = bubble.isYell
|
||||
? ImVec4(1.0f, 0.2f, 0.2f, alpha)
|
||||
: ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
|
||||
ImGui::PushTextWrapPos(200.0f);
|
||||
ImGui::TextWrapped("%s", bubble.message.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---- Public interface methods ----
|
||||
|
||||
void ChatPanel::setupCallbacks(game::GameHandler& gameHandler) {
|
||||
if (!chatBubbleCallbackSet_) {
|
||||
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
|
||||
float duration = 8.0f + static_cast<float>(msg.size()) * 0.06f;
|
||||
if (isYell) duration += 2.0f;
|
||||
if (duration > 15.0f) duration = 15.0f;
|
||||
|
||||
// Replace existing bubble for same sender
|
||||
for (auto& b : chatBubbles_) {
|
||||
if (b.senderGuid == guid) {
|
||||
b.message = msg;
|
||||
b.timeRemaining = duration;
|
||||
b.totalDuration = duration;
|
||||
b.isYell = isYell;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Evict oldest if too many
|
||||
if (chatBubbles_.size() >= 10) {
|
||||
chatBubbles_.erase(chatBubbles_.begin());
|
||||
}
|
||||
chatBubbles_.push_back({guid, msg, duration, duration, isYell});
|
||||
});
|
||||
chatBubbleCallbackSet_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPanel::insertChatLink(const std::string& link) {
|
||||
if (link.empty()) return;
|
||||
size_t curLen = strlen(chatInputBuffer_);
|
||||
if (curLen + link.size() + 1 < sizeof(chatInputBuffer_)) {
|
||||
strncat(chatInputBuffer_, link.c_str(), sizeof(chatInputBuffer_) - curLen - 1);
|
||||
chatInputMoveCursorToEnd_ = true;
|
||||
refocusChatInput_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPanel::activateSlashInput() {
|
||||
refocusChatInput_ = true;
|
||||
chatInputBuffer_[0] = '/';
|
||||
chatInputBuffer_[1] = '\0';
|
||||
chatInputMoveCursorToEnd_ = true;
|
||||
}
|
||||
|
||||
void ChatPanel::activateInput() {
|
||||
refocusChatInput_ = true;
|
||||
}
|
||||
|
||||
void ChatPanel::setWhisperTarget(const std::string& name) {
|
||||
selectedChatType_ = 4; // WHISPER
|
||||
strncpy(whisperTargetBuffer_, name.c_str(), sizeof(whisperTargetBuffer_) - 1);
|
||||
whisperTargetBuffer_[sizeof(whisperTargetBuffer_) - 1] = '\0';
|
||||
refocusChatInput_ = true;
|
||||
}
|
||||
|
||||
ChatPanel::SlashCommands ChatPanel::consumeSlashCommands() {
|
||||
SlashCommands result = slashCmds_;
|
||||
slashCmds_ = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChatPanel::renderSettingsTab(std::function<void()> saveSettingsFn) {
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Appearance");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps)) {
|
||||
saveSettingsFn();
|
||||
}
|
||||
ImGui::SetItemTooltip("Show [HH:MM] before each chat message");
|
||||
|
||||
const char* fontSizes[] = { "Small", "Medium", "Large" };
|
||||
if (ImGui::Combo("Chat Font Size", &chatFontSize, fontSizes, 3)) {
|
||||
saveSettingsFn();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Auto-Join Channels");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Checkbox("General", &chatAutoJoinGeneral)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("Trade", &chatAutoJoinTrade)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("Local", &chatAutoJoinLocal)) saveSettingsFn();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Joined Channels");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels.");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) {
|
||||
restoreDefaults();
|
||||
saveSettingsFn();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPanel::restoreDefaults() {
|
||||
chatShowTimestamps = false;
|
||||
chatFontSize = 1;
|
||||
chatAutoJoinGeneral = true;
|
||||
chatAutoJoinTrade = true;
|
||||
chatAutoJoinLocalDefense = true;
|
||||
chatAutoJoinLFG = true;
|
||||
chatAutoJoinLocal = true;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
File diff suppressed because it is too large
Load diff
2432
src/ui/game_screen_frames.cpp
Normal file
2432
src/ui/game_screen_frames.cpp
Normal file
File diff suppressed because it is too large
Load diff
1719
src/ui/game_screen_hud.cpp
Normal file
1719
src/ui/game_screen_hud.cpp
Normal file
File diff suppressed because it is too large
Load diff
1918
src/ui/game_screen_minimap.cpp
Normal file
1918
src/ui/game_screen_minimap.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@
|
|||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/post_process_pipeline.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/minimap.hpp"
|
||||
|
|
@ -739,7 +740,7 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP
|
|||
}
|
||||
{
|
||||
const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" };
|
||||
bool fsr2Active = renderer && renderer->isFSR2Enabled();
|
||||
bool fsr2Active = renderer && renderer->getPostProcessPipeline()->isFSR2Enabled();
|
||||
if (fsr2Active) {
|
||||
ImGui::BeginDisabled();
|
||||
int disabled = 0;
|
||||
|
|
@ -757,7 +758,7 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP
|
|||
// FXAA — post-process, combinable with MSAA or FSR3
|
||||
{
|
||||
if (ImGui::Checkbox("FXAA (post-process)", &pendingFXAA)) {
|
||||
if (renderer) renderer->setFXAAEnabled(pendingFXAA);
|
||||
if (renderer) renderer->getPostProcessPipeline()->setFXAAEnabled(pendingFXAA);
|
||||
updateGraphicsPresetFromCurrentSettings();
|
||||
saveCallback();
|
||||
}
|
||||
|
|
@ -786,24 +787,24 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP
|
|||
if (fsrMode > 0) {
|
||||
if (fsrMode == 2 && renderer) {
|
||||
ImGui::TextDisabled("FSR3 backend: %s",
|
||||
renderer->isAmdFsr2SdkAvailable() ? "AMD FidelityFX SDK" : "Internal fallback");
|
||||
if (renderer->isAmdFsr3FramegenSdkAvailable()) {
|
||||
renderer->getPostProcessPipeline()->isAmdFsr2SdkAvailable() ? "AMD FidelityFX SDK" : "Internal fallback");
|
||||
if (renderer->getPostProcessPipeline()->isAmdFsr3FramegenSdkAvailable()) {
|
||||
if (ImGui::Checkbox("AMD FSR3 Frame Generation (Experimental)", &pendingAMDFramegen)) {
|
||||
renderer->setAmdFsr3FramegenEnabled(pendingAMDFramegen);
|
||||
renderer->getPostProcessPipeline()->setAmdFsr3FramegenEnabled(pendingAMDFramegen);
|
||||
saveCallback();
|
||||
}
|
||||
const char* runtimeStatus = "Unavailable";
|
||||
if (renderer->isAmdFsr3FramegenRuntimeActive()) {
|
||||
if (renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeActive()) {
|
||||
runtimeStatus = "Active";
|
||||
} else if (renderer->isAmdFsr3FramegenRuntimeReady()) {
|
||||
} else if (renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeReady()) {
|
||||
runtimeStatus = "Ready";
|
||||
} else {
|
||||
runtimeStatus = "Unavailable";
|
||||
}
|
||||
ImGui::TextDisabled("Runtime: %s (%s)",
|
||||
runtimeStatus, renderer->getAmdFsr3FramegenRuntimePath());
|
||||
if (!renderer->isAmdFsr3FramegenRuntimeReady()) {
|
||||
const std::string& runtimeErr = renderer->getAmdFsr3FramegenRuntimeError();
|
||||
runtimeStatus, renderer->getPostProcessPipeline()->getAmdFsr3FramegenRuntimePath());
|
||||
if (!renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeReady()) {
|
||||
const std::string& runtimeErr = renderer->getPostProcessPipeline()->getAmdFsr3FramegenRuntimeError();
|
||||
if (!runtimeErr.empty()) {
|
||||
ImGui::TextDisabled("Reason: %s", runtimeErr.c_str());
|
||||
}
|
||||
|
|
@ -829,18 +830,18 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP
|
|||
}
|
||||
if (ImGui::Combo("FSR Quality", &fsrQualityDisplay, fsrQualityLabels, 4)) {
|
||||
pendingFSRQuality = displayToInternal[fsrQualityDisplay];
|
||||
if (renderer) renderer->setFSRQuality(fsrScaleFactors[pendingFSRQuality]);
|
||||
if (renderer) renderer->getPostProcessPipeline()->setFSRQuality(fsrScaleFactors[pendingFSRQuality]);
|
||||
saveCallback();
|
||||
}
|
||||
if (ImGui::SliderFloat("FSR Sharpness", &pendingFSRSharpness, 0.0f, 2.0f, "%.1f")) {
|
||||
if (renderer) renderer->setFSRSharpness(pendingFSRSharpness);
|
||||
if (renderer) renderer->getPostProcessPipeline()->setFSRSharpness(pendingFSRSharpness);
|
||||
saveCallback();
|
||||
}
|
||||
if (fsrMode == 2) {
|
||||
ImGui::SeparatorText("FSR3 Tuning");
|
||||
if (ImGui::SliderFloat("Jitter Sign", &pendingFSR2JitterSign, -2.0f, 2.0f, "%.2f")) {
|
||||
if (renderer) {
|
||||
renderer->setFSR2DebugTuning(
|
||||
renderer->getPostProcessPipeline()->setFSR2DebugTuning(
|
||||
pendingFSR2JitterSign,
|
||||
pendingFSR2MotionVecScaleX,
|
||||
pendingFSR2MotionVecScaleY);
|
||||
|
|
@ -927,7 +928,7 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP
|
|||
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
if (ImGui::SliderInt("Brightness", &pendingBrightness, 0, 100, "%d%%")) {
|
||||
if (renderer) renderer->setBrightness(static_cast<float>(pendingBrightness) / 50.0f);
|
||||
if (renderer) renderer->getPostProcessPipeline()->setBrightness(static_cast<float>(pendingBrightness) / 50.0f);
|
||||
saveCallback();
|
||||
}
|
||||
|
||||
|
|
@ -951,7 +952,7 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP
|
|||
window->setFullscreen(pendingFullscreen);
|
||||
window->setVsync(pendingVsync);
|
||||
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
||||
if (renderer) renderer->setBrightness(1.0f);
|
||||
if (renderer) renderer->getPostProcessPipeline()->setBrightness(1.0f);
|
||||
pendingWaterRefraction = false;
|
||||
if (renderer) {
|
||||
renderer->setShadowsEnabled(pendingShadows);
|
||||
|
|
@ -1150,7 +1151,7 @@ void SettingsPanel::applyGraphicsPreset(GraphicsPreset preset) {
|
|||
renderer->setShadowsEnabled(true);
|
||||
renderer->setShadowDistance(500.0f);
|
||||
renderer->setMsaaSamples(VK_SAMPLE_COUNT_8_BIT);
|
||||
renderer->setFXAAEnabled(true);
|
||||
renderer->getPostProcessPipeline()->setFXAAEnabled(true);
|
||||
if (auto* wr = renderer->getWMORenderer()) {
|
||||
wr->setNormalMappingEnabled(true);
|
||||
wr->setNormalMapStrength(1.2f);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/ui_sound_manager.hpp"
|
||||
|
||||
|
|
@ -469,7 +470,7 @@ void ToastManager::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t man
|
|||
}
|
||||
}
|
||||
if (auto* renderer = services_.renderer) {
|
||||
renderer->playEmote("cheer");
|
||||
if (auto* ac = renderer->getAnimationController()) ac->playEmote("cheer");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue