Compare commits

..

54 commits

Author SHA1 Message Date
Kelsi
22798d1c76 feat: fire MAIL_SHOW/CLOSED and AUCTION_HOUSE_SHOW/CLOSED events
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Fire MAIL_SHOW when mailbox opens (SMSG_SHOW_MAILBOX) and MAIL_CLOSED
when it closes. Fire AUCTION_HOUSE_SHOW when AH opens and AUCTION_HOUSE_CLOSED
when it closes. Used by mail addons (Postal) and AH addons (Auctionator).
2026-03-20 22:43:29 -07:00
Kelsi
395d6cdcba feat: fire BANKFRAME_OPENED and BANKFRAME_CLOSED events for bank addons
Fire BANKFRAME_OPENED when bank window opens and BANKFRAME_CLOSED when
it closes. Used by bank management addons (Bagnon, BankItems) to detect
when the player is interacting with their bank.
2026-03-20 22:32:21 -07:00
Kelsi
8cc90a69e8 feat: fire MERCHANT_SHOW and MERCHANT_CLOSED events for vendor addons
Fire MERCHANT_SHOW when vendor window opens (SMSG_LIST_INVENTORY) and
MERCHANT_CLOSED when vendor is closed. Used by vendor price addons and
auto-sell addons that need to detect vendor interaction state.
2026-03-20 22:22:36 -07:00
Kelsi
37a5b4c9d9 feat: fire ACTIONBAR_SLOT_CHANGED event on action bar updates
Fire when SMSG_ACTION_BUTTONS populates the action bar on login and when
SMSG_SUPERCEDED_SPELL upgrades spell ranks on the bar. Used by action bar
addons (Bartender, Dominos) to refresh their displays.
2026-03-20 22:13:57 -07:00
Kelsi
fc182f8653 feat: fire SKILL_LINES_CHANGED event when player skills update
Detect changes in player skill values after extractSkillFields() and fire
SKILL_LINES_CHANGED when any skill value changes. Used by profession
tracking addons and skill bar displays.
2026-03-20 21:57:27 -07:00
Kelsi
d68ef2ceb6 feat: fire CHAT_MSG_MONEY and CHAT_MSG_COMBAT_XP_GAIN events
Fire CHAT_MSG_MONEY when gold is looted (used by gold tracking addons
like MoneyFu, Titan Panel). Fire CHAT_MSG_COMBAT_XP_GAIN when XP is
earned (used by XP tracking addons and leveling speed calculators).
2026-03-20 21:47:39 -07:00
Kelsi
44d2b80998 feat: fire CHAT_MSG_LOOT event when items are looted
Fire CHAT_MSG_LOOT addon event from SMSG_ITEM_PUSH_RESULT with the loot
message text, item ID, and count. Used by loot tracking addons (AutoLootPlus,
Loot Appraiser) and damage meters that track loot distribution.
2026-03-20 21:27:04 -07:00
Kelsi
0885f885e8 feat: fire PLAYER_FLAGS_CHANGED event when player flags update
Fires when AFK/DND status, PvP flag, ghost state, or other player flags
change via PLAYER_FLAGS update field. Enables addons that track player
status changes (FlagRSP, TRP3, etc.).
2026-03-20 21:18:25 -07:00
Kelsi
3dcd489e81 feat: show random suffix names in auction owner sold/expired notifications
Parse randomPropertyId from SMSG_AUCTION_OWNER_NOTIFICATION to display
full item names in sold/bid/expired messages like "Your auction of
Gloves of the Monkey has sold!" Completes suffix resolution across
all 9 item display contexts.
2026-03-20 21:02:12 -07:00
Kelsi
9f49f543f6 feat: show random suffix names in auction outbid and expired notifications
Apply getRandomPropertyName() to SMSG_AUCTION_BIDDER_NOTIFICATION and
SMSG_AUCTION_REMOVED_NOTIFICATION so outbid/expired messages show full
item names like "You have been outbid on Leggings of the Eagle" instead
of just "Leggings". Completes suffix name resolution across all AH contexts.
2026-03-20 20:57:23 -07:00
Kelsi
df55242c50 feat: add GetCoinTextureString/GetCoinText Lua money formatting utility
Formats copper amounts into "Xg Ys Zc" strings for addon display.
GetCoinText is aliased to GetCoinTextureString. Used by money display
addons (Titan Panel, MoneyFu) and auction/vendor price formatting.
2026-03-20 20:44:59 -07:00
Kelsi
b659ab9caf feat: add GetPlayerInfoByGUID Lua API for damage meter player identification
Returns (class, englishClass, race, englishRace, sex, name, realm) for a
GUID string. Resolves player name from entity cache. Returns class/race
info for the local player. Used by Details!, Recount, and Skada to
identify players in COMBAT_LOG_EVENT_UNFILTERED data.
2026-03-20 20:22:15 -07:00
Kelsi
8be8d31b85 feat: add GetItemLink Lua API for clickable item links from item IDs
Returns WoW-format quality-colored item link for any item ID from the
item info cache. Used by loot addons, tooltip addons, and chat formatting
to create clickable item references.
2026-03-20 20:07:45 -07:00
Kelsi
6d2a94a844 feat: add GetSpellLink Lua API for clickable spell links in chat
Returns WoW-format spell link string "|cff71d5ff|Hspell:ID|h[Name]|h|r"
for a spell ID or name. Used by damage meters, chat addons, and WeakAuras
to create clickable spell references in chat messages.
2026-03-20 19:57:13 -07:00
Kelsi
bc4ff501e2 feat: show random suffix names in AH bids and seller auction tabs
Extend random property name resolution to the Bids tab and Your Auctions
(seller) tab. All three auction house tabs now display items with their
full suffix names (e.g., "Gloves of the Monkey" instead of "Gloves").
2026-03-20 19:37:17 -07:00
Kelsi
a13dfff9a1 feat: show random suffix names in auction house item listings
Append suffix name from getRandomPropertyName() to auction browse results
so items display as "Leggings of the Eagle" instead of just "Leggings"
in the auction house search table. Uses the randomPropertyId field from
the SMSG_AUCTION_LIST_RESULT packet data.
2026-03-20 19:33:01 -07:00
Kelsi
99f4ded3b5 feat: show random suffix names in loot roll popup and roll-won messages
Apply getRandomPropertyName() to SMSG_LOOT_START_ROLL and SMSG_LOOT_ROLL_WON
handlers so items with random suffixes display correctly in group loot
contexts (e.g., "Leggings of the Eagle" in the Need/Greed popup and
"Player wins Leggings of the Eagle (Need 85)" in chat).
2026-03-20 19:22:59 -07:00
Kelsi
4b3e377add feat: resolve random property/suffix names for item display
Load ItemRandomProperties.dbc and ItemRandomSuffix.dbc lazily to resolve
suffix names like "of the Eagle", "of the Monkey" etc. Add
getRandomPropertyName(id) callback on GameHandler wired through Application.
Append suffix to item names in SMSG_ITEM_PUSH_RESULT loot notifications
so items display as "Leggings of the Eagle" instead of just "Leggings".
2026-03-20 19:18:30 -07:00
Kelsi
23a7d3718c fix: return WoW-standard (start, duration, enabled) from GetSpellCooldown
Previously returned (0, remaining) which broke addons computing remaining
time as start + duration - GetTime(). Now returns (GetTime(), remaining, 1)
when on cooldown and (0, 0, 1) when off cooldown, plus the third 'enabled'
value that WoW always returns. Fixes cooldown display in OmniCC and similar.
2026-03-20 19:03:34 -07:00
Kelsi
3a4d59d584 feat: add GetXPExhaustion and GetRestState Lua APIs for rested XP tracking
GetXPExhaustion() returns rested XP pool remaining (nil if none).
GetRestState() returns 1 (normal) or 2 (rested) based on inn/city state.
Used by XP bar addons like Titan Panel and XP tracking WeakAuras.
2026-03-20 18:53:56 -07:00
Kelsi
2b99011cd8 fix: cap gossipPois_ vector growth and add soft frame rate limiter
Cap gossipPois_ at 200 entries (both gossip POI and quest POI paths) to
prevent unbounded memory growth from rapid gossip/quest queries. Add soft
240 FPS frame rate limiter when vsync is off to prevent 100% CPU usage —
sleeps for remaining frame budget when frame completes in under 4ms.
2026-03-20 18:51:05 -07:00
Kelsi
4bd237b654 feat: add IsUsableSpell Lua API for spell usability checks
Returns (usable, noMana) tuple. Checks if the spell is known and not on
cooldown. Accepts spell ID or name. Used by action bar addons and
WeakAuras for conditional spell display (greyed out when unusable).
2026-03-20 18:42:33 -07:00
Kelsi
ce128990d2 feat: add IsInInstance, GetInstanceInfo, and GetInstanceDifficulty Lua APIs
IsInInstance() returns whether player is in an instance and the type.
GetInstanceInfo() returns map name, instance type, difficulty index/name,
and max players. GetInstanceDifficulty() returns 1-based difficulty index.
Critical for raid/dungeon addons like DBM for instance detection.
2026-03-20 18:33:44 -07:00
Kelsi
2a9a7fe04e feat: add UnitClassification Lua API for nameplate and boss mod addons
Returns WoW-standard classification strings: "normal", "elite", "rareelite",
"worldboss", or "rare" based on creature rank from CreatureCache. Used by
nameplate addons (Plater, TidyPlates) and boss mods (DBM) to detect elite/
boss/rare mobs for special handling.
2026-03-20 18:25:39 -07:00
Kelsi
180990b9f1 feat: play minimap ping sound when party members ping the map
Add playMinimapPing() to UiSoundManager with MapPing.wav (falls back to
target select sound). Play the ping sound in MSG_MINIMAP_PING handler
when the sender is not the local player. Provides audio feedback for
party member map pings, matching WoW behavior.
2026-03-20 18:21:34 -07:00
Kelsi
f03ed8551b feat: add GetGameTime, GetServerTime, UnitXP, and UnitXPMax Lua APIs
GetGameTime() returns server game hours and minutes from the day/night
cycle. GetServerTime() returns Unix timestamp. UnitXP("player") and
UnitXPMax("player") return current and next-level XP values. Used by
XP tracking addons and time-based conditionals.
2026-03-20 18:16:12 -07:00
Kelsi
71837ade19 feat: show zone name on loading screen during world transitions
Add setZoneName() to LoadingScreen and display the map name from Map.dbc
as large gold text with drop shadow above the progress bar. Shown in both
render() and renderOverlay() paths. Zone name is resolved from gameHandler's
getMapName(mapId) during world load. Improves feedback during zone transitions.
2026-03-20 18:12:23 -07:00
Kelsi
ff1840415e fix: invoke despawn callbacks on disconnect to prevent renderer leaks
Mirror the zone-transition cleanup in disconnect(): fire despawn callbacks
for all entities before clearing the entity manager. Prevents M2 instances
and character models from leaking when the player disconnects and reconnects
quickly (e.g., server kick, network recovery).
2026-03-20 18:07:00 -07:00
Kelsi
922177abe0 fix: invoke despawn callbacks during zone transitions to release renderer resources
handleNewWorld() previously called entityManager.clear() directly without
notifying the renderer, leaving stale M2 instances and character models
allocated. Now iterates all entities and fires creatureDespawnCallback,
playerDespawnCallback, and gameObjectDespawnCallback before clearing.
Also clears player caches (visible items, cast states, aura cache,
combat text) to prevent state leaking between zones.
2026-03-20 18:05:09 -07:00
Kelsi
1d7eaaf2a0 fix: compute aura expirationTime for addon countdown timers
The expirationTime field (7th return value of UnitBuff/UnitDebuff/UnitAura)
was hardcoded to 0. Now returns GetTime() + remaining seconds, matching
WoW's convention where addons compute remaining = expirationTime - GetTime().
Enables buff/debuff timer addons like OmniCC and WeakAuras.
2026-03-20 18:00:57 -07:00
Kelsi
5adb9370d2 fix: return caster unit ID from UnitBuff/UnitDebuff/UnitAura
The caster field (8th return value) was always nil. Now returns the
caster's unit ID ("player", "target", "focus", "pet") or hex GUID
string for other units. Enables addons to identify who applied a
buff/debuff for filtering and tracking purposes.
2026-03-20 17:58:53 -07:00
Kelsi
ffe16f5cf2 feat: add equipment slot Lua API for gear inspection addons
Add GetInventoryItemLink(unit, slotId), GetInventoryItemID(unit, slotId),
and GetInventoryItemTexture(unit, slotId) for WoW inventory slots 1-19
(Head through Tabard). Returns quality-colored item links with WoW format.
Enables gear inspection and item level calculation addons.
2026-03-20 17:56:20 -07:00
Kelsi
3f0b152fe9 fix: return debuff type string from UnitBuff/UnitDebuff/UnitAura
The debuffType field (5th return value) was always nil. Now resolves
dispel type from Spell.dbc via getSpellDispelType(): returns "Magic",
"Curse", "Disease", or "Poison" for debuffs. Enables dispel-focused
addons like Decursive and Grid to detect debuff categories.
2026-03-20 17:53:01 -07:00
Kelsi
7c5bec50ef fix: increase world packet size limit from 16KB to 32KB
The 0x4000 (16384) limit was too conservative and could disconnect the
client when the server sends large packets such as SMSG_GUILD_ROSTER
with 500+ members (~30KB) or SMSG_AUCTION_LIST with many results.
Increase to 0x8000 (32768) which covers all normal gameplay while still
protecting against framing desync from encryption errors.
2026-03-20 17:49:49 -07:00
Kelsi
f712d3de94 feat: add quest log Lua API for quest tracking addons
Add GetNumQuestLogEntries(), GetQuestLogTitle(index), GetQuestLogQuestText(index),
and IsQuestComplete(questID). GetQuestLogTitle returns WoW-compatible 8 values
including title, isComplete flag, and questID. Enables quest tracking addons
like Questie and QuestHelper to access the player's quest log.
2026-03-20 17:37:35 -07:00
Kelsi
ee59c37b83 feat: add loot method change notifications and CRITERIA_UPDATE event
Show "Loot method changed to Master Looter/Round Robin/etc." in chat when
group loot method changes via SMSG_GROUP_LIST. Fire CRITERIA_UPDATE addon
event with criteria ID and progress when achievement criteria progress
changes, enabling achievement tracking addons.
2026-03-20 17:33:34 -07:00
Kelsi
c44e1bde0a feat: fire UPDATE_FACTION, QUEST_ACCEPTED, and QUEST_LOG_UPDATE events
Fire UPDATE_FACTION when reputation standings change (SMSG_SET_FACTION_STANDING).
Fire QUEST_ACCEPTED with quest ID when a new quest is added to the log.
Fire QUEST_LOG_UPDATE on both quest acceptance and quest completion.
Enables reputation tracking and quest log addons.
2026-03-20 17:28:28 -07:00
Kelsi
14007c81df feat: add /cancelqueuedspell command to clear spell queue
Add cancelQueuedSpell() method that clears queuedSpellId_ and
queuedSpellTarget_. Wire /cancelqueuedspell and /stopspellqueue
slash commands. Useful for combat macros that need to prevent
queued spells from firing after a current cast.
2026-03-20 17:24:16 -07:00
Kelsi
8761ad9301 fix: clean up combat text, cast bars, and aura cache on entity destroy
When SMSG_DESTROY_OBJECT removes an entity, now also purge combat text
entries targeting that GUID (prevents floating damage numbers on despawned
mobs), erase unit cast state (prevents stale cast bars), and clear cached
auras (prevents stale buff/debuff data for destroyed units).
2026-03-20 17:19:18 -07:00
Kelsi
0f480f5ada feat: add container/bag Lua API for bag addon support
Add GetContainerNumSlots(bag), GetContainerItemInfo(bag, slot),
GetContainerItemLink(bag, slot), and GetContainerNumFreeSlots(bag).
Container 0 = backpack (16 slots), containers 1-4 = equipped bags.
Returns item count, quality, and WoW-format item links with quality
colors. Enables bag management addons (Bagnon, OneBag, AdiBags).
2026-03-20 17:14:07 -07:00
Kelsi
e6fbdfcc02 feat: add /dump command for Lua expression evaluation and debugging
/dump <expression> evaluates a Lua expression and prints the result to
chat. For tables, iterates key-value pairs and displays them. Aliases:
/print. Useful for addon development and debugging game state queries
like "/dump GetSpellInfo(133)" or "/dump UnitHealth('player')".
2026-03-20 17:05:48 -07:00
Kelsi
b3f406c6d3 fix: sync cloud density with weather intensity and DBC cloud coverage
Cloud renderer's density was hardcoded at 0.35 and never updated from the
DBC-driven cloudDensity parameter. Now setDensity() is called each frame
with the lighting manager's cloud coverage value. Active weather (rain/
snow/storm) additionally boosts cloud density by up to 0.4 so clouds
visibly thicken during storms.
2026-03-20 16:50:32 -07:00
Kelsi
d7d6819855 feat: add generic UnitAura(unit, index, filter) Lua API function
Add UnitAura() that accepts WoW-compatible filter strings: "HELPFUL" for
buffs, "HARMFUL" for debuffs. Delegates to existing UnitBuff/UnitDebuff
logic. Many addons (WeakAuras, Grid, etc.) use UnitAura with filter
strings rather than separate UnitBuff/UnitDebuff calls.
2026-03-20 16:42:06 -07:00
Kelsi
4cdccb7430 feat: fire BAG_UPDATE and PLAYER_EQUIPMENT_CHANGED events for addons
Fire BAG_UPDATE and UNIT_INVENTORY_CHANGED when item stack/durability
fields change in UPDATE_OBJECT VALUES path. Fire PLAYER_EQUIPMENT_CHANGED
when equipment slot fields change. Enables bag addons (Bagnon, OneBag) and
gear tracking addons to react to inventory changes.
2026-03-20 16:38:57 -07:00
Kelsi
ae18d25996 feat: add sun height attenuation and warm sunset tint to lens flare
Reduce flare intensity when sun is near the horizon via smoothstep on
sunDir.z (0→0.25 range). Apply amber/orange color shift to flare elements
at sunrise/sunset for a warm golden glow. Prevents overly bright flares
at low sun angles while enhancing atmospheric mood.
2026-03-20 16:34:11 -07:00
Kelsi
bf62061a31 feat: expand slash command autocomplete with 30+ missing commands
Add /reload, /reloadui, /rl, /ready, /notready, /readycheck, /cancellogout,
/clearmainassist, /clearmaintank, /mainassist, /maintank, /cloak, /gdemote,
/gkick, /gleader, /gmotd, /gpromote, /gquit, /groster, /leaveparty,
/removefriend, /score, /script, /targetenemy, /targetfriend, /targetlast,
/ticket, and more to the tab-completion list. Alphabetically sorted.
2026-03-20 16:29:32 -07:00
Kelsi
00201c1232 feat: show enchant name and XP source creature in chat messages
SMSG_ENCHANTMENTLOG now resolves spell name and shows "You enchant with
[name]" or "[Caster] enchants your item with [name]" instead of silent
debug log. SMSG_LOG_XPGAIN now shows creature name: "Wolf dies, you gain
45 experience" instead of generic "You gain 45 experience" for kill XP.
2026-03-20 16:21:52 -07:00
Kelsi
21ead2aa4b feat: add /reload command to re-initialize addon system
Add AddonManager::reload() which saves all SavedVariables, shuts down the
Lua VM, re-initializes it, rescans .toc files, and reloads all addons.
Wire /reload, /reloadui, /rl slash commands that call reload() and fire
VARIABLES_LOADED + PLAYER_LOGIN + PLAYER_ENTERING_WORLD lifecycle events.
Essential for addon development and troubleshooting.
2026-03-20 16:17:04 -07:00
Kelsi
23ebfc7e85 feat: add LFG role check confirmation popup with CMSG_LFG_SET_ROLES
When the dungeon finder initiates a role check (SMSG_LFG_ROLE_CHECK_UPDATE
state=2), show a centered popup with Tank/Healer/DPS checkboxes and
Accept/Leave Queue buttons. Accept sends CMSG_LFG_SET_ROLES with the
selected role mask. Previously only showed passive "Role check in progress"
text with no way to respond.
2026-03-20 16:10:29 -07:00
Kelsi
df7feed648 feat: add distinct STORM weather type with wind-driven particles
Add Weather::Type::STORM enum value and wire it from SMSG_WEATHER type 3.
Storm particles are faster (70 units/s vs rain's 50), wind-angled at 15+
units lateral velocity with gusty turbulence, darker blue-grey tint, and
shorter lifetime. Previously storms rendered identically to rain.
2026-03-20 15:56:58 -07:00
Kelsi
d1bcd2f844 fix: resolve compiler warnings in lua_engine and game_screen
Remove unused getPlayerUnit() helper in lua_engine.cpp (-Wunused-function).
Increase countStr buffer from 8 to 16 bytes in action bar item count
display to eliminate -Wformat-truncation warning for %d with int32_t.
Build is now warning-free.
2026-03-20 15:53:43 -07:00
Kelsi
4b6ed04926 feat: add GetZoneText, GetSubZoneText, and GetMinimapZoneText Lua APIs
Add zone name query functions using worldStateZoneId + getAreaName lookup.
GetRealZoneText is aliased to GetZoneText. These are heavily used by boss
mod addons (DBM) for zone detection and by quest tracking addons.
2026-03-20 15:44:25 -07:00
Kelsi
0dd1b08504 feat: fire spellcast channel and interrupt events for Lua addons
Add UNIT_SPELLCAST_CHANNEL_START (MSG_CHANNEL_START), UNIT_SPELLCAST_CHANNEL_STOP
(MSG_CHANNEL_UPDATE with 0ms remaining), UNIT_SPELLCAST_FAILED (SMSG_CAST_RESULT
with error), and UNIT_SPELLCAST_INTERRUPTED (SMSG_SPELL_FAILURE) events. These
enable addons to track channeled spells and cast interruptions for all units.
2026-03-20 15:37:33 -07:00
Kelsi
e033efc998 feat: add bid status indicators to auction house UI
Show [Winning] (green) or [Outbid] (red) labels on the Bids tab based on
bidderGuid vs player GUID comparison. Show [Bid] (gold) indicator on the
seller's Auctions tab when someone has placed a bid on their listing.
Improves auction house usability by making bid status visible at a glance.
2026-03-20 15:31:41 -07:00
18 changed files with 1219 additions and 51 deletions

View file

@ -27,9 +27,14 @@ public:
void saveAllSavedVariables();
/// Re-initialize the Lua VM and reload all addons (used by /reload).
bool reload();
private:
LuaEngine luaEngine_;
std::vector<TocFile> addons_;
game::GameHandler* gameHandler_ = nullptr;
std::string addonsPath_;
bool loadAddon(const TocFile& addon);
std::string getSavedVariablesPath(const TocFile& addon) const;

View file

@ -78,6 +78,9 @@ public:
// Chat notifications
void playWhisperReceived();
// Minimap ping
void playMinimapPing();
private:
struct UISample {
std::string path;
@ -126,6 +129,7 @@ private:
std::vector<UISample> selectTargetSounds_;
std::vector<UISample> deselectTargetSounds_;
std::vector<UISample> whisperSounds_;
std::vector<UISample> minimapPingSounds_;
// State tracking
float volumeScale_ = 1.0f;

View file

@ -294,6 +294,14 @@ public:
return spellIconPathResolver_ ? spellIconPathResolver_(spellId) : std::string{};
}
// Random property/suffix name resolver: randomPropertyId -> suffix name (e.g., "of the Eagle")
// Positive IDs → ItemRandomProperties.dbc; negative IDs → ItemRandomSuffix.dbc (abs value)
using RandomPropertyNameResolver = std::function<std::string(int32_t)>;
void setRandomPropertyNameResolver(RandomPropertyNameResolver r) { randomPropertyNameResolver_ = std::move(r); }
std::string getRandomPropertyName(int32_t id) const {
return randomPropertyNameResolver_ ? randomPropertyNameResolver_(id) : std::string{};
}
// Emote animation callback: (entityGuid, animationId)
using EmoteAnimCallback = std::function<void(uint64_t, uint32_t)>;
void setEmoteAnimCallback(EmoteAnimCallback cb) { emoteAnimCallback_ = std::move(cb); }
@ -868,6 +876,7 @@ public:
// 400ms spell-queue window: next spell to cast when current finishes
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; }
// Unit cast state (tracked per GUID for target frame + boss frames)
struct UnitCastState {
@ -1442,6 +1451,7 @@ public:
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
void lfgJoin(uint32_t dungeonId, uint8_t roles);
void lfgLeave();
void lfgSetRoles(uint8_t roles);
void lfgAcceptProposal(uint32_t proposalId, bool accept);
void lfgSetBootVote(bool vote);
void lfgTeleport(bool toLfgDungeon = true);
@ -2652,6 +2662,7 @@ private:
AddonChatCallback addonChatCallback_;
AddonEventCallback addonEventCallback_;
SpellIconPathResolver spellIconPathResolver_;
RandomPropertyNameResolver randomPropertyNameResolver_;
EmoteAnimCallback emoteAnimCallback_;
// Targeting

View file

@ -30,6 +30,7 @@ public:
void setProgress(float progress) { loadProgress = progress; }
void setStatus(const std::string& status) { statusText = status; }
void setZoneName(const std::string& name) { zoneName = name; }
// Must be set before initialize() for Vulkan texture upload
void setVkContext(VkContext* ctx) { vkCtx = ctx; }
@ -53,6 +54,7 @@ private:
float loadProgress = 0.0f;
std::string statusText = "Loading...";
std::string zoneName;
int imageWidth = 0;
int imageHeight = 0;

View file

@ -28,7 +28,8 @@ public:
enum class Type {
NONE,
RAIN,
SNOW
SNOW,
STORM
};
Weather();

View file

@ -388,6 +388,7 @@ private:
void renderBgInvitePopup(game::GameHandler& gameHandler);
void renderBfMgrInvitePopup(game::GameHandler& gameHandler);
void renderLfgProposalPopup(game::GameHandler& gameHandler);
void renderLfgRoleCheckPopup(game::GameHandler& gameHandler);
void renderChatBubbles(game::GameHandler& gameHandler);
void renderMailWindow(game::GameHandler& gameHandler);
void renderMailComposeWindow(game::GameHandler& gameHandler);

View file

@ -11,12 +11,14 @@ AddonManager::AddonManager() = default;
AddonManager::~AddonManager() { shutdown(); }
bool AddonManager::initialize(game::GameHandler* gameHandler) {
gameHandler_ = gameHandler;
if (!luaEngine_.initialize()) return false;
luaEngine_.setGameHandler(gameHandler);
return true;
}
void AddonManager::scanAddons(const std::string& addonsPath) {
addonsPath_ = addonsPath;
addons_.clear();
std::error_code ec;
@ -121,6 +123,26 @@ void AddonManager::saveAllSavedVariables() {
}
}
bool AddonManager::reload() {
LOG_INFO("AddonManager: reloading all addons...");
saveAllSavedVariables();
addons_.clear();
luaEngine_.shutdown();
if (!luaEngine_.initialize()) {
LOG_ERROR("AddonManager: failed to reinitialize Lua VM during reload");
return false;
}
luaEngine_.setGameHandler(gameHandler_);
if (!addonsPath_.empty()) {
scanAddons(addonsPath_);
loadAllAddons();
}
LOG_INFO("AddonManager: reload complete");
return true;
}
void AddonManager::shutdown() {
saveAllSavedVariables();
addons_.clear();

View file

@ -59,15 +59,6 @@ static int lua_wow_message(lua_State* L) {
return lua_wow_print(L);
}
// Helper: get player Unit from game handler
static game::Unit* getPlayerUnit(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return nullptr;
auto entity = gh->getEntityManager().getEntity(gh->getPlayerGuid());
if (!entity) return nullptr;
return dynamic_cast<game::Unit*>(entity.get());
}
// Helper: resolve WoW unit IDs to GUID
static uint64_t resolveUnitGuid(game::GameHandler* gh, const std::string& uid) {
if (uid == "player") return gh->getPlayerGuid();
@ -388,10 +379,50 @@ static int lua_UnitAura(lua_State* L, bool wantBuff) {
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L); // icon texture path
lua_pushnumber(L, aura.charges); // count
lua_pushnil(L); // debuffType
// debuffType: resolve from Spell.dbc dispel type
{
uint8_t dt = gh->getSpellDispelType(aura.spellId);
switch (dt) {
case 1: lua_pushstring(L, "Magic"); break;
case 2: lua_pushstring(L, "Curse"); break;
case 3: lua_pushstring(L, "Disease"); break;
case 4: lua_pushstring(L, "Poison"); break;
default: lua_pushnil(L); break;
}
}
lua_pushnumber(L, aura.maxDurationMs > 0 ? aura.maxDurationMs / 1000.0 : 0); // duration
lua_pushnumber(L, 0); // expirationTime (would need absolute time)
lua_pushnil(L); // caster
// expirationTime: GetTime() + remaining seconds (so addons can compute countdown)
if (aura.durationMs > 0) {
uint64_t auraNowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
int32_t remMs = aura.getRemainingMs(auraNowMs);
// GetTime epoch = steady_clock relative to engine start
static auto sStart = std::chrono::steady_clock::now();
double nowSec = std::chrono::duration<double>(
std::chrono::steady_clock::now() - sStart).count();
lua_pushnumber(L, nowSec + remMs / 1000.0);
} else {
lua_pushnumber(L, 0); // permanent aura
}
// caster: return unit ID string if caster is known
if (aura.casterGuid != 0) {
if (aura.casterGuid == gh->getPlayerGuid())
lua_pushstring(L, "player");
else if (aura.casterGuid == gh->getTargetGuid())
lua_pushstring(L, "target");
else if (aura.casterGuid == gh->getFocusGuid())
lua_pushstring(L, "focus");
else if (aura.casterGuid == gh->getPetGuid())
lua_pushstring(L, "pet");
else {
char cBuf[32];
snprintf(cBuf, sizeof(cBuf), "0x%016llX", (unsigned long long)aura.casterGuid);
lua_pushstring(L, cBuf);
}
} else {
lua_pushnil(L);
}
lua_pushboolean(L, 0); // isStealable
lua_pushboolean(L, 0); // shouldConsolidate
lua_pushnumber(L, aura.spellId); // spellId
@ -405,6 +436,17 @@ static int lua_UnitAura(lua_State* L, bool wantBuff) {
static int lua_UnitBuff(lua_State* L) { return lua_UnitAura(L, true); }
static int lua_UnitDebuff(lua_State* L) { return lua_UnitAura(L, false); }
// UnitAura(unit, index, filter) — generic aura query with filter string
// filter: "HELPFUL" = buffs, "HARMFUL" = debuffs, "PLAYER" = cast by player,
// "HELPFUL|PLAYER" = buffs cast by player, etc.
static int lua_UnitAuraGeneric(lua_State* L) {
const char* filter = luaL_optstring(L, 3, "HELPFUL");
std::string f(filter ? filter : "HELPFUL");
for (char& c : f) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
bool wantBuff = (f.find("HARMFUL") == std::string::npos);
return lua_UnitAura(L, wantBuff);
}
// --- Action API ---
static int lua_SendChatMessage(lua_State* L) {
@ -492,9 +534,20 @@ static int lua_GetSpellCooldown(lua_State* L) {
}
}
float cd = gh->getSpellCooldown(spellId);
lua_pushnumber(L, 0); // start time (not tracked precisely, return 0)
lua_pushnumber(L, cd); // duration remaining
return 2;
// WoW returns (start, duration, enabled) where remaining = start + duration - GetTime()
// Compute start = GetTime() - elapsed, duration = total cooldown
static auto sStart = std::chrono::steady_clock::now();
double nowSec = std::chrono::duration<double>(
std::chrono::steady_clock::now() - sStart).count();
if (cd > 0.01f) {
lua_pushnumber(L, nowSec); // start (approximate — we don't track exact start)
lua_pushnumber(L, cd); // duration (remaining, used as total for simplicity)
} else {
lua_pushnumber(L, 0); // not on cooldown
lua_pushnumber(L, 0);
}
lua_pushnumber(L, 1); // enabled (always 1 — spell is usable)
return 3;
}
static int lua_HasTarget(lua_State* L) {
@ -641,6 +694,29 @@ static int lua_GetCurrentMapAreaID(lua_State* L) {
return 1;
}
// GetZoneText() / GetRealZoneText() → current zone name
static int lua_GetZoneText(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, ""); return 1; }
uint32_t zoneId = gh->getWorldStateZoneId();
if (zoneId != 0) {
std::string name = gh->getWhoAreaName(zoneId);
if (!name.empty()) { lua_pushstring(L, name.c_str()); return 1; }
}
lua_pushstring(L, "");
return 1;
}
// GetSubZoneText() → subzone name (same as zone for now — server doesn't always send subzone)
static int lua_GetSubZoneText(lua_State* L) {
return lua_GetZoneText(L); // Best-effort: zone and subzone often overlap
}
// GetMinimapZoneText() → zone name displayed near minimap
static int lua_GetMinimapZoneText(lua_State* L) {
return lua_GetZoneText(L);
}
// --- Player State API ---
// These replace the hardcoded "return false" Lua stubs with real game state.
@ -709,6 +785,310 @@ static int lua_GetUnitSpeed(lua_State* L) {
return 1;
}
// --- Container/Bag API ---
// WoW bags: container 0 = backpack (16 slots), containers 1-4 = equipped bags
static int lua_GetContainerNumSlots(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) { lua_pushnumber(L, 0); return 1; }
const auto& inv = gh->getInventory();
if (container == 0) {
lua_pushnumber(L, inv.getBackpackSize());
} else if (container >= 1 && container <= 4) {
lua_pushnumber(L, inv.getBagSize(container - 1));
} else {
lua_pushnumber(L, 0);
}
return 1;
}
// GetContainerItemInfo(container, slot) → texture, count, locked, quality, readable, lootable, link
static int lua_GetContainerItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
if (!gh) { lua_pushnil(L); return 1; }
const auto& inv = gh->getInventory();
const game::ItemSlot* itemSlot = nullptr;
if (container == 0 && slot >= 1 && slot <= inv.getBackpackSize()) {
itemSlot = &inv.getBackpackSlot(slot - 1); // WoW uses 1-based
} else if (container >= 1 && container <= 4) {
int bagIdx = container - 1;
int bagSize = inv.getBagSize(bagIdx);
if (slot >= 1 && slot <= bagSize)
itemSlot = &inv.getBagSlot(bagIdx, slot - 1);
}
if (!itemSlot || itemSlot->empty()) { lua_pushnil(L); return 1; }
// Get item info for quality/icon
const auto* info = gh->getItemInfo(itemSlot->item.itemId);
lua_pushnil(L); // texture (icon path — would need ItemDisplayInfo icon resolver)
lua_pushnumber(L, itemSlot->item.stackCount); // count
lua_pushboolean(L, 0); // locked
lua_pushnumber(L, info ? info->quality : 0); // quality
lua_pushboolean(L, 0); // readable
lua_pushboolean(L, 0); // lootable
// Build item link with quality color
std::string name = info ? info->name : ("Item #" + std::to_string(itemSlot->item.itemId));
uint32_t q = info ? info->quality : 0;
static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
uint32_t qi = q < 8 ? q : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQH[qi], itemSlot->item.itemId, name.c_str());
lua_pushstring(L, link); // link
return 7;
}
// GetContainerItemLink(container, slot) → item link string
static int lua_GetContainerItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
if (!gh) { lua_pushnil(L); return 1; }
const auto& inv = gh->getInventory();
const game::ItemSlot* itemSlot = nullptr;
if (container == 0 && slot >= 1 && slot <= inv.getBackpackSize()) {
itemSlot = &inv.getBackpackSlot(slot - 1);
} else if (container >= 1 && container <= 4) {
int bagIdx = container - 1;
int bagSize = inv.getBagSize(bagIdx);
if (slot >= 1 && slot <= bagSize)
itemSlot = &inv.getBagSlot(bagIdx, slot - 1);
}
if (!itemSlot || itemSlot->empty()) { lua_pushnil(L); return 1; }
const auto* info = gh->getItemInfo(itemSlot->item.itemId);
std::string name = info ? info->name : ("Item #" + std::to_string(itemSlot->item.itemId));
uint32_t q = info ? info->quality : 0;
char link[256];
static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
uint32_t qi = q < 8 ? q : 1u;
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQH[qi], itemSlot->item.itemId, name.c_str());
lua_pushstring(L, link);
return 1;
}
// GetContainerNumFreeSlots(container) → numFreeSlots, bagType
static int lua_GetContainerNumFreeSlots(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
const auto& inv = gh->getInventory();
int freeSlots = 0;
int totalSlots = 0;
if (container == 0) {
totalSlots = inv.getBackpackSize();
for (int i = 0; i < totalSlots; ++i)
if (inv.getBackpackSlot(i).empty()) ++freeSlots;
} else if (container >= 1 && container <= 4) {
totalSlots = inv.getBagSize(container - 1);
for (int i = 0; i < totalSlots; ++i)
if (inv.getBagSlot(container - 1, i).empty()) ++freeSlots;
}
lua_pushnumber(L, freeSlots);
lua_pushnumber(L, 0); // bagType (0 = normal)
return 2;
}
// --- Equipment Slot API ---
// WoW inventory slot IDs: 1=Head,2=Neck,3=Shoulders,4=Shirt,5=Chest,
// 6=Waist,7=Legs,8=Feet,9=Wrists,10=Hands,11=Ring1,12=Ring2,
// 13=Trinket1,14=Trinket2,15=Back,16=MainHand,17=OffHand,18=Ranged,19=Tabard
static int lua_GetInventoryItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
const char* uid = luaL_optstring(L, 1, "player");
int slotId = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || slotId < 1 || slotId > 19) { lua_pushnil(L); return 1; }
std::string uidStr(uid);
for (char& c : uidStr) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (uidStr != "player") { lua_pushnil(L); return 1; }
const auto& inv = gh->getInventory();
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(slotId - 1));
if (slot.empty()) { lua_pushnil(L); return 1; }
const auto* info = gh->getItemInfo(slot.item.itemId);
std::string name = info ? info->name : slot.item.name;
uint32_t q = info ? info->quality : static_cast<uint32_t>(slot.item.quality);
static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
uint32_t qi = q < 8 ? q : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQH[qi], slot.item.itemId, name.c_str());
lua_pushstring(L, link);
return 1;
}
static int lua_GetInventoryItemID(lua_State* L) {
auto* gh = getGameHandler(L);
const char* uid = luaL_optstring(L, 1, "player");
int slotId = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || slotId < 1 || slotId > 19) { lua_pushnil(L); return 1; }
std::string uidStr(uid);
for (char& c : uidStr) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (uidStr != "player") { lua_pushnil(L); return 1; }
const auto& inv = gh->getInventory();
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(slotId - 1));
if (slot.empty()) { lua_pushnil(L); return 1; }
lua_pushnumber(L, slot.item.itemId);
return 1;
}
static int lua_GetInventoryItemTexture(lua_State* L) {
auto* gh = getGameHandler(L);
const char* uid = luaL_optstring(L, 1, "player");
int slotId = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || slotId < 1 || slotId > 19) { lua_pushnil(L); return 1; }
std::string uidStr(uid);
for (char& c : uidStr) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (uidStr != "player") { lua_pushnil(L); return 1; }
const auto& inv = gh->getInventory();
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(slotId - 1));
if (slot.empty()) { lua_pushnil(L); return 1; }
// Return spell icon path for the item's on-use spell, or nil
lua_pushnil(L);
return 1;
}
// --- Time & XP API ---
static int lua_GetGameTime(lua_State* L) {
// Returns server game time as hours, minutes
auto* gh = getGameHandler(L);
if (gh) {
float gt = gh->getGameTime();
int hours = static_cast<int>(gt) % 24;
int mins = static_cast<int>((gt - static_cast<int>(gt)) * 60.0f);
lua_pushnumber(L, hours);
lua_pushnumber(L, mins);
} else {
lua_pushnumber(L, 12);
lua_pushnumber(L, 0);
}
return 2;
}
static int lua_GetServerTime(lua_State* L) {
lua_pushnumber(L, static_cast<double>(std::time(nullptr)));
return 1;
}
static int lua_UnitXP(lua_State* L) {
const char* uid = luaL_optstring(L, 1, "player");
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); return 1; }
std::string u(uid);
for (char& c : u) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (u == "player") lua_pushnumber(L, gh->getPlayerXp());
else lua_pushnumber(L, 0);
return 1;
}
static int lua_UnitXPMax(lua_State* L) {
const char* uid = luaL_optstring(L, 1, "player");
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 1); return 1; }
std::string u(uid);
for (char& c : u) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (u == "player") {
uint32_t nlxp = gh->getPlayerNextLevelXp();
lua_pushnumber(L, nlxp > 0 ? nlxp : 1);
} else {
lua_pushnumber(L, 1);
}
return 1;
}
// GetXPExhaustion() → rested XP pool remaining (nil if none)
static int lua_GetXPExhaustion(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnil(L); return 1; }
uint32_t rested = gh->getPlayerRestedXp();
if (rested > 0) lua_pushnumber(L, rested);
else lua_pushnil(L);
return 1;
}
// GetRestState() → 1 = normal, 2 = rested
static int lua_GetRestState(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, (gh && gh->isPlayerResting()) ? 2 : 1);
return 1;
}
// --- Quest Log API ---
static int lua_GetNumQuestLogEntries(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
const auto& ql = gh->getQuestLog();
lua_pushnumber(L, ql.size()); // numEntries
lua_pushnumber(L, 0); // numQuests (headers not tracked)
return 2;
}
// GetQuestLogTitle(index) → title, level, suggestedGroup, isHeader, isCollapsed, isComplete, frequency, questID
static int lua_GetQuestLogTitle(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { lua_pushnil(L); return 1; }
const auto& ql = gh->getQuestLog();
if (index > static_cast<int>(ql.size())) { lua_pushnil(L); return 1; }
const auto& q = ql[index - 1]; // 1-based
lua_pushstring(L, q.title.c_str()); // title
lua_pushnumber(L, 0); // level (not tracked)
lua_pushnumber(L, 0); // suggestedGroup
lua_pushboolean(L, 0); // isHeader
lua_pushboolean(L, 0); // isCollapsed
lua_pushboolean(L, q.complete); // isComplete
lua_pushnumber(L, 0); // frequency
lua_pushnumber(L, q.questId); // questID
return 8;
}
// GetQuestLogQuestText(index) → description, objectives
static int lua_GetQuestLogQuestText(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { lua_pushnil(L); return 1; }
const auto& ql = gh->getQuestLog();
if (index > static_cast<int>(ql.size())) { lua_pushnil(L); return 1; }
const auto& q = ql[index - 1];
lua_pushstring(L, ""); // description (not stored)
lua_pushstring(L, q.objectives.c_str()); // objectives
return 2;
}
// IsQuestComplete(questID) → boolean
static int lua_IsQuestComplete(lua_State* L) {
auto* gh = getGameHandler(L);
uint32_t questId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (!gh) { lua_pushboolean(L, 0); return 1; }
for (const auto& q : gh->getQuestLog()) {
if (q.questId == questId) {
lua_pushboolean(L, q.complete);
return 1;
}
}
lua_pushboolean(L, 0);
return 1;
}
// --- Additional WoW API ---
static int lua_UnitAffectingCombat(lua_State* L) {
@ -842,6 +1222,198 @@ static int lua_UnitCreatureType(lua_State* L) {
return 1;
}
// GetPlayerInfoByGUID(guid) → localizedClass, englishClass, localizedRace, englishRace, sex, name, realm
static int lua_GetPlayerInfoByGUID(lua_State* L) {
auto* gh = getGameHandler(L);
const char* guidStr = luaL_checkstring(L, 1);
if (!gh || !guidStr) {
for (int i = 0; i < 7; i++) lua_pushnil(L);
return 7;
}
// Parse hex GUID string "0x0000000000000001"
uint64_t guid = 0;
if (guidStr[0] == '0' && (guidStr[1] == 'x' || guidStr[1] == 'X'))
guid = strtoull(guidStr + 2, nullptr, 16);
else
guid = strtoull(guidStr, nullptr, 16);
if (guid == 0) { for (int i = 0; i < 7; i++) lua_pushnil(L); return 7; }
// Look up entity name
std::string name = gh->lookupName(guid);
if (name.empty() && guid == gh->getPlayerGuid()) {
const auto& chars = gh->getCharacters();
for (const auto& c : chars)
if (c.guid == guid) { name = c.name; break; }
}
// For player GUID, return class/race if it's the local player
const char* className = "Unknown";
const char* raceName = "Unknown";
if (guid == gh->getPlayerGuid()) {
static const char* kClasses[] = {"","Warrior","Paladin","Hunter","Rogue","Priest",
"Death Knight","Shaman","Mage","Warlock","","Druid"};
static const char* kRaces[] = {"","Human","Orc","Dwarf","Night Elf","Undead",
"Tauren","Gnome","Troll","","Blood Elf","Draenei"};
uint8_t cid = gh->getPlayerClass();
uint8_t rid = gh->getPlayerRace();
if (cid < 12) className = kClasses[cid];
if (rid < 12) raceName = kRaces[rid];
}
lua_pushstring(L, className); // 1: localizedClass
lua_pushstring(L, className); // 2: englishClass
lua_pushstring(L, raceName); // 3: localizedRace
lua_pushstring(L, raceName); // 4: englishRace
lua_pushnumber(L, 0); // 5: sex (0=unknown)
lua_pushstring(L, name.c_str()); // 6: name
lua_pushstring(L, ""); // 7: realm
return 7;
}
// GetItemLink(itemId) → "|cFFxxxxxx|Hitem:ID:...|h[Name]|h|r"
static int lua_GetItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnil(L); return 1; }
uint32_t itemId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (itemId == 0) { lua_pushnil(L); return 1; }
const auto* info = gh->getItemInfo(itemId);
if (!info || info->name.empty()) { lua_pushnil(L); return 1; }
static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
uint32_t qi = info->quality < 8 ? info->quality : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQH[qi], itemId, info->name.c_str());
lua_pushstring(L, link);
return 1;
}
// GetSpellLink(spellIdOrName) → "|cFFxxxxxx|Hspell:ID|h[Name]|h|r"
static int lua_GetSpellLink(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnil(L); return 1; }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { lua_pushnil(L); return 1; }
std::string nameLow(name);
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0) { lua_pushnil(L); return 1; }
std::string name = gh->getSpellName(spellId);
if (name.empty()) { lua_pushnil(L); return 1; }
char link[256];
snprintf(link, sizeof(link), "|cff71d5ff|Hspell:%u|h[%s]|h|r", spellId, name.c_str());
lua_pushstring(L, link);
return 1;
}
// IsUsableSpell(spellIdOrName) → usable, noMana
static int lua_IsUsableSpell(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
std::string nameLow(name);
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0 || !gh->getKnownSpells().count(spellId)) {
lua_pushboolean(L, 0);
lua_pushboolean(L, 0);
return 2;
}
// Check if on cooldown
float cd = gh->getSpellCooldown(spellId);
bool onCooldown = (cd > 0.1f);
lua_pushboolean(L, onCooldown ? 0 : 1); // usable (not on cooldown)
lua_pushboolean(L, 0); // noMana (can't determine without spell cost data)
return 2;
}
// IsInInstance() → isInstance, instanceType
static int lua_IsInInstance(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushboolean(L, 0); lua_pushstring(L, "none"); return 2; }
bool inInstance = gh->isInInstance();
lua_pushboolean(L, inInstance);
lua_pushstring(L, inInstance ? "party" : "none"); // simplified: "none", "party", "raid", "pvp", "arena"
return 2;
}
// GetInstanceInfo() → name, type, difficultyIndex, difficultyName, maxPlayers, ...
static int lua_GetInstanceInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) {
lua_pushstring(L, ""); lua_pushstring(L, "none"); lua_pushnumber(L, 0);
lua_pushstring(L, "Normal"); lua_pushnumber(L, 0);
return 5;
}
std::string mapName = gh->getMapName(gh->getCurrentMapId());
lua_pushstring(L, mapName.c_str()); // 1: name
lua_pushstring(L, gh->isInInstance() ? "party" : "none"); // 2: instanceType
lua_pushnumber(L, gh->getInstanceDifficulty()); // 3: difficultyIndex
static const char* kDiff[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"};
uint32_t diff = gh->getInstanceDifficulty();
lua_pushstring(L, (diff < 4) ? kDiff[diff] : "Normal"); // 4: difficultyName
lua_pushnumber(L, 5); // 5: maxPlayers (default 5-man)
return 5;
}
// GetInstanceDifficulty() → difficulty (1=normal, 2=heroic, 3=25normal, 4=25heroic)
static int lua_GetInstanceDifficulty(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? (gh->getInstanceDifficulty() + 1) : 1); // WoW returns 1-based
return 1;
}
// UnitClassification(unit) → "normal", "elite", "rareelite", "worldboss", "rare"
static int lua_UnitClassification(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, "normal"); return 1; }
const char* uid = luaL_optstring(L, 1, "target");
std::string uidStr(uid);
for (char& c : uidStr) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid == 0) { lua_pushstring(L, "normal"); return 1; }
auto entity = gh->getEntityManager().getEntity(guid);
if (!entity || entity->getType() == game::ObjectType::PLAYER) {
lua_pushstring(L, "normal");
return 1;
}
auto unit = std::dynamic_pointer_cast<game::Unit>(entity);
if (!unit) { lua_pushstring(L, "normal"); return 1; }
int rank = gh->getCreatureRank(unit->getEntry());
switch (rank) {
case 1: lua_pushstring(L, "elite"); break;
case 2: lua_pushstring(L, "rareelite"); break;
case 3: lua_pushstring(L, "worldboss"); break;
case 4: lua_pushstring(L, "rare"); break;
default: lua_pushstring(L, "normal"); break;
}
return 1;
}
// --- Frame System ---
// Minimal WoW-compatible frame objects with RegisterEvent/SetScript/GetScript.
// Frames are Lua tables with a metatable that provides methods.
@ -1175,6 +1747,7 @@ void LuaEngine::registerCoreAPI() {
{"InCombatLockdown", lua_InCombatLockdown},
{"UnitBuff", lua_UnitBuff},
{"UnitDebuff", lua_UnitDebuff},
{"UnitAura", lua_UnitAuraGeneric},
{"GetNumAddOns", lua_GetNumAddOns},
{"GetAddOnInfo", lua_GetAddOnInfo},
{"GetSpellInfo", lua_GetSpellInfo},
@ -1183,6 +1756,10 @@ void LuaEngine::registerCoreAPI() {
{"GetLocale", lua_GetLocale},
{"GetBuildInfo", lua_GetBuildInfo},
{"GetCurrentMapAreaID", lua_GetCurrentMapAreaID},
{"GetZoneText", lua_GetZoneText},
{"GetRealZoneText", lua_GetZoneText},
{"GetSubZoneText", lua_GetSubZoneText},
{"GetMinimapZoneText", lua_GetMinimapZoneText},
// Player state (replaces hardcoded stubs)
{"IsMounted", lua_IsMounted},
{"IsFlying", lua_IsFlying},
@ -1201,6 +1778,35 @@ void LuaEngine::registerCoreAPI() {
{"UnitIsFriend", lua_UnitIsFriend},
{"UnitIsEnemy", lua_UnitIsEnemy},
{"UnitCreatureType", lua_UnitCreatureType},
{"UnitClassification", lua_UnitClassification},
{"GetPlayerInfoByGUID", lua_GetPlayerInfoByGUID},
{"GetItemLink", lua_GetItemLink},
{"GetSpellLink", lua_GetSpellLink},
{"IsUsableSpell", lua_IsUsableSpell},
{"IsInInstance", lua_IsInInstance},
{"GetInstanceInfo", lua_GetInstanceInfo},
{"GetInstanceDifficulty", lua_GetInstanceDifficulty},
// Container/bag API
{"GetContainerNumSlots", lua_GetContainerNumSlots},
{"GetContainerItemInfo", lua_GetContainerItemInfo},
{"GetContainerItemLink", lua_GetContainerItemLink},
{"GetContainerNumFreeSlots", lua_GetContainerNumFreeSlots},
// Equipment slot API
{"GetInventoryItemLink", lua_GetInventoryItemLink},
{"GetInventoryItemID", lua_GetInventoryItemID},
{"GetInventoryItemTexture", lua_GetInventoryItemTexture},
// Time/XP API
{"GetGameTime", lua_GetGameTime},
{"GetServerTime", lua_GetServerTime},
{"UnitXP", lua_UnitXP},
{"UnitXPMax", lua_UnitXPMax},
{"GetXPExhaustion", lua_GetXPExhaustion},
{"GetRestState", lua_GetRestState},
// Quest log API
{"GetNumQuestLogEntries", lua_GetNumQuestLogEntries},
{"GetQuestLogTitle", lua_GetQuestLogTitle},
{"GetQuestLogQuestText", lua_GetQuestLogQuestText},
{"IsQuestComplete", lua_IsQuestComplete},
// Utilities
{"strsplit", lua_strsplit},
{"strtrim", lua_strtrim},
@ -1382,6 +1988,20 @@ void LuaEngine::registerCoreAPI() {
" SHAMAN={r=0.0,g=0.44,b=0.87}, MAGE={r=0.41,g=0.80,b=0.94},\n"
" WARLOCK={r=0.58,g=0.51,b=0.79}, DRUID={r=1.0,g=0.49,b=0.04},\n"
"}\n"
// Money formatting utility
"function GetCoinTextureString(copper)\n"
" if not copper or copper == 0 then return '0c' end\n"
" copper = math.floor(copper)\n"
" local g = math.floor(copper / 10000)\n"
" local s = math.floor(math.fmod(copper, 10000) / 100)\n"
" local c = math.fmod(copper, 100)\n"
" local r = ''\n"
" if g > 0 then r = r .. g .. 'g ' end\n"
" if s > 0 then r = r .. s .. 's ' end\n"
" if c > 0 or r == '' then r = r .. c .. 'c' end\n"
" return r\n"
"end\n"
"GetCoinText = GetCoinTextureString\n"
);
}

View file

@ -130,6 +130,12 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
}
}
// Minimap ping sound
minimapPingSounds_.resize(1);
if (!loadSound("Sound\\Interface\\MapPing.wav", minimapPingSounds_[0], assets)) {
minimapPingSounds_ = selectTargetSounds_; // fallback to target select sound
}
LOG_INFO("UISoundManager: Window sounds - Bag: ", (bagOpenLoaded && bagCloseLoaded) ? "YES" : "NO",
", QuestLog: ", (questLogOpenLoaded && questLogCloseLoaded) ? "YES" : "NO",
", CharSheet: ", (charSheetOpenLoaded && charSheetCloseLoaded) ? "YES" : "NO");
@ -236,5 +242,8 @@ void UiSoundManager::playTargetDeselect() { playSound(deselectTargetSounds_); }
// Chat notifications
void UiSoundManager::playWhisperReceived() { playSound(whisperSounds_); }
// Minimap ping
void UiSoundManager::playMinimapPing() { playSound(minimapPingSounds_); }
} // namespace audio
} // namespace wowee

View file

@ -413,6 +413,38 @@ bool Application::initialize() {
return pit->second;
});
}
// Wire random property/suffix name resolver for item display
{
auto propNames = std::make_shared<std::unordered_map<int32_t, std::string>>();
auto propLoaded = std::make_shared<bool>(false);
auto* amPtr = assetManager.get();
gameHandler->setRandomPropertyNameResolver([propNames, propLoaded, amPtr](int32_t id) -> std::string {
if (!amPtr || id == 0) return {};
if (!*propLoaded) {
*propLoaded = true;
// ItemRandomProperties.dbc: ID=0, Name=4 (string)
if (auto dbc = amPtr->loadDBC("ItemRandomProperties.dbc"); dbc && dbc->isLoaded()) {
uint32_t nameField = (dbc->getFieldCount() > 4) ? 4 : 1;
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
int32_t rid = static_cast<int32_t>(dbc->getUInt32(r, 0));
std::string name = dbc->getString(r, nameField);
if (!name.empty() && rid > 0) (*propNames)[rid] = name;
}
}
// ItemRandomSuffix.dbc: ID=0, Name=4 (string) — stored as negative IDs
if (auto dbc = amPtr->loadDBC("ItemRandomSuffix.dbc"); dbc && dbc->isLoaded()) {
uint32_t nameField = (dbc->getFieldCount() > 4) ? 4 : 1;
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
int32_t rid = static_cast<int32_t>(dbc->getUInt32(r, 0));
std::string name = dbc->getString(r, nameField);
if (!name.empty() && rid > 0) (*propNames)[-rid] = name;
}
}
}
auto it = propNames->find(id);
return (it != propNames->end()) ? it->second : std::string{};
});
}
LOG_INFO("Addon system initialized, found ", addonManager_->getAddons().size(), " addon(s)");
} else {
LOG_WARNING("Failed to initialize addon system");
@ -646,6 +678,15 @@ void Application::run() {
LOG_ERROR("GPU device lost — exiting application");
window->setShouldClose(true);
}
// Soft frame rate cap when vsync is off to prevent 100% CPU usage.
// Target ~240 FPS max (~4.2ms per frame); vsync handles its own pacing.
if (!window->isVsyncEnabled() && deltaTime < 0.004f) {
float sleepMs = (0.004f - deltaTime) * 1000.0f;
if (sleepMs > 0.5f)
std::this_thread::sleep_for(std::chrono::microseconds(
static_cast<int64_t>(sleepMs * 900.0f))); // 90% of target to account for sleep overshoot
}
}
} catch (...) {
watchdogRunning.store(false, std::memory_order_release);
@ -4286,6 +4327,15 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
window->swapBuffers();
};
// Set zone name on loading screen from Map.dbc
if (gameHandler) {
std::string mapDisplayName = gameHandler->getMapName(mapId);
if (!mapDisplayName.empty())
loadingScreen.setZoneName(mapDisplayName);
else
loadingScreen.setZoneName("Loading...");
}
showProgress("Entering world...", 0.0f);
// --- Clean up previous map's state on map change ---

View file

@ -784,7 +784,22 @@ void GameHandler::disconnect() {
wardenLoadedModule_.reset();
pendingIncomingPackets_.clear();
pendingUpdateObjectWork_.clear();
// Clear entity state so reconnect sees fresh CREATE_OBJECT for all visible objects.
// Fire despawn callbacks so the renderer releases M2/character model resources.
for (const auto& [guid, entity] : entityManager.getEntities()) {
if (guid == playerGuid) continue;
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_)
creatureDespawnCallback_(guid);
else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_)
playerDespawnCallback_(guid);
else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_)
gameObjectDespawnCallback_(guid);
}
otherPlayerVisibleItemEntries_.clear();
otherPlayerVisibleDirty_.clear();
otherPlayerMoveTimeMs_.clear();
unitCastStates_.clear();
unitAurasCache_.clear();
combatText.clear();
entityManager.clear();
setState(WorldState::DISCONNECTED);
LOG_INFO("Disconnected from world server");
@ -1963,7 +1978,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
/*uint32_t itemSlot =*/ packet.readUInt32();
uint32_t itemId = packet.readUInt32();
/*uint32_t suffixFactor =*/ packet.readUInt32();
/*int32_t randomProp =*/ static_cast<int32_t>(packet.readUInt32());
int32_t randomProp = static_cast<int32_t>(packet.readUInt32());
uint32_t count = packet.readUInt32();
/*uint32_t totalCount =*/ packet.readUInt32();
@ -1972,6 +1987,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (const ItemQueryResponseData* info = getItemInfo(itemId)) {
// Item info already cached — emit immediately.
std::string itemName = info->name.empty() ? ("item #" + std::to_string(itemId)) : info->name;
// Append random suffix name (e.g., "of the Eagle") if present
if (randomProp != 0) {
std::string suffix = getRandomPropertyName(randomProp);
if (!suffix.empty()) itemName += " " + suffix;
}
uint32_t quality = info->quality;
std::string link = buildItemLink(itemId, quality, itemName);
std::string msg = "Received: " + link;
@ -1982,6 +2002,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
sfx->playLootItem();
}
if (itemLootCallback_) itemLootCallback_(itemId, count, quality, itemName);
// Fire CHAT_MSG_LOOT for loot tracking addons
if (addonEventCallback_)
addonEventCallback_("CHAT_MSG_LOOT", {msg, "", std::to_string(itemId), std::to_string(count)});
} else {
// Item info not yet cached; defer until SMSG_ITEM_QUERY_SINGLE_RESPONSE.
pendingItemPushNotifs_.push_back({itemId, count});
@ -2281,6 +2304,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
: ("Spell cast failed (error " + std::to_string(castResult) + ")");
addUIError(errMsg);
if (spellCastFailedCallback_) spellCastFailedCallback_(castResultSpellId);
if (addonEventCallback_)
addonEventCallback_("UNIT_SPELLCAST_FAILED", {"player", std::to_string(castResultSpellId)});
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.language = ChatLanguage::UNIVERSAL;
@ -2350,9 +2375,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
/*uint32_t mapId =*/ packet.readUInt32();
uint32_t slot = packet.readUInt32();
uint32_t itemId = packet.readUInt32();
int32_t rollRandProp = 0;
if (isWotLK) {
/*uint32_t randSuffix =*/ packet.readUInt32();
/*uint32_t randProp =*/ packet.readUInt32();
rollRandProp = static_cast<int32_t>(packet.readUInt32());
}
uint32_t countdown = packet.readUInt32();
uint8_t voteMask = packet.readUInt8();
@ -2362,11 +2388,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
pendingLootRoll_.slot = slot;
pendingLootRoll_.itemId = itemId;
// Ensure item info is queried so the roll popup can show the name/icon.
// The popup re-reads getItemInfo() live, so the name will populate once
// SMSG_ITEM_QUERY_SINGLE_RESPONSE arrives (usually within ~100 ms).
queryItemInfo(itemId, 0);
auto* info = getItemInfo(itemId);
pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId);
std::string rollItemName = info ? info->name : std::to_string(itemId);
if (rollRandProp != 0) {
std::string suffix = getRandomPropertyName(rollRandProp);
if (!suffix.empty()) rollItemName += " " + suffix;
}
pendingLootRoll_.itemName = rollItemName;
pendingLootRoll_.itemQuality = info ? static_cast<uint8_t>(info->quality) : 0;
pendingLootRoll_.rollCountdownMs = (countdown > 0 && countdown <= 120000) ? countdown : 60000;
pendingLootRoll_.voteMask = voteMask;
@ -2522,6 +2551,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
poi.icon = icon;
poi.data = data;
poi.name = std::move(name);
// Cap POI count to prevent unbounded growth from rapid gossip queries
if (gossipPois_.size() >= 200) gossipPois_.erase(gossipPois_.begin());
gossipPois_.push_back(std::move(poi));
LOG_DEBUG("SMSG_GOSSIP_POI: x=", poiX, " y=", poiY, " icon=", icon);
break;
@ -3381,6 +3412,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
}
}
// Fire UNIT_SPELLCAST_INTERRUPTED for Lua addons
if (addonEventCallback_) {
std::string unitId;
if (failGuid == playerGuid || failGuid == 0) unitId = "player";
else if (failGuid == targetGuid) unitId = "target";
else if (failGuid == focusGuid) unitId = "focus";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_INTERRUPTED", {unitId});
}
if (failGuid == playerGuid || failGuid == 0) {
// Player's own cast failed — clear gather-node loot target so the
// next timed cast doesn't try to loot a stale interrupted gather node.
@ -3518,6 +3558,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
ping.wowY = pingX; // canonical WoW Y = west = server's posX
ping.age = 0.0f;
minimapPings_.push_back(ping);
// Play ping sound for other players' pings (not our own)
if (senderGuid != playerGuid) {
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playMinimapPing();
}
}
break;
}
case Opcode::SMSG_ZONE_UNDER_ATTACK: {
@ -4145,6 +4192,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
addSystemChatMessage(buf);
watchedFactionId_ = factionId;
if (repChangeCallback_) repChangeCallback_(name, delta, standing);
if (addonEventCallback_)
addonEventCallback_("UPDATE_FACTION", {});
}
LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing);
}
@ -4543,6 +4592,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
}
LOG_INFO("SMSG_ACTION_BUTTONS: populated action bar from server");
if (addonEventCallback_) addonEventCallback_("ACTIONBAR_SLOT_CHANGED", {});
packet.setReadPos(packet.getSize());
break;
}
@ -4980,8 +5030,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
uint64_t progress = packet.readUInt64();
packet.readUInt32(); // elapsedTime
packet.readUInt32(); // creationTime
uint64_t oldProgress = 0;
auto cpit = criteriaProgress_.find(criteriaId);
if (cpit != criteriaProgress_.end()) oldProgress = cpit->second;
criteriaProgress_[criteriaId] = progress;
LOG_DEBUG("SMSG_CRITERIA_UPDATE: id=", criteriaId, " progress=", progress);
// Fire addon event for achievement tracking addons
if (addonEventCallback_ && progress != oldProgress)
addonEventCallback_("CRITERIA_UPDATE", {std::to_string(criteriaId), std::to_string(progress)});
}
break;
}
@ -5065,12 +5121,27 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_ENCHANTMENTLOG: {
// uint64 targetGuid + uint64 casterGuid + uint32 spellId + uint32 displayId + uint32 animType
if (packet.getSize() - packet.getReadPos() >= 28) {
/*uint64_t targetGuid =*/ packet.readUInt64();
/*uint64_t casterGuid =*/ packet.readUInt64();
uint32_t spellId = packet.readUInt32();
uint64_t enchTargetGuid = packet.readUInt64();
uint64_t enchCasterGuid = packet.readUInt64();
uint32_t enchSpellId = packet.readUInt32();
/*uint32_t displayId =*/ packet.readUInt32();
/*uint32_t animType =*/ packet.readUInt32();
LOG_DEBUG("SMSG_ENCHANTMENTLOG: spellId=", spellId);
LOG_DEBUG("SMSG_ENCHANTMENTLOG: spellId=", enchSpellId);
// Show enchant message if the player is involved
if (enchTargetGuid == playerGuid || enchCasterGuid == playerGuid) {
const std::string& enchName = getSpellName(enchSpellId);
std::string casterName = lookupName(enchCasterGuid);
if (!enchName.empty()) {
std::string msg;
if (enchCasterGuid == playerGuid)
msg = "You enchant with " + enchName + ".";
else if (!casterName.empty())
msg = casterName + " enchants your item with " + enchName + ".";
else
msg = "Your item has been enchanted with " + enchName + ".";
addSystemChatMessage(msg);
}
}
}
break;
}
@ -5263,6 +5334,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
}
}
if (addonEventCallback_) addonEventCallback_("QUEST_LOG_UPDATE", {});
// Re-query all nearby quest giver NPCs so markers refresh
if (socket) {
for (const auto& [guid, entity] : entityManager.getEntities()) {
@ -6137,16 +6209,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
handleAuctionCommandResult(packet);
break;
case Opcode::SMSG_AUCTION_OWNER_NOTIFICATION: {
// auctionId(u32) + action(u32) + error(u32) + itemEntry(u32) + ...
// auctionId(u32) + action(u32) + error(u32) + itemEntry(u32) + randomPropertyId(u32) + ...
// action: 0=sold/won, 1=expired, 2=bid placed on your auction
if (packet.getSize() - packet.getReadPos() >= 16) {
/*uint32_t auctionId =*/ packet.readUInt32();
uint32_t action = packet.readUInt32();
/*uint32_t error =*/ packet.readUInt32();
uint32_t itemEntry = packet.readUInt32();
int32_t ownerRandProp = 0;
if (packet.getSize() - packet.getReadPos() >= 4)
ownerRandProp = static_cast<int32_t>(packet.readUInt32());
ensureItemInfo(itemEntry);
auto* info = getItemInfo(itemEntry);
std::string rawName = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry));
if (ownerRandProp != 0) {
std::string suffix = getRandomPropertyName(ownerRandProp);
if (!suffix.empty()) rawName += " " + suffix;
}
uint32_t aucQuality = info ? info->quality : 1u;
std::string itemLink = buildItemLink(itemEntry, aucQuality, rawName);
if (action == 1)
@ -6160,14 +6239,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_AUCTION_BIDDER_NOTIFICATION: {
// auctionId(u32) + itemEntry(u32) + ...
// auctionHouseId(u32) + auctionId(u32) + bidderGuid(u64) + bidAmount(u32) + outbidAmount(u32) + itemEntry(u32) + randomPropertyId(u32)
if (packet.getSize() - packet.getReadPos() >= 8) {
uint32_t auctionId = packet.readUInt32();
/*uint32_t auctionId =*/ packet.readUInt32();
uint32_t itemEntry = packet.readUInt32();
(void)auctionId;
int32_t bidRandProp = 0;
// Try to read randomPropertyId if enough data remains
if (packet.getSize() - packet.getReadPos() >= 4)
bidRandProp = static_cast<int32_t>(packet.readUInt32());
ensureItemInfo(itemEntry);
auto* info = getItemInfo(itemEntry);
std::string rawName2 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry));
if (bidRandProp != 0) {
std::string suffix = getRandomPropertyName(bidRandProp);
if (!suffix.empty()) rawName2 += " " + suffix;
}
uint32_t bidQuality = info ? info->quality : 1u;
std::string bidLink = buildItemLink(itemEntry, bidQuality, rawName2);
addSystemChatMessage("You have been outbid on " + bidLink + ".");
@ -6180,10 +6266,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 12) {
/*uint32_t auctionId =*/ packet.readUInt32();
uint32_t itemEntry = packet.readUInt32();
/*uint32_t itemRandom =*/ packet.readUInt32();
int32_t itemRandom = static_cast<int32_t>(packet.readUInt32());
ensureItemInfo(itemEntry);
auto* info = getItemInfo(itemEntry);
std::string rawName3 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry));
if (itemRandom != 0) {
std::string suffix = getRandomPropertyName(itemRandom);
if (!suffix.empty()) rawName3 += " " + suffix;
}
uint32_t remQuality = info ? info->quality : 1u;
std::string remLink = buildItemLink(itemEntry, remQuality, rawName3);
addSystemChatMessage("Your auction of " + remLink + " has expired.");
@ -7302,6 +7392,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
// Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons
if (addonEventCallback_) {
std::string unitId;
if (chanCaster == playerGuid) unitId = "player";
else if (chanCaster == targetGuid) unitId = "target";
else if (chanCaster == focusGuid) unitId = "focus";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_CHANNEL_START", {unitId, std::to_string(chanSpellId)});
}
}
break;
}
@ -7329,6 +7428,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
LOG_DEBUG("MSG_CHANNEL_UPDATE: caster=0x", std::hex, chanCaster2, std::dec,
" remaining=", chanRemainMs, "ms");
// Fire UNIT_SPELLCAST_CHANNEL_STOP when channel ends
if (chanRemainMs == 0 && addonEventCallback_) {
std::string unitId;
if (chanCaster2 == playerGuid) unitId = "player";
else if (chanCaster2 == targetGuid) unitId = "target";
else if (chanCaster2 == focusGuid) unitId = "focus";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_CHANNEL_STOP", {unitId});
}
break;
}
@ -12574,6 +12682,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
if (addonEventCallback_) addonEventCallback_("PLAYER_ALIVE", {});
if (ghostStateCallback_) ghostStateCallback_(false);
}
if (addonEventCallback_)
addonEventCallback_("PLAYER_FLAGS_CHANGED", {});
}
else if (ufMeleeAPV != 0xFFFF && key == ufMeleeAPV) { playerMeleeAP_ = static_cast<int32_t>(val); }
else if (ufRangedAPV != 0xFFFF && key == ufRangedAPV) { playerRangedAP_ = static_cast<int32_t>(val); }
@ -12604,7 +12714,11 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
// Do not auto-create quests from VALUES quest-log slot fields for the
// same reason as CREATE_OBJECT2 above (can be misaligned per realm).
if (applyInventoryFields(block.fields)) slotsChanged = true;
if (slotsChanged) rebuildOnlineInventory();
if (slotsChanged) {
rebuildOnlineInventory();
if (addonEventCallback_)
addonEventCallback_("PLAYER_EQUIPMENT_CHANGED", {});
}
extractSkillFields(lastPlayerFields_);
extractExploredZoneFields(lastPlayerFields_);
applyQuestStateFromFields(lastPlayerFields_);
@ -12709,6 +12823,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
}
if (inventoryChanged) {
rebuildOnlineInventory();
if (addonEventCallback_) {
addonEventCallback_("BAG_UPDATE", {});
addonEventCallback_("UNIT_INVENTORY_CHANGED", {"player"});
}
}
}
if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) {
@ -12966,8 +13084,21 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
// Clean up quest giver status
npcQuestStatus_.erase(data.guid);
// Remove combat text entries referencing the destroyed entity so floating
// damage numbers don't linger after the source/target despawns.
combatText.erase(
std::remove_if(combatText.begin(), combatText.end(),
[&data](const CombatTextEntry& e) {
return e.dstGuid == data.guid;
}),
combatText.end());
// Clean up unit cast state (cast bar) for the destroyed unit
unitCastStates_.erase(data.guid);
// Clean up cached auras
unitAurasCache_.erase(data.guid);
tabCycleStale = true;
// Entity count logging disabled
}
void GameHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) {
@ -17143,6 +17274,17 @@ void GameHandler::lfgLeave() {
LOG_INFO("Sent CMSG_LFG_LEAVE");
}
void GameHandler::lfgSetRoles(uint8_t roles) {
if (state != WorldState::IN_WORLD || !socket) return;
const uint32_t wire = wireOpcode(Opcode::CMSG_LFG_SET_ROLES);
if (wire == 0xFFFF) return;
network::Packet pkt(static_cast<uint16_t>(wire));
pkt.writeUInt8(roles);
socket->send(pkt);
LOG_INFO("Sent CMSG_LFG_SET_ROLES: roles=", static_cast<int>(roles));
}
void GameHandler::lfgAcceptProposal(uint32_t proposalId, bool accept) {
if (!socket) return;
@ -19434,7 +19576,10 @@ void GameHandler::handleSupercededSpell(network::Packet& packet) {
LOG_DEBUG("Action bar slot upgraded: spell ", oldSpellId, " -> ", newSpellId);
}
}
if (barChanged) saveCharacterConfig();
if (barChanged) {
saveCharacterConfig();
if (addonEventCallback_) addonEventCallback_("ACTIONBAR_SLOT_CHANGED", {});
}
// Show "Upgraded to X" only when the new spell wasn't already announced by the
// trainer-buy handler. For non-trainer supersedes (e.g. quest rewards), the new
@ -19701,6 +19846,7 @@ void GameHandler::handleGroupList(network::Packet& packet) {
const bool hasRoles = isActiveExpansion("wotlk");
// Snapshot state before reset so we can detect transitions.
const uint32_t prevCount = partyData.memberCount;
const uint8_t prevLootMethod = partyData.lootMethod;
const bool wasInGroup = !partyData.isEmpty();
// Reset before parsing — SMSG_GROUP_LIST is a full replacement, not a delta.
// Without this, repeated GROUP_LIST packets push duplicate members.
@ -19717,6 +19863,14 @@ void GameHandler::handleGroupList(network::Packet& packet) {
} else if (nowInGroup && partyData.memberCount != prevCount) {
LOG_INFO("Group updated: ", partyData.memberCount, " members");
}
// Loot method change notification
if (wasInGroup && nowInGroup && partyData.lootMethod != prevLootMethod) {
static const char* kLootMethods[] = {
"Free for All", "Round Robin", "Master Looter", "Group Loot", "Need Before Greed"
};
const char* methodName = (partyData.lootMethod < 5) ? kLootMethods[partyData.lootMethod] : "Unknown";
addSystemChatMessage(std::string("Loot method changed to ") + methodName + ".");
}
// Fire GROUP_ROSTER_UPDATE / PARTY_MEMBERS_CHANGED for Lua addons
if (addonEventCallback_) {
addonEventCallback_("GROUP_ROSTER_UPDATE", {});
@ -20915,6 +21069,7 @@ void GameHandler::handleQuestPoiQueryResponse(network::Packet& packet) {
poi.name = questTitle.empty() ? "Quest objective" : questTitle;
LOG_DEBUG("Quest POI: questId=", questId, " mapId=", mapId,
" centroid=(", poi.x, ",", poi.y, ") title=", poi.name);
if (gossipPois_.size() >= 200) gossipPois_.erase(gossipPois_.begin());
gossipPois_.push_back(std::move(poi));
}
}
@ -20976,6 +21131,10 @@ void GameHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::strin
entry.title = title.empty() ? ("Quest #" + std::to_string(questId)) : title;
entry.objectives = objectives;
questLog_.push_back(std::move(entry));
if (addonEventCallback_) {
addonEventCallback_("QUEST_ACCEPTED", {std::to_string(questId)});
addonEventCallback_("QUEST_LOG_UPDATE", {});
}
}
bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
@ -21484,6 +21643,7 @@ void GameHandler::openVendor(uint64_t npcGuid) {
}
void GameHandler::closeVendor() {
bool wasOpen = vendorWindowOpen;
vendorWindowOpen = false;
currentVendorItems = ListInventoryData{};
buybackItems_.clear();
@ -21492,6 +21652,7 @@ void GameHandler::closeVendor() {
pendingBuybackWireSlot_ = 0;
pendingBuyItemId_ = 0;
pendingBuyItemSlot_ = 0;
if (wasOpen && addonEventCallback_) addonEventCallback_("MERCHANT_CLOSED", {});
}
void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) {
@ -22213,6 +22374,7 @@ void GameHandler::handleListInventory(network::Packet& packet) {
currentVendorItems.canRepair = savedCanRepair;
vendorWindowOpen = true;
gossipWindowOpen = false; // Close gossip if vendor opens
if (addonEventCallback_) addonEventCallback_("MERCHANT_SHOW", {});
// Auto-sell grey items if enabled
if (autoSellGrey_ && currentVendorItems.vendorGuid != 0) {
@ -22755,11 +22917,24 @@ void GameHandler::handleXpGain(network::Packet& packet) {
// but we can show combat text for XP gains
addCombatText(CombatTextEntry::XP_GAIN, static_cast<int32_t>(data.totalXp), 0, true);
std::string msg = "You gain " + std::to_string(data.totalXp) + " experience.";
// Build XP message with source creature name when available
std::string msg;
if (data.victimGuid != 0 && data.type == 0) {
// Kill XP — resolve creature name
std::string victimName = lookupName(data.victimGuid);
if (!victimName.empty())
msg = victimName + " dies, you gain " + std::to_string(data.totalXp) + " experience.";
else
msg = "You gain " + std::to_string(data.totalXp) + " experience.";
} else {
msg = "You gain " + std::to_string(data.totalXp) + " experience.";
}
if (data.groupBonus > 0) {
msg += " (+" + std::to_string(data.groupBonus) + " group bonus)";
}
addSystemChatMessage(msg);
if (addonEventCallback_)
addonEventCallback_("CHAT_MSG_COMBAT_XP_GAIN", {msg, std::to_string(data.totalXp)});
}
@ -22774,6 +22949,8 @@ void GameHandler::addMoneyCopper(uint32_t amount) {
msg += std::to_string(silver) + "s ";
msg += std::to_string(copper) + "c.";
addSystemChatMessage(msg);
if (addonEventCallback_)
addonEventCallback_("CHAT_MSG_MONEY", {msg});
}
void GameHandler::addSystemChatMessage(const std::string& message) {
@ -22954,7 +23131,24 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
mountCallback_(0);
}
// Clear world state for the new map
// Invoke despawn callbacks for all entities before clearing, so the renderer
// can release M2 instances, character models, and associated resources.
for (const auto& [guid, entity] : entityManager.getEntities()) {
if (guid == playerGuid) continue; // skip self
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
creatureDespawnCallback_(guid);
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
playerDespawnCallback_(guid);
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
gameObjectDespawnCallback_(guid);
}
}
otherPlayerVisibleItemEntries_.clear();
otherPlayerVisibleDirty_.clear();
otherPlayerMoveTimeMs_.clear();
unitCastStates_.clear();
unitAurasCache_.clear();
combatText.clear();
entityManager.clear();
hostileAttackers_.clear();
worldStates_.clear();
@ -24255,7 +24449,19 @@ void GameHandler::extractSkillFields(const std::map<uint16_t, uint32_t>& fields)
}
}
bool skillsChanged = (newSkills.size() != playerSkills_.size());
if (!skillsChanged) {
for (const auto& [id, sk] : newSkills) {
auto it = playerSkills_.find(id);
if (it == playerSkills_.end() || it->second.value != sk.value) {
skillsChanged = true;
break;
}
}
}
playerSkills_ = std::move(newSkills);
if (skillsChanged && addonEventCallback_)
addonEventCallback_("SKILL_LINES_CHANGED", {});
}
void GameHandler::extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields) {
@ -24587,11 +24793,13 @@ void GameHandler::updateAttachedTransportChildren(float /*deltaTime*/) {
// ============================================================
void GameHandler::closeMailbox() {
bool wasOpen = mailboxOpen_;
mailboxOpen_ = false;
mailboxGuid_ = 0;
mailInbox_.clear();
selectedMailIndex_ = -1;
showMailCompose_ = false;
if (wasOpen && addonEventCallback_) addonEventCallback_("MAIL_CLOSED", {});
}
void GameHandler::refreshMailList() {
@ -24768,6 +24976,7 @@ void GameHandler::handleShowMailbox(network::Packet& packet) {
hasNewMail_ = false;
selectedMailIndex_ = -1;
showMailCompose_ = false;
if (addonEventCallback_) addonEventCallback_("MAIL_SHOW", {});
// Request inbox contents
refreshMailList();
}
@ -24926,8 +25135,10 @@ void GameHandler::openBank(uint64_t guid) {
}
void GameHandler::closeBank() {
bool wasOpen = bankOpen_;
bankOpen_ = false;
bankerGuid_ = 0;
if (wasOpen && addonEventCallback_) addonEventCallback_("BANKFRAME_CLOSED", {});
}
void GameHandler::buyBankSlot() {
@ -24958,6 +25169,7 @@ void GameHandler::handleShowBank(network::Packet& packet) {
bankerGuid_ = packet.readUInt64();
bankOpen_ = true;
gossipWindowOpen = false; // Close gossip when bank opens
if (addonEventCallback_) addonEventCallback_("BANKFRAME_OPENED", {});
// Bank items are already tracked via update fields (bank slot GUIDs)
// Trigger rebuild to populate bank slots in inventory
rebuildOnlineInventory();
@ -25076,8 +25288,10 @@ void GameHandler::openAuctionHouse(uint64_t guid) {
}
void GameHandler::closeAuctionHouse() {
bool wasOpen = auctionOpen_;
auctionOpen_ = false;
auctioneerGuid_ = 0;
if (wasOpen && addonEventCallback_) addonEventCallback_("AUCTION_HOUSE_CLOSED", {});
}
void GameHandler::auctionSearch(const std::string& name, uint8_t levelMin, uint8_t levelMax,
@ -25158,6 +25372,7 @@ void GameHandler::handleAuctionHello(network::Packet& packet) {
auctionHouseId_ = data.auctionHouseId;
auctionOpen_ = true;
gossipWindowOpen = false; // Close gossip when auction house opens
if (addonEventCallback_) addonEventCallback_("AUCTION_HOUSE_SHOW", {});
auctionActiveTab_ = 0;
auctionBrowseResults_ = AuctionListResult{};
auctionOwnerResults_ = AuctionListResult{};
@ -25698,9 +25913,10 @@ void GameHandler::handleLootRollWon(network::Packet& packet) {
/*uint32_t slot =*/ packet.readUInt32();
uint64_t winnerGuid = packet.readUInt64();
uint32_t itemId = packet.readUInt32();
int32_t wonRandProp = 0;
if (isWotLK) {
/*uint32_t randSuffix =*/ packet.readUInt32();
/*uint32_t randProp =*/ packet.readUInt32();
wonRandProp = static_cast<int32_t>(packet.readUInt32());
}
uint8_t rollNum = packet.readUInt8();
uint8_t rollType = packet.readUInt8();
@ -25719,6 +25935,10 @@ void GameHandler::handleLootRollWon(network::Packet& packet) {
auto* info = getItemInfo(itemId);
std::string iName = info && !info->name.empty() ? info->name : std::to_string(itemId);
if (wonRandProp != 0) {
std::string suffix = getRandomPropertyName(wonRandProp);
if (!suffix.empty()) iName += " " + suffix;
}
uint32_t wonItemQuality = info ? info->quality : 1u;
std::string wonItemLink = buildItemLink(itemId, wonItemQuality, iName);

View file

@ -668,7 +668,7 @@ void WorldSocket::tryParsePackets() {
closeSocketNoJoin();
return;
}
constexpr uint16_t kMaxWorldPacketSize = 0x4000;
constexpr uint16_t kMaxWorldPacketSize = 0x8000; // 32KB — allows large guild rosters, auction lists
if (size > kMaxWorldPacketSize) {
LOG_ERROR("World packet framing desync: oversized packet size=", size,
" rawHdr=", std::hex,

View file

@ -313,8 +313,12 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec
return;
}
// Sun height attenuation — flare weakens when sun is near horizon (sunrise/sunset)
float sunHeight = sunDir.z; // z = up in render space; 0 = horizon, 1 = zenith
float heightFactor = glm::smoothstep(-0.05f, 0.25f, sunHeight);
// Atmospheric attenuation — fog, clouds, and weather reduce lens flare
float atmosphericFactor = 1.0f;
float atmosphericFactor = heightFactor;
atmosphericFactor *= (1.0f - glm::clamp(fogDensity * 0.8f, 0.0f, 0.9f)); // Heavy fog nearly kills flare
atmosphericFactor *= (1.0f - glm::clamp(cloudDensity * 0.6f, 0.0f, 0.7f)); // Clouds attenuate
atmosphericFactor *= (1.0f - glm::clamp(weatherIntensity * 0.9f, 0.0f, 0.95f)); // Rain/snow heavily attenuates
@ -339,6 +343,9 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
// Warm tint at sunrise/sunset — shift flare color toward orange/amber when sun is low
float warmTint = 1.0f - glm::smoothstep(0.05f, 0.35f, sunHeight);
// Render each flare element
for (const auto& element : flareElements) {
// Calculate position along sun-to-center axis
@ -347,12 +354,19 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec
// Apply visibility, intensity, and atmospheric attenuation
float brightness = element.brightness * visibility * intensityMultiplier * atmosphericFactor;
// Apply warm sunset/sunrise color shift
glm::vec3 tintedColor = element.color;
if (warmTint > 0.01f) {
glm::vec3 warmColor(1.0f, 0.6f, 0.25f); // amber/orange
tintedColor = glm::mix(tintedColor, warmColor, warmTint * 0.5f);
}
// Set push constants
FlarePushConstants push{};
push.position = position;
push.size = element.size;
push.aspectRatio = aspectRatio;
push.colorBrightness = glm::vec4(element.color, brightness);
push.colorBrightness = glm::vec4(tintedColor, brightness);
vkCmdPushConstants(cmd, pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,

View file

@ -261,6 +261,20 @@ void LoadingScreen::renderOverlay() {
ImVec2(0, 0), ImVec2(screenW, screenH));
}
// Zone name header
if (!zoneName.empty()) {
ImFont* font = ImGui::GetFont();
float zoneTextSize = 24.0f;
ImVec2 zoneSize = font->CalcTextSizeA(zoneTextSize, FLT_MAX, 0.0f, zoneName.c_str());
float zoneX = (screenW - zoneSize.x) * 0.5f;
float zoneY = screenH * 0.06f - 44.0f;
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddText(font, zoneTextSize, ImVec2(zoneX + 2.0f, zoneY + 2.0f),
IM_COL32(0, 0, 0, 200), zoneName.c_str());
dl->AddText(font, zoneTextSize, ImVec2(zoneX, zoneY),
IM_COL32(255, 220, 120, 255), zoneName.c_str());
}
// Progress bar
{
const float barWidthFrac = 0.6f;
@ -332,6 +346,22 @@ void LoadingScreen::render() {
ImVec2(0, 0), ImVec2(screenW, screenH));
}
// Zone name header (large text centered above progress bar)
if (!zoneName.empty()) {
ImFont* font = ImGui::GetFont();
float zoneTextSize = 24.0f;
ImVec2 zoneSize = font->CalcTextSizeA(zoneTextSize, FLT_MAX, 0.0f, zoneName.c_str());
float zoneX = (screenW - zoneSize.x) * 0.5f;
float zoneY = screenH * 0.06f - 44.0f; // above percentage text
ImDrawList* dl = ImGui::GetWindowDrawList();
// Drop shadow
dl->AddText(font, zoneTextSize, ImVec2(zoneX + 2.0f, zoneY + 2.0f),
IM_COL32(0, 0, 0, 200), zoneName.c_str());
// Gold text
dl->AddText(font, zoneTextSize, ImVec2(zoneX, zoneY),
IM_COL32(255, 220, 120, 255), zoneName.c_str());
}
// Progress bar (top of screen)
{
const float barWidthFrac = 0.6f;

View file

@ -3192,7 +3192,7 @@ void Renderer::update(float deltaTime) {
// Server-driven weather (SMSG_WEATHER) — authoritative
if (wType == 1) weather->setWeatherType(Weather::Type::RAIN);
else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW);
else if (wType == 3) weather->setWeatherType(Weather::Type::RAIN); // thunderstorm — use rain particles
else if (wType == 3) weather->setWeatherType(Weather::Type::STORM);
else weather->setWeatherType(Weather::Type::NONE);
weather->setIntensity(wInt);
} else {

View file

@ -135,6 +135,14 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
// --- Clouds (DBC-driven colors + sun lighting) ---
if (clouds_) {
// Sync cloud density with weather/DBC-driven cloud coverage.
// Active weather (rain/snow/storm) increases cloud density for visual consistency.
float effectiveDensity = params.cloudDensity;
if (params.weatherIntensity > 0.05f) {
float weatherBoost = params.weatherIntensity * 0.4f; // storms add up to 0.4 density
effectiveDensity = glm::min(1.0f, effectiveDensity + weatherBoost);
}
clouds_->setDensity(effectiveDensity);
clouds_->render(cmd, perFrameSet, params);
}

View file

@ -198,6 +198,10 @@ void Weather::update(const Camera& camera, float deltaTime) {
if (weatherType == Type::RAIN) {
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f); // Fast downward
p.maxLifetime = 5.0f;
} else if (weatherType == Type::STORM) {
// Storm: faster, angled rain with wind
p.velocity = glm::vec3(15.0f, -70.0f, 8.0f);
p.maxLifetime = 3.5f;
} else { // SNOW
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f); // Slow downward
p.maxLifetime = 10.0f;
@ -245,6 +249,12 @@ void Weather::updateParticle(Particle& particle, const Camera& camera, float del
particle.velocity.x = windX;
particle.velocity.z = windZ;
}
// Storm: gusty, turbulent wind with varying direction
if (weatherType == Type::STORM) {
float gust = std::sin(particle.lifetime * 1.5f + particle.position.x * 0.1f) * 5.0f;
particle.velocity.x = 15.0f + gust;
particle.velocity.z = 8.0f + std::cos(particle.lifetime * 2.0f) * 3.0f;
}
// Update position
particle.position += particle.velocity * deltaTime;
@ -275,6 +285,9 @@ void Weather::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (weatherType == Type::RAIN) {
push.particleSize = 3.0f;
push.particleColor = glm::vec4(0.7f, 0.8f, 0.9f, 0.6f);
} else if (weatherType == Type::STORM) {
push.particleSize = 3.5f;
push.particleColor = glm::vec4(0.6f, 0.65f, 0.75f, 0.7f); // Darker, more opaque
} else { // SNOW
push.particleSize = 8.0f;
push.particleColor = glm::vec4(1.0f, 1.0f, 1.0f, 0.9f);

View file

@ -720,6 +720,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderBgInvitePopup(gameHandler);
renderBfMgrInvitePopup(gameHandler);
renderLfgProposalPopup(gameHandler);
renderLfgRoleCheckPopup(gameHandler);
renderGuildRoster(gameHandler);
renderSocialFrame(gameHandler);
renderBuffBar(gameHandler);
@ -2616,24 +2617,32 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
static const std::vector<std::string> kCmds = {
"/afk", "/assist", "/away",
"/cancelaura", "/cancelform", "/cancelshapeshift",
"/cast", "/castsequence", "/chathelp", "/clear", "/clearfocus", "/cleartarget",
"/combatlog", "/dance", "/dismount", "/dnd", "/do", "/duel",
"/cancelaura", "/cancelform", "/cancellogout", "/cancelshapeshift",
"/cast", "/castsequence", "/chathelp", "/clear", "/clearfocus",
"/clearmainassist", "/clearmaintank", "/cleartarget", "/cloak",
"/combatlog", "/dance", "/dismount", "/dnd", "/do", "/duel", "/dump",
"/e", "/emote", "/equip", "/equipset",
"/focus", "/follow", "/forfeit", "/friend",
"/g", "/ginvite", "/gmticket", "/grouploot", "/guild", "/guildinfo",
"/g", "/gdemote", "/ginvite", "/gkick", "/gleader", "/gmotd",
"/gmticket", "/gpromote", "/gquit", "/grouploot", "/groster",
"/guild", "/guildinfo",
"/helm", "/help",
"/i", "/ignore", "/inspect", "/instance", "/invite",
"/j", "/join", "/kick", "/kneel",
"/l", "/leave", "/loc", "/local", "/logout",
"/macrohelp", "/mark", "/me",
"/l", "/leave", "/leaveparty", "/loc", "/local", "/logout",
"/macrohelp", "/mainassist", "/maintank", "/mark", "/me",
"/notready",
"/p", "/party", "/petaggressive", "/petattack", "/petdefensive",
"/petdismiss", "/petfollow", "/pethalt", "/petpassive", "/petstay",
"/played", "/pvp",
"/r", "/raid", "/raidinfo", "/raidwarning", "/random", "/reply", "/roll", "/run",
"/s", "/say", "/screenshot", "/setloot", "/shout", "/sit", "/stand",
"/r", "/raid", "/raidinfo", "/raidwarning", "/random", "/ready",
"/readycheck", "/reload", "/reloadui", "/removefriend",
"/reply", "/rl", "/roll", "/run",
"/s", "/say", "/score", "/screenshot", "/script", "/setloot",
"/shout", "/sit", "/stand",
"/startattack", "/stopattack", "/stopcasting", "/stopfollow", "/stopmacro",
"/t", "/target", "/threat", "/time", "/trade",
"/t", "/target", "/targetenemy", "/targetfriend", "/targetlast",
"/threat", "/ticket", "/time", "/trade",
"/unignore", "/uninvite", "/unstuck", "/use",
"/w", "/whisper", "/who", "/wts", "/wtb",
"/y", "/yell", "/zone"
@ -6007,6 +6016,30 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
return;
}
// /dump <expression> — evaluate Lua expression and print result
if ((cmdLower == "dump" || cmdLower == "print") && spacePos != std::string::npos) {
std::string expr = command.substr(spacePos + 1);
auto* am = core::Application::getInstance().getAddonManager();
if (am && am->isInitialized()) {
// Wrap expression in print(tostring(...)) to display the value
std::string wrapped = "local __v = " + expr +
"; if type(__v) == 'table' then "
" local parts = {} "
" for k,v in pairs(__v) do parts[#parts+1] = tostring(k)..'='..tostring(v) end "
" print('{' .. table.concat(parts, ', ') .. '}') "
"else print(tostring(__v)) end";
am->runScript(wrapped);
} else {
game::MessageChatData errMsg;
errMsg.type = game::ChatType::SYSTEM;
errMsg.language = game::ChatLanguage::UNIVERSAL;
errMsg.message = "Addon system not initialized.";
gameHandler.addLocalChatMessage(errMsg);
}
chatInputBuffer[0] = '\0';
return;
}
// Check addon slash commands (SlashCmdList) before built-in commands
{
auto* am = core::Application::getInstance().getAddonManager();
@ -6034,6 +6067,30 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
return;
}
// /reload or /reloadui — reload all addons (save variables, re-init Lua, re-scan .toc files)
if (cmdLower == "reload" || cmdLower == "reloadui" || cmdLower == "rl") {
auto* am = core::Application::getInstance().getAddonManager();
if (am) {
am->reload();
am->fireEvent("VARIABLES_LOADED");
am->fireEvent("PLAYER_LOGIN");
am->fireEvent("PLAYER_ENTERING_WORLD");
game::MessageChatData rlMsg;
rlMsg.type = game::ChatType::SYSTEM;
rlMsg.language = game::ChatLanguage::UNIVERSAL;
rlMsg.message = "Interface reloaded.";
gameHandler.addLocalChatMessage(rlMsg);
} else {
game::MessageChatData rlMsg;
rlMsg.type = game::ChatType::SYSTEM;
rlMsg.language = game::ChatLanguage::UNIVERSAL;
rlMsg.message = "Addon system not available.";
gameHandler.addLocalChatMessage(rlMsg);
}
chatInputBuffer[0] = '\0';
return;
}
// /stopmacro [conditions]
// Halts execution of the current macro (remaining lines are skipped).
// With a condition block, only stops if the conditions evaluate to true.
@ -7083,6 +7140,12 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
return;
}
if (cmdLower == "cancelqueuedspell" || cmdLower == "stopspellqueue") {
gameHandler.cancelQueuedSpell();
chatInputBuffer[0] = '\0';
return;
}
// /equipset [name] — equip a saved equipment set by name (partial match, case-insensitive)
// /equipset — list available sets in chat
if (cmdLower == "equipset") {
@ -9412,7 +9475,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
}
}
if (totalCount > 0) {
char countStr[8];
char countStr[16];
snprintf(countStr, sizeof(countStr), "%d", totalCount);
ImVec2 btnMax = ImGui::GetItemRectMax();
ImVec2 tsz = ImGui::CalcTextSize(countStr);
@ -14201,6 +14264,71 @@ void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
ImGui::PopStyleColor(3);
}
void GameScreen::renderLfgRoleCheckPopup(game::GameHandler& gameHandler) {
using LfgState = game::GameHandler::LfgState;
if (gameHandler.getLfgState() != LfgState::RoleCheck) return;
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 160.0f, screenH / 2.0f - 80.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(320.0f, 0.0f), ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.96f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.5f, 0.9f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.1f, 0.1f, 0.3f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
const ImGuiWindowFlags flags =
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
if (ImGui::Begin("Role Check##LfgRoleCheck", nullptr, flags)) {
ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "Confirm your role:");
ImGui::Spacing();
// Role checkboxes
bool isTank = (lfgRoles_ & 0x02) != 0;
bool isHealer = (lfgRoles_ & 0x04) != 0;
bool isDps = (lfgRoles_ & 0x08) != 0;
if (ImGui::Checkbox("Tank", &isTank)) lfgRoles_ = (lfgRoles_ & ~0x02) | (isTank ? 0x02 : 0);
ImGui::SameLine(120.0f);
if (ImGui::Checkbox("Healer", &isHealer)) lfgRoles_ = (lfgRoles_ & ~0x04) | (isHealer ? 0x04 : 0);
ImGui::SameLine(220.0f);
if (ImGui::Checkbox("DPS", &isDps)) lfgRoles_ = (lfgRoles_ & ~0x08) | (isDps ? 0x08 : 0);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool hasRole = (lfgRoles_ & 0x0E) != 0;
if (!hasRole) ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.4f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
if (ImGui::Button("Accept", ImVec2(140.0f, 28.0f))) {
gameHandler.lfgSetRoles(lfgRoles_);
}
ImGui::PopStyleColor(2);
if (!hasRole) ImGui::EndDisabled();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.15f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.6f, 0.2f, 0.2f, 1.0f));
if (ImGui::Button("Leave Queue", ImVec2(140.0f, 28.0f))) {
gameHandler.lfgLeave();
}
ImGui::PopStyleColor(2);
}
ImGui::End();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
}
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
// Guild Roster toggle (customizable keybind)
if (!chatInputActive && !ImGui::GetIO().WantTextInput &&
@ -22177,6 +22305,12 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
const auto& auction = results.auctions[i];
auto* info = gameHandler.getItemInfo(auction.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(auction.itemEntry));
// Append random suffix name (e.g., "of the Eagle") if present
if (auction.randomPropertyId != 0) {
std::string suffix = gameHandler.getRandomPropertyName(
static_cast<int32_t>(auction.randomPropertyId));
if (!suffix.empty()) name += " " + suffix;
}
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImVec4 qc = InventoryScreen::getQualityColor(quality);
@ -22370,6 +22504,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
const auto& a = results.auctions[bi];
auto* info = gameHandler.getItemInfo(a.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
if (a.randomPropertyId != 0) {
std::string suffix = gameHandler.getRandomPropertyName(
static_cast<int32_t>(a.randomPropertyId));
if (!suffix.empty()) name += " " + suffix;
}
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImVec4 bqc = InventoryScreen::getQualityColor(quality);
@ -22382,6 +22521,15 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::SameLine();
}
}
// High bidder indicator
bool isHighBidder = (a.bidderGuid != 0 && a.bidderGuid == gameHandler.getPlayerGuid());
if (isHighBidder) {
ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "[Winning]");
ImGui::SameLine();
} else if (a.bidderGuid != 0) {
ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[Outbid]");
ImGui::SameLine();
}
ImGui::TextColored(bqc, "%s", name.c_str());
// Tooltip and shift-click
if (ImGui::IsItemHovered() && info && info->valid)
@ -22445,6 +22593,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
const auto& a = results.auctions[i];
auto* info = gameHandler.getItemInfo(a.itemEntry);
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
if (a.randomPropertyId != 0) {
std::string suffix = gameHandler.getRandomPropertyName(
static_cast<int32_t>(a.randomPropertyId));
if (!suffix.empty()) name += " " + suffix;
}
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
ImGui::TableNextRow();
@ -22457,6 +22610,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
ImGui::SameLine();
}
}
// Bid activity indicator for seller
if (a.bidderGuid != 0) {
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "[Bid]");
ImGui::SameLine();
}
ImGui::TextColored(oqc, "%s", name.c_str());
if (ImGui::IsItemHovered() && info && info->valid)
inventoryScreen.renderItemTooltip(*info);