Compare commits

...

42 commits

Author SHA1 Message Date
Kelsi
5513c4aad5 fix: apply skull-red color and "Lv ??" to level-0 mobs in focus frame
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
Consistent with the target frame fix: focus targets with level 0
(unknown/?? mobs) now show skull-red instead of grey, and display
"Lv ??" instead of "Lv 0".
2026-03-17 14:18:49 -07:00
Kelsi
39f4162ec1 fix: show skull-red color and "Lv ??" for unknown-level mobs in target frame
Level 0 in the update fields means the server hasn't sent or
the mob is undetectable (e.g. high-level raid bosses). Previously
these were colored grey (no-XP path) and displayed "Lv 0". Now
they correctly show skull-red and display "Lv ??" to match WoW.
2026-03-17 14:16:14 -07:00
Kelsi
8b9d626aec feat: show directional arrow on world map player marker
Replace the static filled circle with a red triangle arrow that
rotates to match the character's current facing direction.
Uses the same render-space yaw convention as the 3D scene so
the arrow matches in-world orientation.
2026-03-17 14:10:56 -07:00
Kelsi
b23dbc9ab7 feat: apply out-of-range red tint to ranged items on action bar
Extend the existing out-of-range check to cover ITEM slots whose
inventory type is Ranged (bow/gun/crossbow, 40 yd), RangedRight
(wand, 40 yd), or Thrown (30 yd). The check runs after barItemDef
is resolved so the inventory type is available.
2026-03-17 13:59:42 -07:00
Kelsi
5031351736 fix: add free-list to WardenEmulator heap allocator to prevent exhaustion
The bump-pointer allocator never reused freed blocks, causing the 16 MB
emulated heap to exhaust in long sessions even when blocks were freed.

- First-fit reuse from a free-list before advancing the bump pointer
- Coalesce adjacent free blocks to limit fragmentation
- Roll back the bump pointer when the top free block reaches it
- Reset allocator state on initialize() so re-runs start clean
2026-03-17 13:55:37 -07:00
Kelsi
ae40d393c3 feat: show tactical role badges in party frames; fix talent reset
- Add MT/MA/Asst badges to party frames (matching raid frame treatment)
- Clear learnedTalents_ on SMSG_TALENTS_INVOLUNTARILY_RESET so the
  talent screen stays accurate after a server-side talent wipe
2026-03-17 13:50:49 -07:00
Kelsi
f70df191a9 feat: show tactical role badges (MT/MA/Asst) in raid frames
Render "MT" (orange), "MA" (blue), and "A" (light blue) in the
bottom-left of each raid cell using member flags from SMSG_GROUP_LIST
and SMSG_REAL_GROUP_UPDATE (bits 0x02/0x04/0x01). Complements the
existing LFG role badges at bottom-right.
2026-03-17 13:47:53 -07:00
Kelsi
1daead3767 feat: implement SMSG_REAL_GROUP_UPDATE handler
Parse group type, member flags, and leader GUID instead of silently
discarding the packet. Updates partyData so group frames reflect
role changes and leadership transitions in real time.
2026-03-17 13:44:14 -07:00
Kelsi
a43a43ed8e fix: evict oldest minimap tile textures when cache exceeds 128 entries
Prevents unbounded GPU memory growth in long play sessions where the player
visits many zones. Tiles are inserted into a FIFO deque; when the count of
successfully-loaded tiles exceeds MAX_TILE_CACHE (128), the oldest entry is
destroyed and removed from both the cache map and the deque.

At 256×256×4 bytes per tile this caps minimap GPU usage at ~32 MB.
2026-03-17 13:38:18 -07:00
Kelsi
217edc81d9 feat: add item quality link colours to loot roll, loot notify, and loot all-passed messages 2026-03-17 13:33:07 -07:00
Kelsi
6260ac281e feat: extend item link quality colours to vendor purchase, pet feed, and LFG reward messages 2026-03-17 13:27:27 -07:00
Kelsi
29b5b6f959 feat: show item quality colours in loot, quest-item, and auction chat messages
Add buildItemLink() helper that formats |cff...|Hitem:...|h[Name]|h|r links so
the chat renderer draws item names in their quality colour (grey/white/green/
blue/purple/orange) with a small icon and tooltip on hover.

Applied to: loot received (SMSG_ITEM_PUSH_RESULT), looted from corpse
(handleLootRemoved), quest item count updates, and all three auction
house notifications (sold, outbid, expired).
2026-03-17 13:25:33 -07:00
Kelsi
4049f73ca6 refactor: replace raw console output with LOG_* macros in warden_emulator, transport_manager, keybinding_manager 2026-03-17 13:09:18 -07:00
Kelsi
bf5219c822 refactor: replace std::cout/cerr with LOG_* macros in warden_module.cpp
Convert 60+ raw console output calls to structured LOG_INFO/WARNING/ERROR
macros for consistent logging, proper timestamps, and filtering support.
Remove unused <iostream> include.
2026-03-17 13:04:25 -07:00
Kelsi
8169f5d5c0 feat: add audio feedback for level-up, achievements, duels, group invites, and inventory errors
Wire up remaining UISoundManager calls for milestone and notification events:
- playLevelUp() on SMSG_LEVELUP_INFO
- playAchievementAlert() on SMSG_ACHIEVEMENT_EARNED (self only)
- playTargetSelect() on duel request and group invite
- playError() on inventory change failure
2026-03-17 12:37:19 -07:00
Kelsi
119002626e feat: show chat message when a spell is removed from spellbook
handleRemovedSpell now displays "You have unlearned: [SpellName]."
matching the existing handleLearnedSpell feedback pattern.
2026-03-17 12:35:05 -07:00
Kelsi
6fbf5b5797 feat: add audio feedback for item loot, vendor buy/sell, and spell learning
Wire up remaining UISoundManager calls for core gameplay actions:
- playLootItem() on SMSG_ITEM_PUSH_RESULT and handleLootRemoved
- playPickupBag() on successful vendor purchase (SMSG_BUY_ITEM)
- playDropOnGround() on successful item sell (SMSG_SELL_ITEM)
- playQuestActivate() on trainer spell purchase success
2026-03-17 12:31:38 -07:00
Kelsi
a0b978f95b feat: add audio feedback for quest accept/complete and transaction errors
Wire up UISoundManager calls that were loaded but never invoked:
- playQuestActivate() on quest accept
- playQuestComplete() on server-confirmed quest completion
- playError() on trainer buy failure, vendor buy failure, and sell failure
2026-03-17 12:28:15 -07:00
Kelsi
8c3060f261 feat: show XP percentage in experience bar tooltip
The XP bar tooltip now displays current progress as a percentage
(e.g., "Current: 45000 / 100000 XP (45.0%)"), making it easier to
gauge leveling progress at a glance.
2026-03-17 12:17:23 -07:00
Kelsi
b80d88bded feat: add 'Hold Shift to compare' hint to ItemDef tooltip
The ItemQueryResponseData tooltip overload had this hint but the
primary ItemDef overload did not. Players hovering gear in their
inventory now see the comparison prompt when an equipped equivalent
exists.
2026-03-17 12:12:11 -07:00
Kelsi
1c3f2f4ae3 feat: show exploration XP as floating combat text
Area discovery XP was only shown in the system chat log. Now it also
appears as a floating "+XP" number like kill XP, giving immediate
visual feedback when discovering new zones.
2026-03-17 12:02:17 -07:00
Kelsi
67e6c9a984 fix: TBC parseMailList returns true on empty mailbox for consistency
WotLK and Classic parsers return true on success regardless of mail
count, but TBC returned !inbox.empty() which falsely signals parse
failure on an empty mailbox, potentially causing callers to skip
valid empty-mailbox state.
2026-03-17 11:58:20 -07:00
Kelsi
9750110436 fix: complete item tooltip stat comparison for all secondary stats
The shift-hover gear comparison was missing secondary stat types
(Defense, Dodge, Parry, Block Rating, Hit/Crit/Haste variants,
Healing, Spell Damage, Spell Pen) — only 10 of 22 stat types had
labels. Also adds full extra stats and DPS comparison to the
ItemQueryResponseData tooltip overload (loot window) which had none.
2026-03-17 11:54:01 -07:00
Kelsi
c017c61d2c fix: remove unused syncCounts variable in Warden handler
Eliminates the -Wunused-variable warning for the syncCounts array
that was declared but never populated in the synchronous Warden
check response path.
2026-03-17 11:37:49 -07:00
Kelsi
ef5532cf15 fix: add TBC chat message parser to prevent 12-byte misalignment
TBC 2.4.3 SMSG_MESSAGECHAT has no senderGuid(u64) or unknown(u32)
prefix before type-specific data. The WotLK base parser reads these
12 bytes unconditionally, causing complete misalignment of all chat
message fields — every chat message on a TBC server would parse
garbage for sender, channel, and message content.
2026-03-17 11:23:37 -07:00
Kelsi
e1be8667ed fix: add TBC game object query parser for correct string count
TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE has 2 extra strings after
name[4] (iconName + castBarCaption). WotLK has 3 (adds unk1). Without
this override, the WotLK parser's third readString() consumed bytes
from the data[24] fields, corrupting game object type-specific data
and breaking interactions with doors, chests, mailboxes, and
transports on TBC servers.
2026-03-17 11:20:50 -07:00
Kelsi
1b86f76d31 fix: add TBC overrides for quest giver status and channel packets
TBC 2.4.3 sends quest giver status as uint32 (like Classic), not uint8
(WotLK). Without this override, reading uint8 consumed only 1 of 4
bytes, misaligning all subsequent packet data and breaking quest
markers on NPCs.

TBC channel join/leave packets use Classic format (name+password only).
The WotLK base prepends channelId/hasVoice/joinedByZone, causing
servers to reject the malformed packets and breaking channel features.
2026-03-17 11:16:02 -07:00
Kelsi
dc8619464a fix: add TBC guild roster parser to avoid gender byte misalignment
TBC 2.4.3 SMSG_GUILD_ROSTER has the same rank structure as WotLK
(variable rankCount + goldLimit + bank tab permissions), but does NOT
include a gender byte per member (WotLK added it). Without this
override, TBC fell through to the WotLK parser which read a spurious
gender byte, causing every subsequent field in each member entry to
misalign.
2026-03-17 11:10:54 -07:00
Kelsi
a7f7c4aa93 feat: show power type names in combat log energize/drain entries
Combat log now shows specific power type names (Mana, Rage, Energy,
Focus, Happiness, Runic Power) instead of generic "power" for ENERGIZE
and POWER_DRAIN events. Uses the powerType field added to CombatLogEntry
in the previous commit.
2026-03-17 11:03:20 -07:00
Kelsi
01685cc0bb feat: add ghost mode visual overlay when player is dead
Apply a cold blue-grey fullscreen overlay when the player is in ghost
form, creating a desaturated, muted appearance that clearly signals the
death state. Uses the existing overlay pipeline infrastructure. Applied
in both parallel and non-parallel rendering paths, after underwater tint
but before brightness adjustment so UI elements remain unaffected.
2026-03-17 10:58:07 -07:00
Kelsi
2d53ff0c07 feat: show environmental damage type in combat text and log
Fall, lava, drowning, fatigue, slime, and fire damage now display their
specific type instead of generic "Environmental damage" in both floating
combat text and the combat log window. The envType byte from
SMSG_ENVIRONMENTAL_DAMAGE_LOG is propagated via the powerType field to
the display layer. Added powerType to CombatLogEntry for consistent
access in the persistent combat log.
2026-03-17 10:54:07 -07:00
Kelsi
1152a70201 fix: handle transport data in other player movement packets
Other players on transports (boats, zeppelins, trams) were not properly
tracked because handleOtherPlayerMovement() did not read transport data
from MSG_MOVE_* packets. This caused entities to slide off transports
between movement updates since no transport attachment was established.

Now reads the transport GUID and local offset from the packet using
expansion-aware wire flags (0x200 for WotLK/TBC, 0x02000000 for
Classic/Turtle), registers a transport attachment so the entity follows
the transport smoothly via updateAttachedTransportChildren(), and clears
the attachment when the player disembarks.
2026-03-17 10:40:35 -07:00
Kelsi
f5297f9945 feat: show craft queue count on cast bar during batch crafting 2026-03-17 10:30:18 -07:00
Kelsi
9aed192503 fix: load skill DBCs on login and handle loot slot changes
- Load SkillLine.dbc and SkillLineAbility.dbc during SMSG_INITIAL_SPELLS
  so isProfessionSpell() works immediately without visiting a trainer
- Implement SMSG_LOOT_SLOT_CHANGED handler to remove items taken by
  other players in group loot, keeping the loot window in sync
2026-03-17 10:20:29 -07:00
Kelsi
7b03d5363b feat: profession crafting improvements and combat sound fixes
- Suppress spell sounds for profession/tradeskill spells (crafting is silent)
- Add craft quantity UI to profession trainer: recipe selector, quantity
  input, Create button, and Stop button for active queue
- Known recipes show Create button to cast directly from trainer window
- Craft queue auto-recasts on CREATE_ITEM completion, cancels on failure
- Fix missing combat sounds: player spell impacts on enemies, enemy spell
  cast sounds targeting player, instant melee ability weapon sounds
2026-03-17 10:12:49 -07:00
Kelsi
502d506a44 feat: make bag windows draggable 2026-03-17 10:12:35 -07:00
Kelsi
192c6175b8 feat: add brightness slider to Video settings
Black overlay dims below 50%, white overlay brightens above 50%.
Persisted in settings.cfg, with restore-defaults support.
2026-03-17 09:04:53 -07:00
Kelsi
cf3fe70f1f fix: hide window on shutdown to prevent OS force-close dialog
SDL_HideWindow immediately on shutdown so the OS doesn't show a
"not responding" dialog during the slow cleanup process.
2026-03-17 09:04:47 -07:00
Kelsi
3667ff4998 fix: use uniform 22-byte loot item size for Classic/TBC/Turtle
SMSG_LOOT_RESPONSE items include randomSuffix and randomPropertyId
fields across all expansions, not just WotLK. Using 14-byte size for
Classic/TBC caused item data to be read at wrong offsets.
2026-03-17 09:04:40 -07:00
Kelsi
203514abc7 fix: correct minimap orientation and arrow direction, compact key ring UI
Remove stray X-flip in minimap display shader that mirrored the map
horizontally (West on right instead of East). Fix arrow rotation
fallback path (missing negation) and add character-facing-relative
arrow in rotateWithCamera mode.

Compact key ring: 24px slots in 8-column grid, only show rows with
items, hide when empty. Add Show Key Ring toggle in Settings with
persistence.
2026-03-17 08:18:46 -07:00
Kelsi
e38324619e fix: resolve missing Classic spell icons on action bar and talents
When Classic is active, loadDBC("Spell.dbc") finds the WotLK base DBC
(234 fields) since no binary Classic DBC exists. The Classic layout says
IconID is at field 117, but in the WotLK DBC that field contains
unrelated data (mostly zeros). This caused all spell icon lookups to
fail silently.

Now detects the DBC/layout field count mismatch and falls back to the
WotLK field index 133, which is correct for the base DBC. Classic spell
IDs are a subset of WotLK, so the icon mapping works correctly.
2026-03-17 07:42:01 -07:00
Kelsi
8378eb9232 fix: correct sync Warden MODULE check returning 0x01 instead of 0x00
The sync path's MODULE handler was returning 0x01 (module found) for
unwanted cheat DLLs (WPESPY, TAMIA, PRXDRVPE, etc.) instead of 0x00
(not found). Since VMaNGOS compares the result as a boolean, returning
any non-zero value for a cheat module tells the server "this cheat DLL
is loaded," triggering Warden penalties that accumulate into a kick
after ~3-5 minutes.

Also adds ±4KB hint window search to searchCodePattern for faster
PAGE_A resolution without full brute-force, and restores the turtle
PAGE_A fallback (confirmed patterns are runtime-patched offsets not
present in the on-disk PE).
2026-03-17 07:19:37 -07:00
28 changed files with 1804 additions and 510 deletions

View file

@ -40,7 +40,7 @@ void main() {
float cs = cos(push.rotation); float cs = cos(push.rotation);
float sn = sin(push.rotation); float sn = sin(push.rotation);
vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs); vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs);
vec2 mapUV = push.playerUV + vec2(-rotated.x, rotated.y) * push.zoomRadius * 2.0; vec2 mapUV = push.playerUV + vec2(rotated.x, rotated.y) * push.zoomRadius * 2.0;
vec4 mapColor = texture(uComposite, mapUV); vec4 mapColor = texture(uComposite, mapUV);

View file

@ -786,6 +786,12 @@ public:
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
float getCastTimeRemaining() const { return castTimeRemaining; } float getCastTimeRemaining() const { return castTimeRemaining; }
// Repeat-craft queue
void startCraftQueue(uint32_t spellId, int count);
void cancelCraftQueue();
int getCraftQueueRemaining() const { return craftQueueRemaining_; }
uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; }
// Unit cast state (tracked per GUID for target frame + boss frames) // Unit cast state (tracked per GUID for target frame + boss frames)
struct UnitCastState { struct UnitCastState {
bool casting = false; bool casting = false;
@ -970,6 +976,7 @@ public:
const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; } const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; }
const std::string& getSkillName(uint32_t skillId) const; const std::string& getSkillName(uint32_t skillId) const;
uint32_t getSkillCategory(uint32_t skillId) const; uint32_t getSkillCategory(uint32_t skillId) const;
bool isProfessionSpell(uint32_t spellId) const;
// World entry callback (online mode - triggered when entering world) // World entry callback (online mode - triggered when entering world)
// Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect // Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect
@ -2669,6 +2676,9 @@ private:
bool castIsChannel = false; bool castIsChannel = false;
uint32_t currentCastSpellId = 0; uint32_t currentCastSpellId = 0;
float castTimeRemaining = 0.0f; float castTimeRemaining = 0.0f;
// Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes
uint32_t craftQueueSpellId_ = 0;
int craftQueueRemaining_ = 0;
// Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START) // Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START)
std::unordered_map<uint64_t, UnitCastState> unitCastStates_; std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
uint64_t pendingGameObjectInteractGuid_ = 0; uint64_t pendingGameObjectInteractGuid_ = 0;

View file

@ -26,6 +26,10 @@ public:
// Classic: none, TBC: u8, WotLK: u16. // Classic: none, TBC: u8, WotLK: u16.
virtual uint8_t movementFlags2Size() const { return 2; } virtual uint8_t movementFlags2Size() const { return 2; }
// Wire-format movement flag that gates transport data in MSG_MOVE_* payloads.
// WotLK/TBC: 0x200, Classic/Turtle: 0x02000000.
virtual uint32_t wireOnTransportFlag() const { return 0x00000200; }
// --- Movement --- // --- Movement ---
/** Parse movement block from SMSG_UPDATE_OBJECT */ /** Parse movement block from SMSG_UPDATE_OBJECT */
@ -361,6 +365,20 @@ public:
// TBC/Classic SMSG_QUESTGIVER_QUEST_DETAILS lacks informUnit(u64), flags(u32), // TBC/Classic SMSG_QUESTGIVER_QUEST_DETAILS lacks informUnit(u64), flags(u32),
// isFinished(u8) that WotLK added; uses variable item counts + emote section. // isFinished(u8) that WotLK added; uses variable item counts + emote section.
bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override; bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override;
// TBC 2.4.3 SMSG_GUILD_ROSTER: same rank structure as WotLK (variable rankCount +
// goldLimit + bank tabs), but NO gender byte per member (WotLK added it)
bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override;
// TBC 2.4.3 SMSG_QUESTGIVER_STATUS: uint32 status (WotLK uses uint8)
uint8_t readQuestGiverStatus(network::Packet& packet) override;
// TBC 2.4.3 SMSG_MESSAGECHAT: no senderGuid/unknown prefix before type-specific data
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
// TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE: 2 extra strings after names
// (iconName + castBarCaption); WotLK has 3 (adds unk1)
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
// TBC 2.4.3 CMSG_JOIN_CHANNEL: name+password only (WotLK prepends channelId+hasVoice+joinedByZone)
network::Packet buildJoinChannel(const std::string& channelName, const std::string& password) override;
// TBC 2.4.3 CMSG_LEAVE_CHANNEL: name only (WotLK prepends channelId)
network::Packet buildLeaveChannel(const std::string& channelName) override;
}; };
/** /**
@ -380,6 +398,7 @@ public:
class ClassicPacketParsers : public TbcPacketParsers { class ClassicPacketParsers : public TbcPacketParsers {
public: public:
uint8_t movementFlags2Size() const override { return 0; } uint8_t movementFlags2Size() const override { return 0; }
uint32_t wireOnTransportFlag() const override { return 0x02000000; }
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override; bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override; bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override; void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;

View file

@ -74,6 +74,7 @@ struct CombatLogEntry {
int32_t amount = 0; int32_t amount = 0;
uint32_t spellId = 0; uint32_t spellId = 0;
bool isPlayerSource = false; bool isPlayerSource = false;
uint8_t powerType = 0; // For ENERGIZE/DRAIN: power type; for ENVIRONMENTAL: env damage type
time_t timestamp = 0; // Wall-clock time (std::time(nullptr)) time_t timestamp = 0; // Wall-clock time (std::time(nullptr))
std::string sourceName; // Resolved display name of attacker/caster std::string sourceName; // Resolved display name of attacker/caster
std::string targetName; // Resolved display name of victim/target std::string targetName; // Resolved display name of victim/target

View file

@ -152,6 +152,7 @@ private:
// Memory allocation tracking // Memory allocation tracking
std::map<uint32_t, size_t> allocations_; std::map<uint32_t, size_t> allocations_;
std::map<uint32_t, size_t> freeBlocks_; // free-list keyed by base address
uint32_t nextHeapAddr_; uint32_t nextHeapAddr_;
// Hook handles for cleanup // Hook handles for cleanup

View file

@ -41,10 +41,12 @@ public:
* @param expectedHash 20-byte expected HMAC-SHA1 digest * @param expectedHash 20-byte expected HMAC-SHA1 digest
* @param patternLen Length of the pattern to search for * @param patternLen Length of the pattern to search for
* @param imageOnly If true, search only executable sections (.text) * @param imageOnly If true, search only executable sections (.text)
* @param hintOffset RVA hint from PAGE_A request check this position first
* @return true if a matching pattern was found in the PE image * @return true if a matching pattern was found in the PE image
*/ */
bool searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], bool searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20],
uint8_t patternLen, bool imageOnly) const; uint8_t patternLen, bool imageOnly,
uint32_t hintOffset = 0, bool hintOnly = false) const;
/** Write a little-endian uint32 at the given virtual address in the PE image. */ /** Write a little-endian uint32 at the given virtual address in the PE image. */
void writeLE32(uint32_t va, uint32_t value); void writeLE32(uint32_t va, uint32_t value);

View file

@ -2060,8 +2060,9 @@ public:
/** SMSG_LOOT_RESPONSE parser */ /** SMSG_LOOT_RESPONSE parser */
class LootResponseParser { class LootResponseParser {
public: public:
// isWotlkFormat: true for WotLK 3.3.5a (22 bytes/item with randomSuffix+randomProp), // isWotlkFormat: true for WotLK (has trailing quest item section),
// false for Classic 1.12 and TBC 2.4.3 (14 bytes/item). // false for Classic/TBC (no quest item section).
// Per-item size is 22 bytes across all expansions.
static bool parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat = true); static bool parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat = true);
}; };

View file

@ -7,6 +7,7 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <deque>
#include <algorithm> #include <algorithm>
namespace wowee { namespace wowee {
@ -73,7 +74,10 @@ private:
bool trsParsed = false; bool trsParsed = false;
// Tile texture cache: hash → VkTexture // Tile texture cache: hash → VkTexture
// Evicted (FIFO) when the count of successfully-loaded tiles exceeds MAX_TILE_CACHE.
static constexpr size_t MAX_TILE_CACHE = 128;
std::unordered_map<std::string, std::unique_ptr<VkTexture>> tileTextureCache; std::unordered_map<std::string, std::unique_ptr<VkTexture>> tileTextureCache;
std::deque<std::string> tileInsertionOrder; // hashes of successfully loaded tiles, oldest first
std::unique_ptr<VkTexture> noDataTexture; std::unique_ptr<VkTexture> noDataTexture;
// Composite render target (3x3 tiles = 768x768) // Composite render target (3x3 tiles = 768x768)

View file

@ -381,6 +381,13 @@ private:
void initOverlayPipeline(); void initOverlayPipeline();
void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE); void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE);
// Brightness (1.0 = default, <1 darkens, >1 brightens)
float brightness_ = 1.0f;
public:
void setBrightness(float b) { brightness_ = b; }
float getBrightness() const { return brightness_; }
private:
// FSR 1.0 upscaling state // FSR 1.0 upscaling state
struct FSRState { struct FSRState {
bool enabled = false; bool enabled = false;

View file

@ -51,7 +51,8 @@ public:
void compositePass(VkCommandBuffer cmd); void compositePass(VkCommandBuffer cmd);
/// ImGui overlay — call INSIDE the main render pass (during ImGui frame). /// ImGui overlay — call INSIDE the main render pass (during ImGui frame).
void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight,
float playerYawDeg = 0.0f);
void setMapName(const std::string& name); void setMapName(const std::string& name);
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData); void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
@ -71,7 +72,8 @@ private:
float& top, float& bottom) const; float& top, float& bottom) const;
void loadZoneTextures(int zoneIdx); void loadZoneTextures(int zoneIdx);
void requestComposite(int zoneIdx); void requestComposite(int zoneIdx);
void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight,
float playerYawDeg);
void updateExploration(const glm::vec3& playerRenderPos); void updateExploration(const glm::vec3& playerRenderPos);
void zoomIn(const glm::vec3& playerRenderPos); void zoomIn(const glm::vec3& playerRenderPos);
void zoomOut(); void zoomOut();

View file

@ -171,6 +171,7 @@ private:
bool pendingShadows = true; bool pendingShadows = true;
float pendingShadowDistance = 300.0f; float pendingShadowDistance = 300.0f;
bool pendingWaterRefraction = false; bool pendingWaterRefraction = false;
int pendingBrightness = 50; // 0-100, maps to 0.0-2.0 (50 = 1.0 default)
int pendingMasterVolume = 100; int pendingMasterVolume = 100;
int pendingMusicVolume = 30; int pendingMusicVolume = 30;
int pendingAmbientVolume = 100; int pendingAmbientVolume = 100;
@ -192,6 +193,7 @@ private:
bool pendingMinimapNpcDots = false; bool pendingMinimapNpcDots = false;
bool pendingShowLatencyMeter = true; bool pendingShowLatencyMeter = true;
bool pendingSeparateBags = true; bool pendingSeparateBags = true;
bool pendingShowKeyring = true;
bool pendingAutoLoot = false; bool pendingAutoLoot = false;
// Keybinding customization // Keybinding customization

View file

@ -39,6 +39,8 @@ public:
bool isSeparateBags() const { return separateBags_; } bool isSeparateBags() const { return separateBags_; }
void toggleCompactBags() { compactBags_ = !compactBags_; } void toggleCompactBags() { compactBags_ = !compactBags_; }
bool isCompactBags() const { return compactBags_; } bool isCompactBags() const { return compactBags_; }
void setShowKeyring(bool show) { showKeyring_ = show; }
bool isShowKeyring() const { return showKeyring_; }
bool isBackpackOpen() const { return backpackOpen_; } bool isBackpackOpen() const { return backpackOpen_; }
bool isBagOpen(int idx) const { return idx >= 0 && idx < 4 ? bagOpen_[idx] : false; } bool isBagOpen(int idx) const { return idx >= 0 && idx < 4 ? bagOpen_[idx] : false; }
@ -79,6 +81,7 @@ private:
bool bKeyWasDown = false; bool bKeyWasDown = false;
bool separateBags_ = true; bool separateBags_ = true;
bool compactBags_ = false; bool compactBags_ = false;
bool showKeyring_ = true;
bool backpackOpen_ = false; bool backpackOpen_ = false;
std::array<bool, 4> bagOpen_{}; std::array<bool, 4> bagOpen_{};
bool cKeyWasDown = false; bool cKeyWasDown = false;

View file

@ -576,6 +576,12 @@ void Application::run() {
void Application::shutdown() { void Application::shutdown() {
LOG_WARNING("Shutting down application..."); LOG_WARNING("Shutting down application...");
// Hide the window immediately so the OS doesn't think the app is frozen
// during the (potentially slow) resource cleanup below.
if (window && window->getSDLWindow()) {
SDL_HideWindow(window->getSDLWindow());
}
// Stop background world preloader before destroying AssetManager // Stop background world preloader before destroying AssetManager
cancelWorldPreload(); cancelWorldPreload();

File diff suppressed because it is too large Load diff

View file

@ -1228,7 +1228,7 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMe
} }
} }
return !inbox.empty(); return true;
} }
// ============================================================================ // ============================================================================
@ -1537,5 +1537,333 @@ bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogDa
return true; return true;
} }
// ============================================================================
// TBC 2.4.3 SMSG_MESSAGECHAT
// TBC format: type(u8) + language(u32) + [type-specific data] + msgLen(u32) + msg + tag(u8)
// WotLK adds senderGuid(u64) + unknown(u32) before type-specific data.
// ============================================================================
bool TbcPacketParsers::parseMessageChat(network::Packet& packet, MessageChatData& data) {
if (packet.getSize() < 10) {
LOG_ERROR("[TBC] SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes");
return false;
}
uint8_t typeVal = packet.readUInt8();
data.type = static_cast<ChatType>(typeVal);
uint32_t langVal = packet.readUInt32();
data.language = static_cast<ChatLanguage>(langVal);
// TBC: NO senderGuid or unknown field here (WotLK has senderGuid(u64) + unk(u32))
switch (data.type) {
case ChatType::MONSTER_SAY:
case ChatType::MONSTER_YELL:
case ChatType::MONSTER_EMOTE:
case ChatType::MONSTER_WHISPER:
case ChatType::MONSTER_PARTY:
case ChatType::RAID_BOSS_EMOTE: {
// senderGuid(u64) + nameLen(u32) + name + targetGuid(u64)
data.senderGuid = packet.readUInt64();
uint32_t nameLen = packet.readUInt32();
if (nameLen > 0 && nameLen < 256) {
data.senderName.resize(nameLen);
for (uint32_t i = 0; i < nameLen; ++i) {
data.senderName[i] = static_cast<char>(packet.readUInt8());
}
if (!data.senderName.empty() && data.senderName.back() == '\0') {
data.senderName.pop_back();
}
}
data.receiverGuid = packet.readUInt64();
break;
}
case ChatType::SAY:
case ChatType::PARTY:
case ChatType::YELL:
case ChatType::WHISPER:
case ChatType::WHISPER_INFORM:
case ChatType::GUILD:
case ChatType::OFFICER:
case ChatType::RAID:
case ChatType::RAID_LEADER:
case ChatType::RAID_WARNING:
case ChatType::EMOTE:
case ChatType::TEXT_EMOTE: {
// senderGuid(u64) + senderGuid(u64) — written twice by server
data.senderGuid = packet.readUInt64();
/*duplicateGuid*/ packet.readUInt64();
break;
}
case ChatType::CHANNEL: {
// channelName(string) + rank(u32) + senderGuid(u64)
data.channelName = packet.readString();
/*uint32_t rank =*/ packet.readUInt32();
data.senderGuid = packet.readUInt64();
break;
}
default: {
// All other types: senderGuid(u64) + senderGuid(u64) — written twice
data.senderGuid = packet.readUInt64();
/*duplicateGuid*/ packet.readUInt64();
break;
}
}
// Read message length + message
uint32_t messageLen = packet.readUInt32();
if (messageLen > 0 && messageLen < 8192) {
data.message.resize(messageLen);
for (uint32_t i = 0; i < messageLen; ++i) {
data.message[i] = static_cast<char>(packet.readUInt8());
}
if (!data.message.empty() && data.message.back() == '\0') {
data.message.pop_back();
}
}
// Read chat tag
if (packet.getReadPos() < packet.getSize()) {
data.chatTag = packet.readUInt8();
}
LOG_DEBUG("[TBC] SMSG_MESSAGECHAT: type=", getChatTypeString(data.type),
" sender=", data.senderName.empty() ? std::to_string(data.senderGuid) : data.senderName);
return true;
}
// ============================================================================
// TBC 2.4.3 quest giver status
// TBC sends uint32 (like Classic), WotLK changed to uint8.
// TBC 2.4.3 enum: 0=NONE,1=UNAVAILABLE,2=CHAT,3=INCOMPLETE,4=REWARD_REP,
// 5=AVAILABLE_REP,6=AVAILABLE,7=REWARD2,8=REWARD
// ============================================================================
uint8_t TbcPacketParsers::readQuestGiverStatus(network::Packet& packet) {
uint32_t tbcStatus = packet.readUInt32();
switch (tbcStatus) {
case 0: return 0; // NONE
case 1: return 1; // UNAVAILABLE
case 2: return 0; // CHAT → NONE (no marker)
case 3: return 5; // INCOMPLETE → WotLK INCOMPLETE
case 4: return 6; // REWARD_REP → WotLK REWARD_REP
case 5: return 7; // AVAILABLE_REP → WotLK AVAILABLE_LOW_LEVEL
case 6: return 8; // AVAILABLE → WotLK AVAILABLE
case 7: return 10; // REWARD2 → WotLK REWARD
case 8: return 10; // REWARD → WotLK REWARD
default: return 0;
}
}
// ============================================================================
// TBC 2.4.3 channel join/leave
// Classic/TBC: just name+password (no channelId/hasVoice/joinedByZone prefix)
// ============================================================================
network::Packet TbcPacketParsers::buildJoinChannel(const std::string& channelName, const std::string& password) {
network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL));
packet.writeString(channelName);
packet.writeString(password);
LOG_DEBUG("[TBC] Built CMSG_JOIN_CHANNEL: channel=", channelName);
return packet;
}
network::Packet TbcPacketParsers::buildLeaveChannel(const std::string& channelName) {
network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL));
packet.writeString(channelName);
LOG_DEBUG("[TBC] Built CMSG_LEAVE_CHANNEL: channel=", channelName);
return packet;
}
// ============================================================================
// TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE
// TBC has 2 extra strings after name[4] (iconName + castBarCaption).
// WotLK has 3 (adds unk1). Classic has 0.
// ============================================================================
bool TbcPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) {
if (packet.getSize() < 4) {
LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)");
return false;
}
data.entry = packet.readUInt32();
if (data.entry & 0x80000000) {
data.entry &= ~0x80000000;
data.name = "";
return true;
}
if (packet.getSize() - packet.getReadPos() < 8) {
LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")");
return false;
}
data.type = packet.readUInt32();
data.displayId = packet.readUInt32();
// 4 name strings
data.name = packet.readString();
packet.readString();
packet.readString();
packet.readString();
// TBC: 2 extra strings (iconName + castBarCaption) — WotLK has 3, Classic has 0
packet.readString(); // iconName
packet.readString(); // castBarCaption
// Read 24 type-specific data fields
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining >= 24 * 4) {
for (int i = 0; i < 24; i++) {
data.data[i] = packet.readUInt32();
}
data.hasData = true;
} else if (remaining > 0) {
uint32_t fieldsToRead = remaining / 4;
for (uint32_t i = 0; i < fieldsToRead && i < 24; i++) {
data.data[i] = packet.readUInt32();
}
if (fieldsToRead < 24) {
LOG_WARNING("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated in data fields (", fieldsToRead,
" of 24, entry=", data.entry, ")");
}
}
if (data.type == 15) { // MO_TRANSPORT
LOG_DEBUG("TBC GO query: MO_TRANSPORT entry=", data.entry,
" name=\"", data.name, "\" displayId=", data.displayId,
" taxiPathId=", data.data[0], " moveSpeed=", data.data[1]);
} else {
LOG_DEBUG("TBC GO query: ", data.name, " type=", data.type, " entry=", data.entry);
}
return true;
}
// ============================================================================
// TBC 2.4.3 guild roster parser
// Same rank structure as WotLK (variable rankCount + goldLimit + bank tabs),
// but NO gender byte per member (WotLK added it).
// ============================================================================
bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData& data) {
if (packet.getSize() < 4) {
LOG_ERROR("TBC SMSG_GUILD_ROSTER too small: ", packet.getSize());
return false;
}
uint32_t numMembers = packet.readUInt32();
const uint32_t MAX_GUILD_MEMBERS = 1000;
if (numMembers > MAX_GUILD_MEMBERS) {
LOG_WARNING("TBC GuildRoster: numMembers capped (requested=", numMembers, ")");
numMembers = MAX_GUILD_MEMBERS;
}
data.motd = packet.readString();
data.guildInfo = packet.readString();
if (packet.getReadPos() + 4 > packet.getSize()) {
LOG_WARNING("TBC GuildRoster: truncated before rankCount");
data.ranks.clear();
data.members.clear();
return true;
}
uint32_t rankCount = packet.readUInt32();
const uint32_t MAX_GUILD_RANKS = 20;
if (rankCount > MAX_GUILD_RANKS) {
LOG_WARNING("TBC GuildRoster: rankCount capped (requested=", rankCount, ")");
rankCount = MAX_GUILD_RANKS;
}
data.ranks.resize(rankCount);
for (uint32_t i = 0; i < rankCount; ++i) {
if (packet.getReadPos() + 4 > packet.getSize()) {
LOG_WARNING("TBC GuildRoster: truncated rank at index ", i);
break;
}
data.ranks[i].rights = packet.readUInt32();
if (packet.getReadPos() + 4 > packet.getSize()) {
data.ranks[i].goldLimit = 0;
} else {
data.ranks[i].goldLimit = packet.readUInt32();
}
// 6 bank tab flags + 6 bank tab items per day (guild banks added in TBC 2.3)
for (int t = 0; t < 6; ++t) {
if (packet.getReadPos() + 8 > packet.getSize()) break;
packet.readUInt32(); // tabFlags
packet.readUInt32(); // tabItemsPerDay
}
}
data.members.resize(numMembers);
for (uint32_t i = 0; i < numMembers; ++i) {
if (packet.getReadPos() + 9 > packet.getSize()) {
LOG_WARNING("TBC GuildRoster: truncated member at index ", i);
break;
}
auto& m = data.members[i];
m.guid = packet.readUInt64();
m.online = (packet.readUInt8() != 0);
if (packet.getReadPos() >= packet.getSize()) {
m.name.clear();
} else {
m.name = packet.readString();
}
if (packet.getReadPos() + 1 > packet.getSize()) {
m.rankIndex = 0;
m.level = 1;
m.classId = 0;
m.gender = 0;
m.zoneId = 0;
} else {
m.rankIndex = packet.readUInt32();
if (packet.getReadPos() + 2 > packet.getSize()) {
m.level = 1;
m.classId = 0;
} else {
m.level = packet.readUInt8();
m.classId = packet.readUInt8();
}
// TBC: NO gender byte (WotLK added it)
m.gender = 0;
if (packet.getReadPos() + 4 > packet.getSize()) {
m.zoneId = 0;
} else {
m.zoneId = packet.readUInt32();
}
}
if (!m.online) {
if (packet.getReadPos() + 4 > packet.getSize()) {
m.lastOnline = 0.0f;
} else {
m.lastOnline = packet.readFloat();
}
}
if (packet.getReadPos() >= packet.getSize()) {
m.publicNote.clear();
m.officerNote.clear();
} else {
m.publicNote = packet.readString();
if (packet.getReadPos() >= packet.getSize()) {
m.officerNote.clear();
} else {
m.officerNote = packet.readString();
}
}
}
LOG_INFO("Parsed TBC SMSG_GUILD_ROSTER: ", numMembers, " members, motd=", data.motd);
return true;
}
} // namespace game } // namespace game
} // namespace wowee } // namespace wowee

View file

@ -9,7 +9,6 @@
#include <glm/gtc/constants.hpp> #include <glm/gtc/constants.hpp>
#include <glm/gtx/quaternion.hpp> #include <glm/gtx/quaternion.hpp>
#include <cmath> #include <cmath>
#include <iostream>
#include <map> #include <map>
#include <algorithm> #include <algorithm>
@ -31,13 +30,13 @@ void TransportManager::update(float deltaTime) {
void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry) { void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry) {
auto pathIt = paths_.find(pathId); auto pathIt = paths_.find(pathId);
if (pathIt == paths_.end()) { if (pathIt == paths_.end()) {
std::cerr << "TransportManager: Path " << pathId << " not found for transport " << guid << std::endl; LOG_ERROR("TransportManager: Path ", pathId, " not found for transport ", guid);
return; return;
} }
const auto& path = pathIt->second; const auto& path = pathIt->second;
if (path.points.empty()) { if (path.points.empty()) {
std::cerr << "TransportManager: Path " << pathId << " has no waypoints" << std::endl; LOG_ERROR("TransportManager: Path ", pathId, " has no waypoints");
return; return;
} }
@ -128,7 +127,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
void TransportManager::unregisterTransport(uint64_t guid) { void TransportManager::unregisterTransport(uint64_t guid) {
transports_.erase(guid); transports_.erase(guid);
std::cout << "TransportManager: Unregistered transport " << guid << std::endl; LOG_INFO("TransportManager: Unregistered transport ", guid);
} }
ActiveTransport* TransportManager::getTransport(uint64_t guid) { ActiveTransport* TransportManager::getTransport(uint64_t guid) {
@ -168,7 +167,7 @@ glm::mat4 TransportManager::getTransportInvTransform(uint64_t transportGuid) {
void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm::vec3>& waypoints, bool looping, float speed) { void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm::vec3>& waypoints, bool looping, float speed) {
if (waypoints.empty()) { if (waypoints.empty()) {
std::cerr << "TransportManager: Cannot load empty path " << pathId << std::endl; LOG_ERROR("TransportManager: Cannot load empty path ", pathId);
return; return;
} }
@ -227,7 +226,7 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) { void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) {
auto* transport = getTransport(guid); auto* transport = getTransport(guid);
if (!transport) { if (!transport) {
std::cerr << "TransportManager: Cannot set deck bounds for unknown transport " << guid << std::endl; LOG_ERROR("TransportManager: Cannot set deck bounds for unknown transport ", guid);
return; return;
} }

View file

@ -1,7 +1,8 @@
#include "game/warden_emulator.hpp" #include "game/warden_emulator.hpp"
#include <iostream> #include "core/logger.hpp"
#include <cstring> #include <cstring>
#include <chrono> #include <chrono>
#include <iterator>
#ifdef HAVE_UNICORN #ifdef HAVE_UNICORN
// Unicorn Engine headers // Unicorn Engine headers
@ -43,17 +44,27 @@ WardenEmulator::~WardenEmulator() {
bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint32_t baseAddress) { bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint32_t baseAddress) {
if (uc_) { if (uc_) {
std::cerr << "[WardenEmulator] Already initialized" << '\n'; LOG_ERROR("WardenEmulator: Already initialized");
return false; return false;
} }
// Reset allocator state so re-initialization starts with a clean heap.
allocations_.clear();
freeBlocks_.clear();
apiAddresses_.clear();
hooks_.clear();
nextHeapAddr_ = heapBase_;
std::cout << "[WardenEmulator] Initializing x86 emulator (Unicorn Engine)" << '\n'; {
std::cout << "[WardenEmulator] Module: " << moduleSize << " bytes at 0x" << std::hex << baseAddress << std::dec << '\n'; char addrBuf[32];
std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", baseAddress);
LOG_INFO("WardenEmulator: Initializing x86 emulator (Unicorn Engine)");
LOG_INFO("WardenEmulator: Module: ", moduleSize, " bytes at ", addrBuf);
}
// Create x86 32-bit emulator // Create x86 32-bit emulator
uc_err err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc_); uc_err err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc_);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] uc_open failed: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: uc_open failed: ", uc_strerror(err));
return false; return false;
} }
@ -63,9 +74,12 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
// Detect overlap between module and heap/stack regions early. // Detect overlap between module and heap/stack regions early.
uint32_t modEnd = moduleBase_ + moduleSize_; uint32_t modEnd = moduleBase_ + moduleSize_;
if (modEnd > heapBase_ && moduleBase_ < heapBase_ + heapSize_) { if (modEnd > heapBase_ && moduleBase_ < heapBase_ + heapSize_) {
std::cerr << "[WardenEmulator] Module [0x" << std::hex << moduleBase_ {
<< ", 0x" << modEnd << ") overlaps heap [0x" << heapBase_ char buf[256];
<< ", 0x" << (heapBase_ + heapSize_) << ") — adjust HEAP_BASE\n" << std::dec; std::snprintf(buf, sizeof(buf), "WardenEmulator: Module [0x%X, 0x%X) overlaps heap [0x%X, 0x%X) - adjust HEAP_BASE",
moduleBase_, modEnd, heapBase_, heapBase_ + heapSize_);
LOG_ERROR(buf);
}
uc_close(uc_); uc_close(uc_);
uc_ = nullptr; uc_ = nullptr;
return false; return false;
@ -74,7 +88,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
// Map module memory (code + data) // Map module memory (code + data)
err = uc_mem_map(uc_, moduleBase_, moduleSize_, UC_PROT_ALL); err = uc_mem_map(uc_, moduleBase_, moduleSize_, UC_PROT_ALL);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] Failed to map module memory: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: Failed to map module memory: ", uc_strerror(err));
uc_close(uc_); uc_close(uc_);
uc_ = nullptr; uc_ = nullptr;
return false; return false;
@ -83,7 +97,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
// Write module code to emulated memory // Write module code to emulated memory
err = uc_mem_write(uc_, moduleBase_, moduleCode, moduleSize); err = uc_mem_write(uc_, moduleBase_, moduleCode, moduleSize);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] Failed to write module code: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: Failed to write module code: ", uc_strerror(err));
uc_close(uc_); uc_close(uc_);
uc_ = nullptr; uc_ = nullptr;
return false; return false;
@ -92,7 +106,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
// Map stack // Map stack
err = uc_mem_map(uc_, stackBase_, stackSize_, UC_PROT_READ | UC_PROT_WRITE); err = uc_mem_map(uc_, stackBase_, stackSize_, UC_PROT_READ | UC_PROT_WRITE);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] Failed to map stack: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: Failed to map stack: ", uc_strerror(err));
uc_close(uc_); uc_close(uc_);
uc_ = nullptr; uc_ = nullptr;
return false; return false;
@ -106,7 +120,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
// Map heap // Map heap
err = uc_mem_map(uc_, heapBase_, heapSize_, UC_PROT_READ | UC_PROT_WRITE); err = uc_mem_map(uc_, heapBase_, heapSize_, UC_PROT_READ | UC_PROT_WRITE);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] Failed to map heap: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: Failed to map heap: ", uc_strerror(err));
uc_close(uc_); uc_close(uc_);
uc_ = nullptr; uc_ = nullptr;
return false; return false;
@ -115,7 +129,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
// Map API stub area // Map API stub area
err = uc_mem_map(uc_, apiStubBase_, 0x10000, UC_PROT_ALL); err = uc_mem_map(uc_, apiStubBase_, 0x10000, UC_PROT_ALL);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] Failed to map API stub area: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: Failed to map API stub area: ", uc_strerror(err));
uc_close(uc_); uc_close(uc_);
uc_ = nullptr; uc_ = nullptr;
return false; return false;
@ -127,7 +141,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
err = uc_mem_map(uc_, 0x0, 0x1000, UC_PROT_READ); err = uc_mem_map(uc_, 0x0, 0x1000, UC_PROT_READ);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
// Non-fatal — just log it; the emulator will still function // Non-fatal — just log it; the emulator will still function
std::cerr << "[WardenEmulator] Note: could not map null guard page: " << uc_strerror(err) << '\n'; LOG_WARNING("WardenEmulator: could not map null guard page: ", uc_strerror(err));
} }
// Add hooks for debugging and invalid memory access // Add hooks for debugging and invalid memory access
@ -135,9 +149,12 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0); uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0);
hooks_.push_back(hh); hooks_.push_back(hh);
std::cout << "[WardenEmulator] ✓ Emulator initialized successfully" << '\n'; {
std::cout << "[WardenEmulator] Stack: 0x" << std::hex << stackBase_ << " - 0x" << (stackBase_ + stackSize_) << '\n'; char sBuf[128];
std::cout << "[WardenEmulator] Heap: 0x" << heapBase_ << " - 0x" << (heapBase_ + heapSize_) << std::dec << '\n'; std::snprintf(sBuf, sizeof(sBuf), "WardenEmulator: Emulator initialized Stack: 0x%X-0x%X Heap: 0x%X-0x%X",
stackBase_, stackBase_ + stackSize_, heapBase_, heapBase_ + heapSize_);
LOG_INFO(sBuf);
}
return true; return true;
} }
@ -153,8 +170,11 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName,
// Store mapping // Store mapping
apiAddresses_[dllName][functionName] = stubAddr; apiAddresses_[dllName][functionName] = stubAddr;
std::cout << "[WardenEmulator] Hooked " << dllName << "!" << functionName {
<< " at 0x" << std::hex << stubAddr << std::dec << '\n'; char hBuf[32];
std::snprintf(hBuf, sizeof(hBuf), "0x%X", stubAddr);
LOG_DEBUG("WardenEmulator: Hooked ", dllName, "!", functionName, " at ", hBuf);
}
// TODO: Write stub code that triggers a hook callback // TODO: Write stub code that triggers a hook callback
// For now, just return the address for IAT patching // For now, just return the address for IAT patching
@ -163,7 +183,7 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName,
} }
void WardenEmulator::setupCommonAPIHooks() { void WardenEmulator::setupCommonAPIHooks() {
std::cout << "[WardenEmulator] Setting up common Windows API hooks..." << '\n'; LOG_INFO("WardenEmulator: Setting up common Windows API hooks...");
// kernel32.dll // kernel32.dll
hookAPI("kernel32.dll", "VirtualAlloc", apiVirtualAlloc); hookAPI("kernel32.dll", "VirtualAlloc", apiVirtualAlloc);
@ -174,7 +194,7 @@ void WardenEmulator::setupCommonAPIHooks() {
hookAPI("kernel32.dll", "GetCurrentProcessId", apiGetCurrentProcessId); hookAPI("kernel32.dll", "GetCurrentProcessId", apiGetCurrentProcessId);
hookAPI("kernel32.dll", "ReadProcessMemory", apiReadProcessMemory); hookAPI("kernel32.dll", "ReadProcessMemory", apiReadProcessMemory);
std::cout << "[WardenEmulator] ✓ Common API hooks registered" << '\n'; LOG_INFO("WardenEmulator: Common API hooks registered");
} }
uint32_t WardenEmulator::writeData(const void* data, size_t size) { uint32_t WardenEmulator::writeData(const void* data, size_t size) {
@ -198,12 +218,15 @@ std::vector<uint8_t> WardenEmulator::readData(uint32_t address, size_t size) {
uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector<uint32_t>& args) { uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector<uint32_t>& args) {
if (!uc_) { if (!uc_) {
std::cerr << "[WardenEmulator] Not initialized" << '\n'; LOG_ERROR("WardenEmulator: Not initialized");
return 0; return 0;
} }
std::cout << "[WardenEmulator] Calling function at 0x" << std::hex << address << std::dec {
<< " with " << args.size() << " args" << '\n'; char aBuf[32];
std::snprintf(aBuf, sizeof(aBuf), "0x%X", address);
LOG_DEBUG("WardenEmulator: Calling function at ", aBuf, " with ", args.size(), " args");
}
// Get current ESP // Get current ESP
uint32_t esp; uint32_t esp;
@ -227,7 +250,7 @@ uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector<uint32
// Execute until return address // Execute until return address
uc_err err = uc_emu_start(uc_, address, retAddr, 0, 0); uc_err err = uc_emu_start(uc_, address, retAddr, 0, 0);
if (err != UC_ERR_OK) { if (err != UC_ERR_OK) {
std::cerr << "[WardenEmulator] Execution failed: " << uc_strerror(err) << '\n'; LOG_ERROR("WardenEmulator: Execution failed: ", uc_strerror(err));
return 0; return 0;
} }
@ -235,7 +258,11 @@ uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector<uint32
uint32_t eax; uint32_t eax;
uc_reg_read(uc_, UC_X86_REG_EAX, &eax); uc_reg_read(uc_, UC_X86_REG_EAX, &eax);
std::cout << "[WardenEmulator] Function returned 0x" << std::hex << eax << std::dec << '\n'; {
char rBuf[32];
std::snprintf(rBuf, sizeof(rBuf), "0x%X", eax);
LOG_DEBUG("WardenEmulator: Function returned ", rBuf);
}
return eax; return eax;
} }
@ -262,20 +289,44 @@ std::string WardenEmulator::readString(uint32_t address, size_t maxLen) {
} }
uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t protection) { uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t protection) {
if (size == 0) return 0;
// Align to 4KB // Align to 4KB
size = (size + 0xFFF) & ~0xFFF; size = (size + 0xFFF) & ~0xFFF;
const uint32_t allocSize = static_cast<uint32_t>(size);
if (nextHeapAddr_ + size > heapBase_ + heapSize_) { // First-fit from free list so released blocks can be reused.
std::cerr << "[WardenEmulator] Heap exhausted" << '\n'; for (auto it = freeBlocks_.begin(); it != freeBlocks_.end(); ++it) {
if (it->second < size) continue;
const uint32_t addr = it->first;
const size_t blockSz = it->second;
freeBlocks_.erase(it);
if (blockSz > size)
freeBlocks_[addr + allocSize] = blockSz - size;
allocations_[addr] = size;
{
char mBuf[32];
std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr);
LOG_DEBUG("WardenEmulator: Reused ", size, " bytes at ", mBuf);
}
return addr;
}
const uint64_t heapEnd = static_cast<uint64_t>(heapBase_) + heapSize_;
if (static_cast<uint64_t>(nextHeapAddr_) + size > heapEnd) {
LOG_ERROR("WardenEmulator: Heap exhausted");
return 0; return 0;
} }
uint32_t addr = nextHeapAddr_; uint32_t addr = nextHeapAddr_;
nextHeapAddr_ += size; nextHeapAddr_ += allocSize;
allocations_[addr] = size; allocations_[addr] = size;
std::cout << "[WardenEmulator] Allocated " << size << " bytes at 0x" << std::hex << addr << std::dec << '\n'; {
char mBuf[32];
std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr);
LOG_DEBUG("WardenEmulator: Allocated ", size, " bytes at ", mBuf);
}
return addr; return addr;
} }
@ -283,12 +334,54 @@ uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t p
bool WardenEmulator::freeMemory(uint32_t address) { bool WardenEmulator::freeMemory(uint32_t address) {
auto it = allocations_.find(address); auto it = allocations_.find(address);
if (it == allocations_.end()) { if (it == allocations_.end()) {
std::cerr << "[WardenEmulator] Invalid free at 0x" << std::hex << address << std::dec << '\n'; {
char fBuf[32];
std::snprintf(fBuf, sizeof(fBuf), "0x%X", address);
LOG_ERROR("WardenEmulator: Invalid free at ", fBuf);
}
return false; return false;
} }
std::cout << "[WardenEmulator] Freed " << it->second << " bytes at 0x" << std::hex << address << std::dec << '\n'; {
char fBuf[32];
std::snprintf(fBuf, sizeof(fBuf), "0x%X", address);
LOG_DEBUG("WardenEmulator: Freed ", it->second, " bytes at ", fBuf);
}
const size_t freedSize = it->second;
allocations_.erase(it); allocations_.erase(it);
// Insert in free list and coalesce adjacent blocks to limit fragmentation.
auto [curr, inserted] = freeBlocks_.emplace(address, freedSize);
if (!inserted) curr->second += freedSize;
if (curr != freeBlocks_.begin()) {
auto prev = std::prev(curr);
if (static_cast<uint64_t>(prev->first) + prev->second == curr->first) {
prev->second += curr->second;
freeBlocks_.erase(curr);
curr = prev;
}
}
auto next = std::next(curr);
if (next != freeBlocks_.end() &&
static_cast<uint64_t>(curr->first) + curr->second == next->first) {
curr->second += next->second;
freeBlocks_.erase(next);
}
// Roll back the bump pointer if the highest free block reaches it.
while (!freeBlocks_.empty()) {
auto last = std::prev(freeBlocks_.end());
if (static_cast<uint64_t>(last->first) + last->second == nextHeapAddr_) {
nextHeapAddr_ = last->first;
freeBlocks_.erase(last);
} else {
break;
}
}
return true; return true;
} }
@ -319,8 +412,12 @@ uint32_t WardenEmulator::apiVirtualAlloc(WardenEmulator& emu, const std::vector<
uint32_t flAllocationType = args[2]; uint32_t flAllocationType = args[2];
uint32_t flProtect = args[3]; uint32_t flProtect = args[3];
std::cout << "[WinAPI] VirtualAlloc(0x" << std::hex << lpAddress << ", " << std::dec {
<< dwSize << ", 0x" << std::hex << flAllocationType << ", 0x" << flProtect << ")" << std::dec << '\n'; char vBuf[128];
std::snprintf(vBuf, sizeof(vBuf), "WinAPI: VirtualAlloc(0x%X, %u, 0x%X, 0x%X)",
lpAddress, dwSize, flAllocationType, flProtect);
LOG_DEBUG(vBuf);
}
// Ignore lpAddress hint for now // Ignore lpAddress hint for now
return emu.allocateMemory(dwSize, flProtect); return emu.allocateMemory(dwSize, flProtect);
@ -332,7 +429,11 @@ uint32_t WardenEmulator::apiVirtualFree(WardenEmulator& emu, const std::vector<u
uint32_t lpAddress = args[0]; uint32_t lpAddress = args[0];
std::cout << "[WinAPI] VirtualFree(0x" << std::hex << lpAddress << ")" << std::dec << '\n'; {
char vBuf[64];
std::snprintf(vBuf, sizeof(vBuf), "WinAPI: VirtualFree(0x%X)", lpAddress);
LOG_DEBUG(vBuf);
}
return emu.freeMemory(lpAddress) ? 1 : 0; return emu.freeMemory(lpAddress) ? 1 : 0;
} }
@ -342,7 +443,7 @@ uint32_t WardenEmulator::apiGetTickCount([[maybe_unused]] WardenEmulator& emu, [
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count(); auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
uint32_t ticks = static_cast<uint32_t>(ms & 0xFFFFFFFF); uint32_t ticks = static_cast<uint32_t>(ms & 0xFFFFFFFF);
std::cout << "[WinAPI] GetTickCount() = " << ticks << '\n'; LOG_DEBUG("WinAPI: GetTickCount() = ", ticks);
return ticks; return ticks;
} }
@ -350,18 +451,18 @@ uint32_t WardenEmulator::apiSleep([[maybe_unused]] WardenEmulator& emu, const st
if (args.size() < 1) return 0; if (args.size() < 1) return 0;
uint32_t dwMilliseconds = args[0]; uint32_t dwMilliseconds = args[0];
std::cout << "[WinAPI] Sleep(" << dwMilliseconds << ")" << '\n'; LOG_DEBUG("WinAPI: Sleep(", dwMilliseconds, ")");
// Don't actually sleep in emulator // Don't actually sleep in emulator
return 0; return 0;
} }
uint32_t WardenEmulator::apiGetCurrentThreadId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) { uint32_t WardenEmulator::apiGetCurrentThreadId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) {
std::cout << "[WinAPI] GetCurrentThreadId() = 1234" << '\n'; LOG_DEBUG("WinAPI: GetCurrentThreadId() = 1234");
return 1234; // Fake thread ID return 1234; // Fake thread ID
} }
uint32_t WardenEmulator::apiGetCurrentProcessId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) { uint32_t WardenEmulator::apiGetCurrentProcessId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) {
std::cout << "[WinAPI] GetCurrentProcessId() = 5678" << '\n'; LOG_DEBUG("WinAPI: GetCurrentProcessId() = 5678");
return 5678; // Fake process ID return 5678; // Fake process ID
} }
@ -375,8 +476,11 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
uint32_t nSize = args[3]; uint32_t nSize = args[3];
uint32_t lpNumberOfBytesRead = args[4]; uint32_t lpNumberOfBytesRead = args[4];
std::cout << "[WinAPI] ReadProcessMemory(0x" << std::hex << lpBaseAddress {
<< ", " << std::dec << nSize << " bytes)" << '\n'; char rBuf[64];
std::snprintf(rBuf, sizeof(rBuf), "WinAPI: ReadProcessMemory(0x%X, %u bytes)", lpBaseAddress, nSize);
LOG_DEBUG(rBuf);
}
// Read from emulated memory and write to buffer // Read from emulated memory and write to buffer
std::vector<uint8_t> data(nSize); std::vector<uint8_t> data(nSize);
@ -400,7 +504,7 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
// ============================================================================ // ============================================================================
void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) { void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) {
std::cout << "[Trace] 0x" << std::hex << address << std::dec << '\n'; (void)address; // Trace disabled by default to avoid log spam
} }
void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) { void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) {
@ -415,9 +519,12 @@ void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, ui
case UC_MEM_FETCH_PROT: typeStr = "FETCH_PROT"; break; case UC_MEM_FETCH_PROT: typeStr = "FETCH_PROT"; break;
} }
std::cerr << "[WardenEmulator] Invalid memory access: " << typeStr {
<< " at 0x" << std::hex << address << std::dec char mBuf[128];
<< " (size=" << size << ")" << '\n'; std::snprintf(mBuf, sizeof(mBuf), "WardenEmulator: Invalid memory access: %s at 0x%llX (size=%d)",
typeStr, static_cast<unsigned long long>(address), size);
LOG_ERROR(mBuf);
}
} }
#else // !HAVE_UNICORN #else // !HAVE_UNICORN

View file

@ -1,5 +1,6 @@
#include "game/warden_memory.hpp" #include "game/warden_memory.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
#include <chrono>
#include <fstream> #include <fstream>
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
@ -406,10 +407,31 @@ void WardenMemory::patchRuntimeGlobals() {
writeLE32(WORLD_ENABLES, enables); writeLE32(WORLD_ENABLES, enables);
LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec); LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec);
// LastHardwareAction // LastHardwareAction — must be a recent GetTickCount()-style timestamp
// so the anti-AFK scan sees (currentTime - lastAction) < threshold.
constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8; constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8;
writeLE32(LAST_HARDWARE_ACTION, 60000); uint32_t nowMs = static_cast<uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
writeLE32(LAST_HARDWARE_ACTION, nowMs - 2000);
LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec); LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec);
// Embed the 37-byte Warden module memcpy pattern in BSS so that
// FIND_CODE_BY_HASH (PAGE_B) brute-force search can find it.
// This is the pattern VMaNGOS's "Warden Memory Read check" looks for.
constexpr uint32_t MEMCPY_PATTERN_VA = 0xCE8700;
static const uint8_t kWardenMemcpyPattern[37] = {
0x56, 0x57, 0xFC, 0x8B, 0x54, 0x24, 0x14, 0x8B,
0x74, 0x24, 0x10, 0x8B, 0x44, 0x24, 0x0C, 0x8B,
0xCA, 0x8B, 0xF8, 0xC1, 0xE9, 0x02, 0x74, 0x02,
0xF3, 0xA5, 0xB1, 0x03, 0x23, 0xCA, 0x74, 0x02,
0xF3, 0xA4, 0x5F, 0x5E, 0xC3
};
uint32_t patRva = MEMCPY_PATTERN_VA - imageBase_;
if (patRva + sizeof(kWardenMemcpyPattern) <= imageSize_) {
std::memcpy(image_.data() + patRva, kWardenMemcpyPattern, sizeof(kWardenMemcpyPattern));
LOG_WARNING("WardenMemory: Embedded Warden memcpy pattern at 0x", std::hex, MEMCPY_PATTERN_VA, std::dec);
}
} }
void WardenMemory::patchTurtleWowBinary() { void WardenMemory::patchTurtleWowBinary() {
@ -837,7 +859,8 @@ void WardenMemory::verifyWardenScanEntries() {
} }
bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20],
uint8_t patternLen, bool imageOnly) const { uint8_t patternLen, bool imageOnly,
uint32_t hintOffset, bool hintOnly) const {
if (!loaded_ || patternLen == 0 || patternLen > 255) return false; if (!loaded_ || patternLen == 0 || patternLen > 255) return false;
// Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1) // Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1)
@ -849,21 +872,56 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
auto cacheIt = codePatternCache_.find(cacheKey); auto cacheIt = codePatternCache_.find(cacheKey);
if (cacheIt != codePatternCache_.end()) { if (cacheIt != codePatternCache_.end()) {
LOG_WARNING("WardenMemory: Code pattern cache HIT → ",
cacheIt->second ? "found" : "not found");
return cacheIt->second; return cacheIt->second;
} }
// FIND_MEM_IMAGE_CODE_BY_HASH (imageOnly=true) searches ALL sections of // --- Fast path: check the hint offset directly (single HMAC) ---
// the PE image — not just executable ones. The original Warden module // The PAGE_A offset field is the RVA where the server expects the pattern.
// walks every PE section when scanning the WoW.exe memory image. if (hintOffset > 0 && hintOffset + patternLen <= imageSize_) {
// FIND_CODE_BY_HASH (imageOnly=false) searches all process memory; since uint8_t hmacOut[20];
// we only have the PE image, both cases search the full image. unsigned int hmacLen = 0;
HMAC(EVP_sha1(), seed, 4,
image_.data() + hintOffset, patternLen,
hmacOut, &hmacLen);
if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) {
LOG_WARNING("WardenMemory: Code pattern found at hint RVA 0x", std::hex,
hintOffset, std::dec, " (direct hit)");
codePatternCache_[cacheKey] = true;
return true;
}
}
// --- Wider hint window: search ±4096 bytes around hint offset ---
if (hintOffset > 0) {
size_t winStart = (hintOffset > 4096) ? hintOffset - 4096 : 0;
size_t winEnd = std::min(static_cast<size_t>(hintOffset) + 4096 + patternLen,
static_cast<size_t>(imageSize_));
if (winEnd > winStart + patternLen) {
for (size_t i = winStart; i + patternLen <= winEnd; i++) {
if (i == hintOffset) continue; // already checked
uint8_t hmacOut[20];
unsigned int hmacLen = 0;
HMAC(EVP_sha1(), seed, 4,
image_.data() + i, patternLen,
hmacOut, &hmacLen);
if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) {
LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, i,
std::dec, " (hint window, delta=", static_cast<int>(i) - static_cast<int>(hintOffset), ")");
codePatternCache_[cacheKey] = true;
return true;
}
}
}
}
// If hint-only mode, skip the expensive brute-force search.
if (hintOnly) return false;
// --- Brute-force fallback: search all PE sections ---
struct Range { size_t start; size_t end; }; struct Range { size_t start; size_t end; };
std::vector<Range> ranges; std::vector<Range> ranges;
if (imageOnly && image_.size() >= 64) { if (imageOnly && image_.size() >= 64) {
// Collect ALL PE sections (not just executable ones)
uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8) uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8)
| (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24); | (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24);
if (peOffset + 4 + 20 <= image_.size()) { if (peOffset + 4 + 20 <= image_.size()) {
@ -885,11 +943,14 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
} }
if (ranges.empty()) { if (ranges.empty()) {
// Fallback: search entire image
if (patternLen <= imageSize_) if (patternLen <= imageSize_)
ranges.push_back({0, imageSize_}); ranges.push_back({0, imageSize_});
} }
auto bruteStart = std::chrono::steady_clock::now();
LOG_WARNING("WardenMemory: Brute-force searching ", ranges.size(), " section(s), hint=0x",
std::hex, hintOffset, std::dec, " patLen=", (int)patternLen);
size_t totalPositions = 0; size_t totalPositions = 0;
for (const auto& r : ranges) { for (const auto& r : ranges) {
size_t positions = r.end - r.start - patternLen + 1; size_t positions = r.end - r.start - patternLen + 1;
@ -900,8 +961,11 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
image_.data() + r.start + i, patternLen, image_.data() + r.start + i, patternLen,
hmacOut, &hmacLen); hmacOut, &hmacLen);
if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) {
auto elapsed = std::chrono::duration<float>(
std::chrono::steady_clock::now() - bruteStart).count();
LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex,
r.start + i, std::dec, " (searched ", totalPositions + i + 1, " positions)"); r.start + i, std::dec, " (searched ", totalPositions + i + 1,
" positions in ", elapsed, "s)");
codePatternCache_[cacheKey] = true; codePatternCache_[cacheKey] = true;
return true; return true;
} }
@ -909,8 +973,10 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
totalPositions += positions; totalPositions += positions;
} }
auto elapsed = std::chrono::duration<float>(
std::chrono::steady_clock::now() - bruteStart).count();
LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ", LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ",
ranges.size(), " section(s)"); ranges.size(), " section(s), took ", elapsed, "s");
codePatternCache_[cacheKey] = false; codePatternCache_[cacheKey] = false;
return false; return false;
} }

View file

@ -1,9 +1,9 @@
#include "game/warden_module.hpp" #include "game/warden_module.hpp"
#include "auth/crypto.hpp" #include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
#include <filesystem> #include <filesystem>
#include <iostream>
#include <zlib.h> #include <zlib.h>
#include <openssl/rsa.h> #include <openssl/rsa.h>
#include <openssl/bn.h> #include <openssl/bn.h>
@ -51,28 +51,30 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
moduleData_ = moduleData; moduleData_ = moduleData;
md5Hash_ = md5Hash; md5Hash_ = md5Hash;
std::cout << "[WardenModule] Loading module (MD5: "; {
for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) { char hexBuf[17] = {};
printf("%02X", md5Hash[i]); for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) {
snprintf(hexBuf + i * 2, 3, "%02X", md5Hash[i]);
}
LOG_INFO("WardenModule: Loading module (MD5: ", hexBuf, "...)");
} }
std::cout << "...)" << '\n';
// Step 1: Verify MD5 hash // Step 1: Verify MD5 hash
if (!verifyMD5(moduleData, md5Hash)) { if (!verifyMD5(moduleData, md5Hash)) {
std::cerr << "[WardenModule] MD5 verification failed; continuing in compatibility mode" << '\n'; LOG_ERROR("WardenModule: MD5 verification failed; continuing in compatibility mode");
} }
std::cout << "[WardenModule] ✓ MD5 verified" << '\n'; LOG_INFO("WardenModule: MD5 verified");
// Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed) // Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed)
if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm] if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm]
std::cerr << "[WardenModule] RC4 decryption failed; using raw module bytes fallback" << '\n'; LOG_ERROR("WardenModule: RC4 decryption failed; using raw module bytes fallback");
decryptedData_ = moduleData; decryptedData_ = moduleData;
} }
std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n'; LOG_INFO("WardenModule: RC4 decrypted (", decryptedData_.size(), " bytes)");
// Step 3: Verify RSA signature // Step 3: Verify RSA signature
if (!verifyRSASignature(decryptedData_)) { if (!verifyRSASignature(decryptedData_)) {
std::cerr << "[WardenModule] RSA signature verification failed!" << '\n'; LOG_ERROR("WardenModule: RSA signature verification failed!");
// Note: Currently returns true (skipping verification) due to placeholder modulus // Note: Currently returns true (skipping verification) due to placeholder modulus
} }
@ -84,42 +86,42 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
dataWithoutSig = decryptedData_; dataWithoutSig = decryptedData_;
} }
if (!decompressZlib(dataWithoutSig, decompressedData_)) { if (!decompressZlib(dataWithoutSig, decompressedData_)) {
std::cerr << "[WardenModule] zlib decompression failed; using decrypted bytes fallback" << '\n'; LOG_ERROR("WardenModule: zlib decompression failed; using decrypted bytes fallback");
decompressedData_ = decryptedData_; decompressedData_ = decryptedData_;
} }
// Step 5: Parse custom executable format // Step 5: Parse custom executable format
if (!parseExecutableFormat(decompressedData_)) { if (!parseExecutableFormat(decompressedData_)) {
std::cerr << "[WardenModule] Executable format parsing failed; continuing with minimal module image" << '\n'; LOG_ERROR("WardenModule: Executable format parsing failed; continuing with minimal module image");
} }
// Step 6: Apply relocations // Step 6: Apply relocations
if (!applyRelocations()) { if (!applyRelocations()) {
std::cerr << "[WardenModule] Address relocations failed; continuing with unrelocated image" << '\n'; LOG_ERROR("WardenModule: Address relocations failed; continuing with unrelocated image");
} }
// Step 7: Bind APIs // Step 7: Bind APIs
if (!bindAPIs()) { if (!bindAPIs()) {
std::cerr << "[WardenModule] API binding failed!" << '\n'; LOG_ERROR("WardenModule: API binding failed!");
// Note: Currently returns true (stub) on both Windows and Linux // Note: Currently returns true (stub) on both Windows and Linux
} }
// Step 8: Initialize module // Step 8: Initialize module
if (!initializeModule()) { if (!initializeModule()) {
std::cerr << "[WardenModule] Module initialization failed; continuing with stub callbacks" << '\n'; LOG_ERROR("WardenModule: Module initialization failed; continuing with stub callbacks");
} }
// Module loading pipeline complete! // Module loading pipeline complete!
// Note: Steps 6-8 are stubs/platform-limited, but infrastructure is ready // Note: Steps 6-8 are stubs/platform-limited, but infrastructure is ready
loaded_ = true; // Mark as loaded (infrastructure complete) loaded_ = true; // Mark as loaded (infrastructure complete)
std::cout << "[WardenModule] ✓ Module loading pipeline COMPLETE" << '\n'; LOG_INFO("WardenModule: Module loading pipeline COMPLETE");
std::cout << "[WardenModule] Status: Infrastructure ready, execution stubs in place" << '\n'; LOG_INFO("WardenModule: Status: Infrastructure ready, execution stubs in place");
std::cout << "[WardenModule] Limitations:" << '\n'; LOG_INFO("WardenModule: Limitations:");
std::cout << "[WardenModule] - Relocations: needs real module data" << '\n'; LOG_INFO("WardenModule: - Relocations: needs real module data");
std::cout << "[WardenModule] - API Binding: Windows only (or Wine on Linux)" << '\n'; LOG_INFO("WardenModule: - API Binding: Windows only (or Wine on Linux)");
std::cout << "[WardenModule] - Execution: disabled (unsafe without validation)" << '\n'; LOG_INFO("WardenModule: - Execution: disabled (unsafe without validation)");
std::cout << "[WardenModule] For strict servers: Would need to enable actual x86 execution" << '\n'; LOG_INFO("WardenModule: For strict servers: Would need to enable actual x86 execution");
return true; return true;
} }
@ -127,25 +129,25 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData, bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
[[maybe_unused]] std::vector<uint8_t>& responseOut) { [[maybe_unused]] std::vector<uint8_t>& responseOut) {
if (!loaded_) { if (!loaded_) {
std::cerr << "[WardenModule] Module not loaded, cannot process checks" << '\n'; LOG_ERROR("WardenModule: Module not loaded, cannot process checks");
return false; return false;
} }
#ifdef HAVE_UNICORN #ifdef HAVE_UNICORN
if (emulator_ && emulator_->isInitialized() && funcList_.packetHandler) { if (emulator_ && emulator_->isInitialized() && funcList_.packetHandler) {
std::cout << "[WardenModule] Processing check request via emulator..." << '\n'; LOG_INFO("WardenModule: Processing check request via emulator...");
std::cout << "[WardenModule] Check data: " << checkData.size() << " bytes" << '\n'; LOG_INFO("WardenModule: Check data: ", checkData.size(), " bytes");
// Allocate memory for check data in emulated space // Allocate memory for check data in emulated space
uint32_t checkDataAddr = emulator_->allocateMemory(checkData.size(), 0x04); uint32_t checkDataAddr = emulator_->allocateMemory(checkData.size(), 0x04);
if (checkDataAddr == 0) { if (checkDataAddr == 0) {
std::cerr << "[WardenModule] Failed to allocate memory for check data" << '\n'; LOG_ERROR("WardenModule: Failed to allocate memory for check data");
return false; return false;
} }
// Write check data to emulated memory // Write check data to emulated memory
if (!emulator_->writeMemory(checkDataAddr, checkData.data(), checkData.size())) { if (!emulator_->writeMemory(checkDataAddr, checkData.data(), checkData.size())) {
std::cerr << "[WardenModule] Failed to write check data" << '\n'; LOG_ERROR("WardenModule: Failed to write check data");
emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(checkDataAddr);
return false; return false;
} }
@ -153,7 +155,7 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
// Allocate response buffer in emulated space (assume max 1KB response) // Allocate response buffer in emulated space (assume max 1KB response)
uint32_t responseAddr = emulator_->allocateMemory(1024, 0x04); uint32_t responseAddr = emulator_->allocateMemory(1024, 0x04);
if (responseAddr == 0) { if (responseAddr == 0) {
std::cerr << "[WardenModule] Failed to allocate response buffer" << '\n'; LOG_ERROR("WardenModule: Failed to allocate response buffer");
emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(checkDataAddr);
return false; return false;
} }
@ -162,13 +164,13 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
// Call module's PacketHandler // Call module's PacketHandler
// void PacketHandler(uint8_t* checkData, size_t checkSize, // void PacketHandler(uint8_t* checkData, size_t checkSize,
// uint8_t* responseOut, size_t* responseSizeOut) // uint8_t* responseOut, size_t* responseSizeOut)
std::cout << "[WardenModule] Calling PacketHandler..." << '\n'; LOG_INFO("WardenModule: Calling PacketHandler...");
// For now, this is a placeholder - actual calling would depend on // For now, this is a placeholder - actual calling would depend on
// the module's exact function signature // the module's exact function signature
std::cout << "[WardenModule] ⚠ PacketHandler execution stubbed" << '\n'; LOG_WARNING("WardenModule: PacketHandler execution stubbed");
std::cout << "[WardenModule] Would call emulated function to process checks" << '\n'; LOG_INFO("WardenModule: Would call emulated function to process checks");
std::cout << "[WardenModule] This would generate REAL responses (not fakes!)" << '\n'; LOG_INFO("WardenModule: This would generate REAL responses (not fakes!)");
// Clean up // Clean up
emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(checkDataAddr);
@ -179,7 +181,7 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
return false; return false;
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "[WardenModule] Exception during PacketHandler: " << e.what() << '\n'; LOG_ERROR("WardenModule: Exception during PacketHandler: ", e.what());
emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(checkDataAddr);
emulator_->freeMemory(responseAddr); emulator_->freeMemory(responseAddr);
return false; return false;
@ -187,8 +189,8 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
} }
#endif #endif
std::cout << "[WardenModule] ⚠ processCheckRequest NOT IMPLEMENTED" << '\n'; LOG_WARNING("WardenModule: processCheckRequest NOT IMPLEMENTED");
std::cout << "[WardenModule] Would call module->PacketHandler() here" << '\n'; LOG_INFO("WardenModule: Would call module->PacketHandler() here");
// For now, return false to fall back to fake responses in GameHandler // For now, return false to fall back to fake responses in GameHandler
return false; return false;
@ -219,13 +221,13 @@ void WardenModule::unload() {
if (moduleMemory_) { if (moduleMemory_) {
// Call module's Unload() function if loaded // Call module's Unload() function if loaded
if (loaded_ && funcList_.unload) { if (loaded_ && funcList_.unload) {
std::cout << "[WardenModule] Calling module unload callback..." << '\n'; LOG_INFO("WardenModule: Calling module unload callback...");
// TODO: Implement callback when execution layer is complete // TODO: Implement callback when execution layer is complete
// funcList_.unload(nullptr); // funcList_.unload(nullptr);
} }
// Free executable memory region // Free executable memory region
std::cout << "[WardenModule] Freeing " << moduleSize_ << " bytes of executable memory" << '\n'; LOG_INFO("WardenModule: Freeing ", moduleSize_, " bytes of executable memory");
#ifdef _WIN32 #ifdef _WIN32
VirtualFree(moduleMemory_, 0, MEM_RELEASE); VirtualFree(moduleMemory_, 0, MEM_RELEASE);
#else #else
@ -264,7 +266,7 @@ bool WardenModule::decryptRC4(const std::vector<uint8_t>& encrypted,
const std::vector<uint8_t>& key, const std::vector<uint8_t>& key,
std::vector<uint8_t>& decryptedOut) { std::vector<uint8_t>& decryptedOut) {
if (key.size() != 16) { if (key.size() != 16) {
std::cerr << "[WardenModule] Invalid RC4 key size: " << key.size() << " (expected 16)" << '\n'; LOG_ERROR("WardenModule: Invalid RC4 key size: ", key.size(), " (expected 16)");
return false; return false;
} }
@ -299,7 +301,7 @@ bool WardenModule::decryptRC4(const std::vector<uint8_t>& encrypted,
bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) { bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
// RSA-2048 signature is last 256 bytes // RSA-2048 signature is last 256 bytes
if (data.size() < 256) { if (data.size() < 256) {
std::cerr << "[WardenModule] Data too small for RSA signature (need at least 256 bytes)" << '\n'; LOG_ERROR("WardenModule: Data too small for RSA signature (need at least 256 bytes)");
return false; return false;
} }
@ -385,7 +387,7 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
if (pkey) EVP_PKEY_free(pkey); if (pkey) EVP_PKEY_free(pkey);
if (decryptedLen < 0) { if (decryptedLen < 0) {
std::cerr << "[WardenModule] RSA public decrypt failed" << '\n'; LOG_ERROR("WardenModule: RSA public decrypt failed");
return false; return false;
} }
@ -398,24 +400,24 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
std::vector<uint8_t> actualHash(decryptedSig.end() - 20, decryptedSig.end()); std::vector<uint8_t> actualHash(decryptedSig.end() - 20, decryptedSig.end());
if (std::memcmp(actualHash.data(), expectedHash.data(), 20) == 0) { if (std::memcmp(actualHash.data(), expectedHash.data(), 20) == 0) {
std::cout << "[WardenModule] ✓ RSA signature verified" << '\n'; LOG_INFO("WardenModule: RSA signature verified");
return true; return true;
} }
} }
std::cerr << "[WardenModule] RSA signature verification FAILED (hash mismatch)" << '\n'; LOG_ERROR("WardenModule: RSA signature verification FAILED (hash mismatch)");
std::cerr << "[WardenModule] NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification" << '\n'; LOG_ERROR("WardenModule: NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification");
// For development, return true to proceed (since we don't have real modulus) // For development, return true to proceed (since we don't have real modulus)
// TODO: Set to false once real modulus is extracted // TODO: Set to false once real modulus is extracted
std::cout << "[WardenModule] ⚠ Skipping RSA verification (placeholder modulus)" << '\n'; LOG_WARNING("WardenModule: Skipping RSA verification (placeholder modulus)");
return true; // TEMPORARY - change to false for production return true; // TEMPORARY - change to false for production
} }
bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed, bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
std::vector<uint8_t>& decompressedOut) { std::vector<uint8_t>& decompressedOut) {
if (compressed.size() < 4) { if (compressed.size() < 4) {
std::cerr << "[WardenModule] Compressed data too small (need at least 4 bytes for size header)" << '\n'; LOG_ERROR("WardenModule: Compressed data too small (need at least 4 bytes for size header)");
return false; return false;
} }
@ -426,11 +428,11 @@ bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
(compressed[2] << 16) | (compressed[2] << 16) |
(compressed[3] << 24); (compressed[3] << 24);
std::cout << "[WardenModule] Uncompressed size: " << uncompressedSize << " bytes" << '\n'; LOG_INFO("WardenModule: Uncompressed size: ", uncompressedSize, " bytes");
// Sanity check (modules shouldn't be larger than 10MB) // Sanity check (modules shouldn't be larger than 10MB)
if (uncompressedSize > 10 * 1024 * 1024) { if (uncompressedSize > 10 * 1024 * 1024) {
std::cerr << "[WardenModule] Uncompressed size suspiciously large: " << uncompressedSize << " bytes" << '\n'; LOG_ERROR("WardenModule: Uncompressed size suspiciously large: ", uncompressedSize, " bytes");
return false; return false;
} }
@ -447,7 +449,7 @@ bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
// Initialize inflater // Initialize inflater
int ret = inflateInit(&stream); int ret = inflateInit(&stream);
if (ret != Z_OK) { if (ret != Z_OK) {
std::cerr << "[WardenModule] inflateInit failed: " << ret << '\n'; LOG_ERROR("WardenModule: inflateInit failed: ", ret);
return false; return false;
} }
@ -458,19 +460,18 @@ bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
inflateEnd(&stream); inflateEnd(&stream);
if (ret != Z_STREAM_END) { if (ret != Z_STREAM_END) {
std::cerr << "[WardenModule] inflate failed: " << ret << '\n'; LOG_ERROR("WardenModule: inflate failed: ", ret);
return false; return false;
} }
std::cout << "[WardenModule] ✓ zlib decompression successful (" LOG_INFO("WardenModule: zlib decompression successful (", stream.total_out, " bytes decompressed)");
<< stream.total_out << " bytes decompressed)" << '\n';
return true; return true;
} }
bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) { bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
if (exeData.size() < 4) { if (exeData.size() < 4) {
std::cerr << "[WardenModule] Executable data too small for header" << '\n'; LOG_ERROR("WardenModule: Executable data too small for header");
return false; return false;
} }
@ -481,11 +482,11 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
(exeData[2] << 16) | (exeData[2] << 16) |
(exeData[3] << 24); (exeData[3] << 24);
std::cout << "[WardenModule] Final code size: " << finalCodeSize << " bytes" << '\n'; LOG_INFO("WardenModule: Final code size: ", finalCodeSize, " bytes");
// Sanity check (executable shouldn't be larger than 5MB) // Sanity check (executable shouldn't be larger than 5MB)
if (finalCodeSize > 5 * 1024 * 1024 || finalCodeSize == 0) { if (finalCodeSize > 5 * 1024 * 1024 || finalCodeSize == 0) {
std::cerr << "[WardenModule] Invalid final code size: " << finalCodeSize << '\n'; LOG_ERROR("WardenModule: Invalid final code size: ", finalCodeSize);
return false; return false;
} }
@ -500,7 +501,7 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
PAGE_EXECUTE_READWRITE PAGE_EXECUTE_READWRITE
); );
if (!moduleMemory_) { if (!moduleMemory_) {
std::cerr << "[WardenModule] VirtualAlloc failed" << '\n'; LOG_ERROR("WardenModule: VirtualAlloc failed");
return false; return false;
} }
#else #else
@ -513,7 +514,7 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
0 0
); );
if (moduleMemory_ == MAP_FAILED) { if (moduleMemory_ == MAP_FAILED) {
std::cerr << "[WardenModule] mmap failed: " << strerror(errno) << '\n'; LOG_ERROR("WardenModule: mmap failed: ", strerror(errno));
moduleMemory_ = nullptr; moduleMemory_ = nullptr;
return false; return false;
} }
@ -522,8 +523,7 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
moduleSize_ = finalCodeSize; moduleSize_ = finalCodeSize;
std::memset(moduleMemory_, 0, moduleSize_); // Zero-initialize std::memset(moduleMemory_, 0, moduleSize_); // Zero-initialize
std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at " LOG_INFO("WardenModule: Allocated ", moduleSize_, " bytes of executable memory");
<< moduleMemory_ << '\n';
auto readU16LE = [&](size_t at) -> uint16_t { auto readU16LE = [&](size_t at) -> uint16_t {
return static_cast<uint16_t>(exeData[at] | (exeData[at + 1] << 8)); return static_cast<uint16_t>(exeData[at] | (exeData[at + 1] << 8));
@ -669,10 +669,10 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
if (usedFormat == PairFormat::SkipCopyData) formatName = "skip/copy/data"; if (usedFormat == PairFormat::SkipCopyData) formatName = "skip/copy/data";
if (usedFormat == PairFormat::CopySkipData) formatName = "copy/skip/data"; if (usedFormat == PairFormat::CopySkipData) formatName = "copy/skip/data";
std::cout << "[WardenModule] Parsed " << parsedPairCount << " pairs using format " LOG_INFO("WardenModule: Parsed ", parsedPairCount, " pairs using format ",
<< formatName << ", final offset: " << parsedFinalOffset << "/" << finalCodeSize << '\n'; formatName, ", final offset: ", parsedFinalOffset, "/", finalCodeSize);
std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_ LOG_INFO("WardenModule: Relocation data starts at decompressed offset ", relocDataOffset_,
<< " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n'; " (", (exeData.size() - relocDataOffset_), " bytes remaining)");
return true; return true;
} }
@ -683,13 +683,13 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize); std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize);
} }
relocDataOffset_ = 0; relocDataOffset_ = 0;
std::cerr << "[WardenModule] Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback" << '\n'; LOG_ERROR("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback");
return true; return true;
} }
bool WardenModule::applyRelocations() { bool WardenModule::applyRelocations() {
if (!moduleMemory_ || moduleSize_ == 0) { if (!moduleMemory_ || moduleSize_ == 0) {
std::cerr << "[WardenModule] No module memory allocated for relocations" << '\n'; LOG_ERROR("WardenModule: No module memory allocated for relocations");
return false; return false;
} }
@ -698,7 +698,7 @@ bool WardenModule::applyRelocations() {
// Each offset in the module image has moduleBase_ added to the 32-bit value there // Each offset in the module image has moduleBase_ added to the 32-bit value there
if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) { if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) {
std::cout << "[WardenModule] No relocation data available" << '\n'; LOG_INFO("WardenModule: No relocation data available");
return true; return true;
} }
@ -722,24 +722,27 @@ bool WardenModule::applyRelocations() {
std::memcpy(addr, &val, sizeof(uint32_t)); std::memcpy(addr, &val, sizeof(uint32_t));
relocCount++; relocCount++;
} else { } else {
std::cerr << "[WardenModule] Relocation offset " << currentOffset LOG_ERROR("WardenModule: Relocation offset ", currentOffset,
<< " out of bounds (moduleSize=" << moduleSize_ << ")" << '\n'; " out of bounds (moduleSize=", moduleSize_, ")");
} }
} }
std::cout << "[WardenModule] Applied " << relocCount << " relocations (base=0x" {
<< std::hex << moduleBase_ << std::dec << ")" << '\n'; char baseBuf[32];
std::snprintf(baseBuf, sizeof(baseBuf), "0x%X", moduleBase_);
LOG_INFO("WardenModule: Applied ", relocCount, " relocations (base=", baseBuf, ")");
}
return true; return true;
} }
bool WardenModule::bindAPIs() { bool WardenModule::bindAPIs() {
if (!moduleMemory_ || moduleSize_ == 0) { if (!moduleMemory_ || moduleSize_ == 0) {
std::cerr << "[WardenModule] No module memory allocated for API binding" << '\n'; LOG_ERROR("WardenModule: No module memory allocated for API binding");
return false; return false;
} }
std::cout << "[WardenModule] Binding Windows APIs for module..." << '\n'; LOG_INFO("WardenModule: Binding Windows APIs for module...");
// Common Windows APIs used by Warden modules: // Common Windows APIs used by Warden modules:
// //
@ -759,14 +762,14 @@ bool WardenModule::bindAPIs() {
#ifdef _WIN32 #ifdef _WIN32
// On Windows: Use GetProcAddress to resolve imports // On Windows: Use GetProcAddress to resolve imports
std::cout << "[WardenModule] Platform: Windows - using GetProcAddress" << '\n'; LOG_INFO("WardenModule: Platform: Windows - using GetProcAddress");
HMODULE kernel32 = GetModuleHandleA("kernel32.dll"); HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
HMODULE user32 = GetModuleHandleA("user32.dll"); HMODULE user32 = GetModuleHandleA("user32.dll");
HMODULE ntdll = GetModuleHandleA("ntdll.dll"); HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (!kernel32 || !user32 || !ntdll) { if (!kernel32 || !user32 || !ntdll) {
std::cerr << "[WardenModule] Failed to get module handles" << '\n'; LOG_ERROR("WardenModule: Failed to get module handles");
return false; return false;
} }
@ -777,8 +780,8 @@ bool WardenModule::bindAPIs() {
// - Resolve address using GetProcAddress // - Resolve address using GetProcAddress
// - Write address to Import Address Table (IAT) // - Write address to Import Address Table (IAT)
std::cout << "[WardenModule] ⚠ Windows API binding is STUB (needs PE import table parsing)" << '\n'; LOG_WARNING("WardenModule: Windows API binding is STUB (needs PE import table parsing)");
std::cout << "[WardenModule] Would parse PE headers and patch IAT with resolved addresses" << '\n'; LOG_INFO("WardenModule: Would parse PE headers and patch IAT with resolved addresses");
#else #else
// On Linux: Cannot directly execute Windows code // On Linux: Cannot directly execute Windows code
@ -787,15 +790,15 @@ bool WardenModule::bindAPIs() {
// 2. Implement Windows API stubs (limited functionality) // 2. Implement Windows API stubs (limited functionality)
// 3. Use binfmt_misc + Wine (transparent Windows executable support) // 3. Use binfmt_misc + Wine (transparent Windows executable support)
std::cout << "[WardenModule] Platform: Linux - Windows module execution NOT supported" << '\n'; LOG_WARNING("WardenModule: Platform: Linux - Windows module execution NOT supported");
std::cout << "[WardenModule] Options:" << '\n'; LOG_INFO("WardenModule: Options:");
std::cout << "[WardenModule] 1. Run wowee under Wine (provides Windows API layer)" << '\n'; LOG_INFO("WardenModule: 1. Run wowee under Wine (provides Windows API layer)");
std::cout << "[WardenModule] 2. Use a Windows VM" << '\n'; LOG_INFO("WardenModule: 2. Use a Windows VM");
std::cout << "[WardenModule] 3. Implement Windows API stubs (limited, complex)" << '\n'; LOG_INFO("WardenModule: 3. Implement Windows API stubs (limited, complex)");
// For now, we'll return true to continue the loading pipeline // For now, we'll return true to continue the loading pipeline
// Real execution would fail, but this allows testing the infrastructure // Real execution would fail, but this allows testing the infrastructure
std::cout << "[WardenModule] ⚠ Skipping API binding (Linux platform limitation)" << '\n'; LOG_WARNING("WardenModule: Skipping API binding (Linux platform limitation)");
#endif #endif
return true; // Return true to continue (stub implementation) return true; // Return true to continue (stub implementation)
@ -803,11 +806,11 @@ bool WardenModule::bindAPIs() {
bool WardenModule::initializeModule() { bool WardenModule::initializeModule() {
if (!moduleMemory_ || moduleSize_ == 0) { if (!moduleMemory_ || moduleSize_ == 0) {
std::cerr << "[WardenModule] No module memory allocated for initialization" << '\n'; LOG_ERROR("WardenModule: No module memory allocated for initialization");
return false; return false;
} }
std::cout << "[WardenModule] Initializing Warden module..." << '\n'; LOG_INFO("WardenModule: Initializing Warden module...");
// Module initialization protocol: // Module initialization protocol:
// //
@ -844,27 +847,27 @@ bool WardenModule::initializeModule() {
// Stub callbacks (would need real implementations) // Stub callbacks (would need real implementations)
callbacks.sendPacket = []([[maybe_unused]] uint8_t* data, size_t len) { callbacks.sendPacket = []([[maybe_unused]] uint8_t* data, size_t len) {
std::cout << "[WardenModule Callback] sendPacket(" << len << " bytes)" << '\n'; LOG_DEBUG("WardenModule Callback: sendPacket(", len, " bytes)");
// TODO: Send CMSG_WARDEN_DATA packet // TODO: Send CMSG_WARDEN_DATA packet
}; };
callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) { callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) {
std::cout << "[WardenModule Callback] validateModule()" << '\n'; LOG_DEBUG("WardenModule Callback: validateModule()");
// TODO: Validate module hash // TODO: Validate module hash
}; };
callbacks.allocMemory = [](size_t size) -> void* { callbacks.allocMemory = [](size_t size) -> void* {
std::cout << "[WardenModule Callback] allocMemory(" << size << ")" << '\n'; LOG_DEBUG("WardenModule Callback: allocMemory(", size, ")");
return malloc(size); return malloc(size);
}; };
callbacks.freeMemory = [](void* ptr) { callbacks.freeMemory = [](void* ptr) {
std::cout << "[WardenModule Callback] freeMemory()" << '\n'; LOG_DEBUG("WardenModule Callback: freeMemory()");
free(ptr); free(ptr);
}; };
callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) { callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) {
std::cout << "[WardenModule Callback] generateRC4()" << '\n'; LOG_DEBUG("WardenModule Callback: generateRC4()");
// TODO: Re-key RC4 cipher // TODO: Re-key RC4 cipher
}; };
@ -873,7 +876,7 @@ bool WardenModule::initializeModule() {
}; };
callbacks.logMessage = [](const char* msg) { callbacks.logMessage = [](const char* msg) {
std::cout << "[WardenModule Log] " << msg << '\n'; LOG_INFO("WardenModule Log: ", msg);
}; };
// Module entry point is typically at offset 0 (first bytes of loaded code) // Module entry point is typically at offset 0 (first bytes of loaded code)
@ -881,24 +884,28 @@ bool WardenModule::initializeModule() {
#ifdef HAVE_UNICORN #ifdef HAVE_UNICORN
// Use Unicorn emulator for cross-platform execution // Use Unicorn emulator for cross-platform execution
std::cout << "[WardenModule] Initializing Unicorn emulator..." << '\n'; LOG_INFO("WardenModule: Initializing Unicorn emulator...");
emulator_ = std::make_unique<WardenEmulator>(); emulator_ = std::make_unique<WardenEmulator>();
if (!emulator_->initialize(moduleMemory_, moduleSize_, moduleBase_)) { if (!emulator_->initialize(moduleMemory_, moduleSize_, moduleBase_)) {
std::cerr << "[WardenModule] Failed to initialize emulator" << '\n'; LOG_ERROR("WardenModule: Failed to initialize emulator");
return false; return false;
} }
// Setup Windows API hooks // Setup Windows API hooks
emulator_->setupCommonAPIHooks(); emulator_->setupCommonAPIHooks();
std::cout << "[WardenModule] ✓ Emulator initialized successfully" << '\n'; {
std::cout << "[WardenModule] Ready to execute module at 0x" << std::hex << moduleBase_ << std::dec << '\n'; char addrBuf[32];
std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", moduleBase_);
LOG_INFO("WardenModule: Emulator initialized successfully");
LOG_INFO("WardenModule: Ready to execute module at ", addrBuf);
}
// Allocate memory for ClientCallbacks structure in emulated space // Allocate memory for ClientCallbacks structure in emulated space
uint32_t callbackStructAddr = emulator_->allocateMemory(sizeof(ClientCallbacks), 0x04); uint32_t callbackStructAddr = emulator_->allocateMemory(sizeof(ClientCallbacks), 0x04);
if (callbackStructAddr == 0) { if (callbackStructAddr == 0) {
std::cerr << "[WardenModule] Failed to allocate memory for callbacks" << '\n'; LOG_ERROR("WardenModule: Failed to allocate memory for callbacks");
return false; return false;
} }
@ -921,13 +928,21 @@ bool WardenModule::initializeModule() {
emulator_->writeMemory(callbackStructAddr + (i * 4), &addr, 4); emulator_->writeMemory(callbackStructAddr + (i * 4), &addr, 4);
} }
std::cout << "[WardenModule] Prepared ClientCallbacks at 0x" << std::hex << callbackStructAddr << std::dec << '\n'; {
char cbBuf[32];
std::snprintf(cbBuf, sizeof(cbBuf), "0x%X", callbackStructAddr);
LOG_INFO("WardenModule: Prepared ClientCallbacks at ", cbBuf);
}
// Call module entry point // Call module entry point
// Entry point is typically at module base (offset 0) // Entry point is typically at module base (offset 0)
uint32_t entryPoint = moduleBase_; uint32_t entryPoint = moduleBase_;
std::cout << "[WardenModule] Calling module entry point at 0x" << std::hex << entryPoint << std::dec << '\n'; {
char epBuf[32];
std::snprintf(epBuf, sizeof(epBuf), "0x%X", entryPoint);
LOG_INFO("WardenModule: Calling module entry point at ", epBuf);
}
try { try {
// Call: WardenFuncList* InitModule(ClientCallbacks* callbacks) // Call: WardenFuncList* InitModule(ClientCallbacks* callbacks)
@ -935,21 +950,28 @@ bool WardenModule::initializeModule() {
uint32_t result = emulator_->callFunction(entryPoint, args); uint32_t result = emulator_->callFunction(entryPoint, args);
if (result == 0) { if (result == 0) {
std::cerr << "[WardenModule] Module entry returned NULL" << '\n'; LOG_ERROR("WardenModule: Module entry returned NULL");
return false; return false;
} }
std::cout << "[WardenModule] ✓ Module initialized, WardenFuncList at 0x" << std::hex << result << std::dec << '\n'; {
char resBuf[32];
std::snprintf(resBuf, sizeof(resBuf), "0x%X", result);
LOG_INFO("WardenModule: Module initialized, WardenFuncList at ", resBuf);
}
// Read WardenFuncList structure from emulated memory // Read WardenFuncList structure from emulated memory
// Structure has 4 function pointers (16 bytes) // Structure has 4 function pointers (16 bytes)
uint32_t funcAddrs[4] = {}; uint32_t funcAddrs[4] = {};
if (emulator_->readMemory(result, funcAddrs, 16)) { if (emulator_->readMemory(result, funcAddrs, 16)) {
std::cout << "[WardenModule] Module exported functions:" << '\n'; char fb[4][32];
std::cout << "[WardenModule] generateRC4Keys: 0x" << std::hex << funcAddrs[0] << std::dec << '\n'; for (int fi = 0; fi < 4; ++fi)
std::cout << "[WardenModule] unload: 0x" << std::hex << funcAddrs[1] << std::dec << '\n'; std::snprintf(fb[fi], sizeof(fb[fi]), "0x%X", funcAddrs[fi]);
std::cout << "[WardenModule] packetHandler: 0x" << std::hex << funcAddrs[2] << std::dec << '\n'; LOG_INFO("WardenModule: Module exported functions:");
std::cout << "[WardenModule] tick: 0x" << std::hex << funcAddrs[3] << std::dec << '\n'; LOG_INFO("WardenModule: generateRC4Keys: ", fb[0]);
LOG_INFO("WardenModule: unload: ", fb[1]);
LOG_INFO("WardenModule: packetHandler: ", fb[2]);
LOG_INFO("WardenModule: tick: ", fb[3]);
// Store function addresses for later use // Store function addresses for later use
// funcList_.generateRC4Keys = ... (would wrap emulator calls) // funcList_.generateRC4Keys = ... (would wrap emulator calls)
@ -958,10 +980,10 @@ bool WardenModule::initializeModule() {
// funcList_.tick = ... // funcList_.tick = ...
} }
std::cout << "[WardenModule] ✓ Module fully initialized and ready!" << '\n'; LOG_INFO("WardenModule: Module fully initialized and ready!");
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "[WardenModule] Exception during module initialization: " << e.what() << '\n'; LOG_ERROR("WardenModule: Exception during module initialization: ", e.what());
return false; return false;
} }
@ -970,14 +992,14 @@ bool WardenModule::initializeModule() {
typedef void* (*ModuleEntryPoint)(ClientCallbacks*); typedef void* (*ModuleEntryPoint)(ClientCallbacks*);
ModuleEntryPoint entryPoint = reinterpret_cast<ModuleEntryPoint>(moduleMemory_); ModuleEntryPoint entryPoint = reinterpret_cast<ModuleEntryPoint>(moduleMemory_);
std::cout << "[WardenModule] Calling module entry point at " << moduleMemory_ << '\n'; LOG_INFO("WardenModule: Calling module entry point at ", moduleMemory_);
// NOTE: This would execute native x86 code // NOTE: This would execute native x86 code
// Extremely dangerous without proper validation! // Extremely dangerous without proper validation!
// void* result = entryPoint(&callbacks); // void* result = entryPoint(&callbacks);
std::cout << "[WardenModule] ⚠ Module entry point call is DISABLED (unsafe without validation)" << '\n'; LOG_WARNING("WardenModule: Module entry point call is DISABLED (unsafe without validation)");
std::cout << "[WardenModule] Would execute x86 code at " << moduleMemory_ << '\n'; LOG_INFO("WardenModule: Would execute x86 code at ", moduleMemory_);
// TODO: Extract WardenFuncList from result // TODO: Extract WardenFuncList from result
// funcList_.packetHandler = ... // funcList_.packetHandler = ...
@ -986,9 +1008,9 @@ bool WardenModule::initializeModule() {
// funcList_.unload = ... // funcList_.unload = ...
#else #else
std::cout << "[WardenModule] ⚠ Cannot execute Windows x86 code on Linux" << '\n'; LOG_WARNING("WardenModule: Cannot execute Windows x86 code on Linux");
std::cout << "[WardenModule] Module entry point: " << moduleMemory_ << '\n'; LOG_INFO("WardenModule: Module entry point: ", moduleMemory_);
std::cout << "[WardenModule] Would call entry point with ClientCallbacks struct" << '\n'; LOG_INFO("WardenModule: Would call entry point with ClientCallbacks struct");
#endif #endif
// For now, return true to mark module as "loaded" at infrastructure level // For now, return true to mark module as "loaded" at infrastructure level
@ -998,7 +1020,7 @@ bool WardenModule::initializeModule() {
// 3. Exception handling for crashes // 3. Exception handling for crashes
// 4. Sandboxing for security // 4. Sandboxing for security
std::cout << "[WardenModule] ⚠ Module initialization is STUB" << '\n'; LOG_WARNING("WardenModule: Module initialization is STUB");
return true; // Stub implementation return true; // Stub implementation
} }
@ -1023,7 +1045,7 @@ WardenModuleManager::WardenModuleManager() {
// Create cache directory if it doesn't exist // Create cache directory if it doesn't exist
std::filesystem::create_directories(cacheDirectory_); std::filesystem::create_directories(cacheDirectory_);
std::cout << "[WardenModuleManager] Cache directory: " << cacheDirectory_ << '\n'; LOG_INFO("WardenModuleManager: Cache directory: ", cacheDirectory_);
} }
WardenModuleManager::~WardenModuleManager() { WardenModuleManager::~WardenModuleManager() {
@ -1060,12 +1082,11 @@ bool WardenModuleManager::receiveModuleChunk(const std::vector<uint8_t>& md5Hash
std::vector<uint8_t>& buffer = downloadBuffer_[md5Hash]; std::vector<uint8_t>& buffer = downloadBuffer_[md5Hash];
buffer.insert(buffer.end(), chunkData.begin(), chunkData.end()); buffer.insert(buffer.end(), chunkData.begin(), chunkData.end());
std::cout << "[WardenModuleManager] Received chunk (" << chunkData.size() LOG_INFO("WardenModuleManager: Received chunk (", chunkData.size(),
<< " bytes, total: " << buffer.size() << ")" << '\n'; " bytes, total: ", buffer.size(), ")");
if (isComplete) { if (isComplete) {
std::cout << "[WardenModuleManager] Module download complete (" LOG_INFO("WardenModuleManager: Module download complete (", buffer.size(), " bytes)");
<< buffer.size() << " bytes)" << '\n';
// Cache to disk // Cache to disk
cacheModule(md5Hash, buffer); cacheModule(md5Hash, buffer);
@ -1085,14 +1106,14 @@ bool WardenModuleManager::cacheModule(const std::vector<uint8_t>& md5Hash,
std::ofstream file(cachePath, std::ios::binary); std::ofstream file(cachePath, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "[WardenModuleManager] Failed to write cache: " << cachePath << '\n'; LOG_ERROR("WardenModuleManager: Failed to write cache: ", cachePath);
return false; return false;
} }
file.write(reinterpret_cast<const char*>(moduleData.data()), moduleData.size()); file.write(reinterpret_cast<const char*>(moduleData.data()), moduleData.size());
file.close(); file.close();
std::cout << "[WardenModuleManager] Cached module to: " << cachePath << '\n'; LOG_INFO("WardenModuleManager: Cached module to: ", cachePath);
return true; return true;
} }
@ -1116,7 +1137,7 @@ bool WardenModuleManager::loadCachedModule(const std::vector<uint8_t>& md5Hash,
file.read(reinterpret_cast<char*>(moduleDataOut.data()), fileSize); file.read(reinterpret_cast<char*>(moduleDataOut.data()), fileSize);
file.close(); file.close();
std::cout << "[WardenModuleManager] Loaded cached module (" << fileSize << " bytes)" << '\n'; LOG_INFO("WardenModuleManager: Loaded cached module (", fileSize, " bytes)");
return true; return true;
} }

View file

@ -4257,10 +4257,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
data.gold = packet.readUInt32(); data.gold = packet.readUInt32();
uint8_t itemCount = packet.readUInt8(); uint8_t itemCount = packet.readUInt8();
// Item wire size: // Per-item wire size is 22 bytes across all expansions:
// WotLK 3.3.5a: slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22 // slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22
// Classic/TBC: slot(1)+itemId(4)+count(4)+displayInfo(4)+slotType(1) = 14 constexpr size_t kItemSize = 22u;
const size_t kItemSize = isWotlkFormat ? 22u : 14u;
auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool { auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool {
for (uint8_t i = 0; i < listCount; ++i) { for (uint8_t i = 0; i < listCount; ++i) {
@ -4270,21 +4269,14 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
} }
LootItem item; LootItem item;
item.slotIndex = packet.readUInt8(); item.slotIndex = packet.readUInt8();
item.itemId = packet.readUInt32(); item.itemId = packet.readUInt32();
item.count = packet.readUInt32(); item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32(); item.displayInfoId = packet.readUInt32();
item.randomSuffix = packet.readUInt32();
if (isWotlkFormat) { item.randomPropertyId = packet.readUInt32();
item.randomSuffix = packet.readUInt32(); item.lootSlotType = packet.readUInt8();
item.randomPropertyId = packet.readUInt32(); item.isQuestItem = markQuestItems;
} else {
item.randomSuffix = 0;
item.randomPropertyId = 0;
}
item.lootSlotType = packet.readUInt8();
item.isQuestItem = markQuestItems;
data.items.push_back(item); data.items.push_back(item);
} }
return true; return true;
@ -4296,8 +4288,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
return false; return false;
} }
// Quest item section only present in WotLK 3.3.5a
uint8_t questItemCount = 0; uint8_t questItemCount = 0;
if (packet.getSize() - packet.getReadPos() >= 1) { if (isWotlkFormat && packet.getSize() - packet.getReadPos() >= 1) {
questItemCount = packet.readUInt8(); questItemCount = packet.readUInt8();
data.items.reserve(data.items.size() + questItemCount); data.items.reserve(data.items.size() + questItemCount);
if (!parseLootItemList(questItemCount, true)) { if (!parseLootItemList(questItemCount, true)) {

View file

@ -228,6 +228,7 @@ void Minimap::shutdown() {
if (tex) tex->destroy(device, alloc); if (tex) tex->destroy(device, alloc);
} }
tileTextureCache.clear(); tileTextureCache.clear();
tileInsertionOrder.clear();
if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); } if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); }
if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); } if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); }
@ -362,6 +363,15 @@ VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) {
VkTexture* ptr = tex.get(); VkTexture* ptr = tex.get();
tileTextureCache[hash] = std::move(tex); tileTextureCache[hash] = std::move(tex);
tileInsertionOrder.push_back(hash);
// Evict oldest tiles when cache grows too large to bound GPU memory usage.
while (tileInsertionOrder.size() > MAX_TILE_CACHE) {
const std::string& oldest = tileInsertionOrder.front();
tileTextureCache.erase(oldest);
tileInsertionOrder.pop_front();
}
return ptr; return ptr;
} }
@ -513,14 +523,15 @@ void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera,
float arrowRotation = 0.0f; float arrowRotation = 0.0f;
if (!rotateWithCamera) { if (!rotateWithCamera) {
// Prefer authoritative orientation if provided. This value is expected
// to already match minimap shader rotation convention.
if (hasPlayerOrientation) { if (hasPlayerOrientation) {
arrowRotation = playerOrientation; arrowRotation = playerOrientation;
} else { } else {
glm::vec3 fwd = playerCamera.getForward(); glm::vec3 fwd = playerCamera.getForward();
arrowRotation = std::atan2(-fwd.x, fwd.y); arrowRotation = -std::atan2(-fwd.x, fwd.y);
} }
} else if (hasPlayerOrientation) {
// Show character facing relative to the rotated map
arrowRotation = playerOrientation + rotation;
} }
MinimapDisplayPush push{}; MinimapDisplayPush push{};

View file

@ -5287,6 +5287,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
renderOverlay(tint, cmd); renderOverlay(tint, cmd);
} }
} }
// Ghost mode desaturation: cold blue-grey overlay when dead/ghost
if (ghostMode_) {
renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd);
}
// Brightness overlay (applied before minimap so it doesn't affect UI)
if (brightness_ < 0.99f) {
renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_), cmd);
} else if (brightness_ > 1.01f) {
float alpha = (brightness_ - 1.0f) / 1.0f; // maps 1.0-2.0 → 0.0-1.0
renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd);
}
if (minimap && minimap->isEnabled() && camera && window) { if (minimap && minimap->isEnabled() && camera && window) {
glm::vec3 minimapCenter = camera->getPosition(); glm::vec3 minimapCenter = camera->getPosition();
if (cameraController && cameraController->isThirdPerson()) if (cameraController && cameraController->isThirdPerson())
@ -5421,6 +5432,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
renderOverlay(tint); renderOverlay(tint);
} }
} }
// Ghost mode desaturation: cold blue-grey overlay when dead/ghost
if (ghostMode_) {
renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f));
}
// Brightness overlay (applied before minimap so it doesn't affect UI)
if (brightness_ < 0.99f) {
renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_));
} else if (brightness_ > 1.01f) {
float alpha = (brightness_ - 1.0f) / 1.0f;
renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha));
}
if (minimap && minimap->isEnabled() && camera && window) { if (minimap && minimap->isEnabled() && camera && window) {
glm::vec3 minimapCenter = camera->getPosition(); glm::vec3 minimapCenter = camera->getPosition();
if (cameraController && cameraController->isThirdPerson()) if (cameraController && cameraController->isThirdPerson())

View file

@ -835,7 +835,8 @@ void WorldMap::zoomOut() {
// Main render (input + ImGui overlay) // Main render (input + ImGui overlay)
// -------------------------------------------------------- // --------------------------------------------------------
void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight,
float playerYawDeg) {
if (!initialized || !assetManager) return; if (!initialized || !assetManager) return;
auto& input = core::Input::getInstance(); auto& input = core::Input::getInstance();
@ -886,14 +887,14 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr
} }
if (!open) return; if (!open) return;
renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight); renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight, playerYawDeg);
} }
// -------------------------------------------------------- // --------------------------------------------------------
// ImGui overlay // ImGui overlay
// -------------------------------------------------------- // --------------------------------------------------------
void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, float playerYawDeg) {
float sw = static_cast<float>(screenWidth); float sw = static_cast<float>(screenWidth);
float sh = static_cast<float>(screenHeight); float sh = static_cast<float>(screenHeight);
@ -1014,8 +1015,20 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi
playerUV.y >= 0.0f && playerUV.y <= 1.0f) { playerUV.y >= 0.0f && playerUV.y <= 1.0f) {
float px = imgMin.x + playerUV.x * displayW; float px = imgMin.x + playerUV.x * displayW;
float py = imgMin.y + playerUV.y * displayH; float py = imgMin.y + playerUV.y * displayH;
drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255)); // Directional arrow: render-space (cos,sin) maps to screen (-dx,-dy)
drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f); // because render+X=west=left and render+Y=north=up (screen Y is down).
float yawRad = glm::radians(playerYawDeg);
float adx = -std::cos(yawRad); // screen-space arrow X
float ady = -std::sin(yawRad); // screen-space arrow Y
float apx = -ady, apy = adx; // perpendicular (left/right of arrow)
constexpr float TIP = 9.0f; // tip distance from center
constexpr float TAIL = 4.0f; // tail distance from center
constexpr float HALF = 5.0f; // half base width
ImVec2 tip(px + adx * TIP, py + ady * TIP);
ImVec2 bl (px - adx * TAIL + apx * HALF, py - ady * TAIL + apy * HALF);
ImVec2 br (px - adx * TAIL - apx * HALF, py - ady * TAIL - apy * HALF);
drawList->AddTriangleFilled(tip, bl, br, IM_COL32(255, 40, 40, 255));
drawList->AddTriangle(tip, bl, br, IM_COL32(0, 0, 0, 200), 1.5f);
} }
} }

View file

@ -2091,18 +2091,20 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
std::string cmd = buf.substr(1, sp - 1); std::string cmd = buf.substr(1, sp - 1);
for (char& c : cmd) c = std::tolower(c); for (char& c : cmd) c = std::tolower(c);
int detected = -1; int detected = -1;
bool isReply = false;
if (cmd == "s" || cmd == "say") detected = 0; if (cmd == "s" || cmd == "say") detected = 0;
else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1; else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1;
else if (cmd == "p" || cmd == "party") detected = 2; else if (cmd == "p" || cmd == "party") detected = 2;
else if (cmd == "g" || cmd == "guild") detected = 3; else if (cmd == "g" || cmd == "guild") detected = 3;
else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4; else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4;
else if (cmd == "r" || cmd == "reply") { detected = 4; isReply = true; }
else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5; else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5;
else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6; else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6;
else if (cmd == "bg" || cmd == "battleground") detected = 7; else if (cmd == "bg" || cmd == "battleground") detected = 7;
else if (cmd == "rw" || cmd == "raidwarning") detected = 8; else if (cmd == "rw" || cmd == "raidwarning") detected = 8;
else if (cmd == "i" || cmd == "instance") detected = 9; else if (cmd == "i" || cmd == "instance") detected = 9;
else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc. else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc.
if (detected >= 0 && (selectedChatType != detected || detected == 10)) { if (detected >= 0 && (selectedChatType != detected || detected == 10 || isReply)) {
// For channel shortcuts, also update selectedChannelIdx // For channel shortcuts, also update selectedChannelIdx
if (detected == 10) { if (detected == 10) {
int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc. int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc.
@ -2114,8 +2116,16 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
selectedChatType = detected; selectedChatType = detected;
// Strip the prefix, keep only the message part // Strip the prefix, keep only the message part
std::string remaining = buf.substr(sp + 1); std::string remaining = buf.substr(sp + 1);
// For whisper, first word after /w is the target // /r reply: pre-fill whisper target from last whisper sender
if (detected == 4) { if (detected == 4 && isReply) {
std::string lastSender = gameHandler.getLastWhisperSender();
if (!lastSender.empty()) {
strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1);
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
}
// remaining is the message — don't extract a target from it
} else if (detected == 4) {
// For whisper, first word after /w is the target
size_t msgStart = remaining.find(' '); size_t msgStart = remaining.find(' ');
if (msgStart != std::string::npos) { if (msgStart != std::string::npos) {
std::string wTarget = remaining.substr(0, msgStart); std::string wTarget = remaining.substr(0, msgStart);
@ -2576,6 +2586,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
uint64_t closestHostileUnitGuid = 0; uint64_t closestHostileUnitGuid = 0;
float closestQuestGoT = 1e30f; float closestQuestGoT = 1e30f;
uint64_t closestQuestGoGuid = 0; uint64_t closestQuestGoGuid = 0;
float closestGoT = 1e30f;
uint64_t closestGoGuid = 0;
const uint64_t myGuid = gameHandler.getPlayerGuid(); const uint64_t myGuid = gameHandler.getPlayerGuid();
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
auto t = entity->getType(); auto t = entity->getType();
@ -2598,16 +2610,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
heightOffset = 0.3f; heightOffset = 0.3f;
} }
} else if (t == game::ObjectType::GAMEOBJECT) { } else if (t == game::ObjectType::GAMEOBJECT) {
// For GOs with no renderer instance yet, use a tight fallback hitRadius = 2.5f;
// sphere so invisible/unloaded doodads aren't accidentally clicked. heightOffset = 1.2f;
hitRadius = 1.2f;
heightOffset = 1.0f;
// Quest objective GOs should be easier to click.
auto go = std::static_pointer_cast<game::GameObject>(entity);
if (questObjectiveGoEntries.count(go->getEntry())) {
hitRadius = 2.2f;
heightOffset = 1.2f;
}
} }
hitCenter = core::coords::canonicalToRender( hitCenter = core::coords::canonicalToRender(
glm::vec3(entity->getX(), entity->getY(), entity->getZ())); glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
@ -2626,12 +2630,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
closestHostileUnitGuid = guid; closestHostileUnitGuid = guid;
} }
} }
if (t == game::ObjectType::GAMEOBJECT && !questObjectiveGoEntries.empty()) { if (t == game::ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<game::GameObject>(entity); if (hitT < closestGoT) {
if (questObjectiveGoEntries.count(go->getEntry())) { closestGoT = hitT;
if (hitT < closestQuestGoT) { closestGoGuid = guid;
closestQuestGoT = hitT; }
closestQuestGoGuid = guid; if (!questObjectiveGoEntries.empty()) {
auto go = std::static_pointer_cast<game::GameObject>(entity);
if (questObjectiveGoEntries.count(go->getEntry())) {
if (hitT < closestQuestGoT) {
closestQuestGoT = hitT;
closestQuestGoGuid = guid;
}
} }
} }
} }
@ -2643,12 +2653,23 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
} }
} }
// Prefer quest objective GOs over hostile monsters when both are hittable. // Priority: quest GO > closer of (GO, hostile unit) > closest anything.
if (closestQuestGoGuid != 0) { if (closestQuestGoGuid != 0) {
closestGuid = closestQuestGoGuid; closestGuid = closestQuestGoGuid;
closestType = game::ObjectType::GAMEOBJECT; closestType = game::ObjectType::GAMEOBJECT;
} else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) {
// Both a GO and hostile unit were hit — prefer whichever is closer.
if (closestGoT <= closestHostileUnitT) {
closestGuid = closestGoGuid;
closestType = game::ObjectType::GAMEOBJECT;
} else {
closestGuid = closestHostileUnitGuid;
closestType = game::ObjectType::UNIT;
}
} else if (closestGoGuid != 0) {
closestGuid = closestGoGuid;
closestType = game::ObjectType::GAMEOBJECT;
} else if (closestHostileUnitGuid != 0) { } else if (closestHostileUnitGuid != 0) {
// Prefer hostile monsters over nearby gameobjects/others when right-click picking.
closestGuid = closestHostileUnitGuid; closestGuid = closestHostileUnitGuid;
closestType = game::ObjectType::UNIT; closestType = game::ObjectType::UNIT;
} }
@ -3536,17 +3557,22 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
// WoW level-based color for hostile mobs // WoW level-based color for hostile mobs
uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t playerLv = gameHandler.getPlayerLevel();
uint32_t mobLv = u->getLevel(); uint32_t mobLv = u->getLevel();
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv); if (mobLv == 0) {
if (game::GameHandler::killXp(playerLv, mobLv) == 0) { // Level 0 = unknown/?? (e.g. high-level raid bosses) — always skull red
hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
} else if (diff >= 10) {
hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard
} else if (diff >= 5) {
hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard
} else if (diff >= -2) {
hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even
} else { } else {
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
if (game::GameHandler::killXp(playerLv, mobLv) == 0) {
hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP
} else if (diff >= 10) {
hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard
} else if (diff >= 5) {
hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard
} else if (diff >= -2) {
hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even
} else {
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy
}
} }
} else { } else {
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly
@ -3724,7 +3750,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
if (target->getType() == game::ObjectType::PLAYER) { if (target->getType() == game::ObjectType::PLAYER) {
levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
} }
ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); if (unit->getLevel() == 0)
ImGui::TextColored(levelColor, "Lv ??");
else
ImGui::TextColored(levelColor, "Lv %u", unit->getLevel());
// Classification badge: Elite / Rare Elite / Boss / Rare // Classification badge: Elite / Rare Elite / Boss / Rare
if (target->getType() == game::ObjectType::UNIT) { if (target->getType() == game::ObjectType::UNIT) {
int rank = gameHandler.getCreatureRank(unit->getEntry()); int rank = gameHandler.getCreatureRank(unit->getEntry());
@ -4315,17 +4344,21 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
} else if (u->isHostile()) { } else if (u->isHostile()) {
uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t playerLv = gameHandler.getPlayerLevel();
uint32_t mobLv = u->getLevel(); uint32_t mobLv = u->getLevel();
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv); if (mobLv == 0) {
if (game::GameHandler::killXp(playerLv, mobLv) == 0) focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // ?? level = skull red
focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); } else {
else if (diff >= 10) int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); if (game::GameHandler::killXp(playerLv, mobLv) == 0)
else if (diff >= 5) focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f);
focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); else if (diff >= 10)
else if (diff >= -2) focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); else if (diff >= 5)
else focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f);
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); else if (diff >= -2)
focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f);
else
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
}
} else { } else {
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
} }
@ -4458,7 +4491,10 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
// Level + health on same row // Level + health on same row
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("Lv %u", unit->getLevel()); if (unit->getLevel() == 0)
ImGui::TextDisabled("Lv ??");
else
ImGui::TextDisabled("Lv %u", unit->getLevel());
uint32_t hp = unit->getHealth(); uint32_t hp = unit->getHealth();
uint32_t maxHp = unit->getMaxHealth(); uint32_t maxHp = unit->getMaxHealth();
@ -5951,6 +5987,28 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
message = ""; message = "";
isChannelCommand = true; isChannelCommand = true;
} }
} else if (cmdLower == "r" || cmdLower == "reply") {
switchChatType = 4;
std::string lastSender = gameHandler.getLastWhisperSender();
if (lastSender.empty()) {
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = "No one has whispered you yet.";
gameHandler.addLocalChatMessage(sysMsg);
chatInputBuffer[0] = '\0';
return;
}
target = lastSender;
strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1);
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
if (spacePos != std::string::npos) {
message = command.substr(spacePos + 1);
type = game::ChatType::WHISPER;
} else {
message = "";
}
isChannelCommand = true;
} }
// Check for emote commands // Check for emote commands
@ -6496,10 +6554,11 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
} }
glm::vec3 playerPos = renderer->getCharacterPosition(); glm::vec3 playerPos = renderer->getCharacterPosition();
float playerYaw = renderer->getCharacterYaw();
auto* window = app.getWindow(); auto* window = app.getWindow();
int screenW = window ? window->getWidth() : 1280; int screenW = window ? window->getWidth() : 1280;
int screenH = window ? window->getHeight() : 720; int screenH = window ? window->getHeight() : 720;
wm->render(playerPos, screenW, screenH); wm->render(playerPos, screenW, screenH, playerYaw);
// Sync showWorldMap_ if the map closed itself (e.g. ESC key inside the overlay). // Sync showWorldMap_ if the map closed itself (e.g. ESC key inside the overlay).
if (!wm->isOpen()) showWorldMap_ = false; if (!wm->isOpen()) showWorldMap_ = false;
@ -6551,17 +6610,23 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage
} }
}; };
// Always use expansion-aware layout if available // Use expansion-aware layout if available AND the DBC field count
// Field indices vary by expansion: Classic=117, TBC=124, WotLK=133 // matches the expansion's expected format. Classic=173, TBC=216,
// WotLK=234 fields. When Classic is active but the base WotLK DBC
// is loaded (234 fields), field 117 is NOT IconID — we must use
// the WotLK field 133 instead.
uint32_t iconField = 133; // WotLK default
uint32_t idField = 0;
if (spellL) { if (spellL) {
tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]); uint32_t layoutIcon = (*spellL)["IconID"];
} // Only trust the expansion layout if the DBC has a compatible
// field count (within ~20 of the layout's icon field).
// Fallback if expansion layout missing or yielded nothing if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) {
// Only use WotLK field 133 as last resort if we have no layout iconField = layoutIcon;
if (spellIconIds_.empty() && !spellL && fieldCount > 133) { idField = (*spellL)["ID"];
tryLoadIcons(0, 133); }
} }
tryLoadIcons(idField, iconField);
} }
} }
@ -6664,6 +6729,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
// Out-of-range check: red tint when a targeted spell cannot reach the current target. // Out-of-range check: red tint when a targeted spell cannot reach the current target.
// Only applies to SPELL slots with a known max range (>5 yd) and an active target. // Only applies to SPELL slots with a known max range (>5 yd) and an active target.
// Item range is checked below after barItemDef is populated.
bool outOfRange = false; bool outOfRange = false;
if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0
&& !onCooldown && gameHandler.hasTarget()) { && !onCooldown && gameHandler.hasTarget()) {
@ -6753,6 +6819,33 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0 const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0
&& barItemDef == nullptr && !onCooldown); && barItemDef == nullptr && !onCooldown);
// Ranged item out-of-range check (runs after barItemDef is populated above).
// invType 15=Ranged (bow/gun/crossbow), 26=Thrown, 28=RangedRight (wand/crossbow).
if (!outOfRange && slot.type == game::ActionBarSlot::ITEM && barItemDef
&& !onCooldown && gameHandler.hasTarget()) {
constexpr uint8_t INVTYPE_RANGED = 15;
constexpr uint8_t INVTYPE_THROWN = 26;
constexpr uint8_t INVTYPE_RANGEDRIGHT = 28;
uint32_t itemMaxRange = 0;
if (barItemDef->inventoryType == INVTYPE_RANGED ||
barItemDef->inventoryType == INVTYPE_RANGEDRIGHT)
itemMaxRange = 40;
else if (barItemDef->inventoryType == INVTYPE_THROWN)
itemMaxRange = 30;
if (itemMaxRange > 0) {
auto& em = gameHandler.getEntityManager();
auto playerEnt = em.getEntity(gameHandler.getPlayerGuid());
auto targetEnt = em.getEntity(gameHandler.getTargetGuid());
if (playerEnt && targetEnt) {
float dx = playerEnt->getX() - targetEnt->getX();
float dy = playerEnt->getY() - targetEnt->getY();
float dz = playerEnt->getZ() - targetEnt->getZ();
if (std::sqrt(dx*dx + dy*dy + dz*dz) > static_cast<float>(itemMaxRange))
outOfRange = true;
}
}
}
bool clicked = false; bool clicked = false;
if (iconTex) { if (iconTex) {
ImVec4 tintColor(1, 1, 1, 1); ImVec4 tintColor(1, 1, 1, 1);
@ -7645,7 +7738,8 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0; uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0;
ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience"); ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience");
ImGui::Separator(); ImGui::Separator();
ImGui::Text("Current: %u / %u XP", currentXp, nextLevelXp); float xpPct = nextLevelXp > 0 ? (100.0f * currentXp / nextLevelXp) : 0.0f;
ImGui::Text("Current: %u / %u XP (%.1f%%)", currentXp, nextLevelXp, xpPct);
ImGui::Text("To next level: %u XP", xpToLevel); ImGui::Text("To next level: %u XP", xpToLevel);
if (restedXp > 0) { if (restedXp > 0) {
float restedLevels = static_cast<float>(restedXp) / static_cast<float>(nextLevelXp); float restedLevels = static_cast<float>(restedXp) / static_cast<float>(nextLevelXp);
@ -7828,16 +7922,21 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) {
: ImVec4(0.8f, 0.6f, 0.2f, 1.0f); // gold for casts : ImVec4(0.8f, 0.6f, 0.2f, 1.0f); // gold for casts
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
char overlay[64]; char overlay[96];
if (currentSpellId == 0) { if (currentSpellId == 0) {
snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining()); snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining());
} else { } else {
const std::string& spellName = gameHandler.getSpellName(currentSpellId); const std::string& spellName = gameHandler.getSpellName(currentSpellId);
const char* verb = channeling ? "Channeling" : "Casting"; const char* verb = channeling ? "Channeling" : "Casting";
if (!spellName.empty()) int queueLeft = gameHandler.getCraftQueueRemaining();
snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining()); if (!spellName.empty()) {
else if (queueLeft > 0)
snprintf(overlay, sizeof(overlay), "%s (%.1fs) [%d left]", spellName.c_str(), gameHandler.getCastTimeRemaining(), queueLeft);
else
snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining());
} else {
snprintf(overlay, sizeof(overlay), "%s... (%.1fs)", verb, gameHandler.getCastTimeRemaining()); snprintf(overlay, sizeof(overlay), "%s... (%.1fs)", verb, gameHandler.getCastTimeRemaining());
}
} }
if (iconTex) { if (iconTex) {
@ -8413,10 +8512,21 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
snprintf(text, sizeof(text), "+%d", entry.amount); snprintf(text, sizeof(text), "+%d", entry.amount);
color = ImVec4(0.4f, 1.0f, 0.5f, alpha); color = ImVec4(0.4f, 1.0f, 0.5f, alpha);
break; break;
case game::CombatTextEntry::ENVIRONMENTAL: case game::CombatTextEntry::ENVIRONMENTAL: {
snprintf(text, sizeof(text), "-%d", entry.amount); const char* envLabel = "";
switch (entry.powerType) {
case 0: envLabel = "Fatigue "; break;
case 1: envLabel = "Drowning "; break;
case 2: envLabel = ""; break; // Fall: just show the number (WoW convention)
case 3: envLabel = "Lava "; break;
case 4: envLabel = "Slime "; break;
case 5: envLabel = "Fire "; break;
default: envLabel = ""; break;
}
snprintf(text, sizeof(text), "%s-%d", envLabel, entry.amount);
color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental
break; break;
}
case game::CombatTextEntry::ENERGIZE: case game::CombatTextEntry::ENERGIZE:
snprintf(text, sizeof(text), "+%d", entry.amount); snprintf(text, sizeof(text), "+%d", entry.amount);
switch (entry.powerType) { switch (entry.powerType) {
@ -9285,6 +9395,15 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
else if (m.roles & 0x08) else if (m.roles & 0x08)
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D"); draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D");
// Tactical role badge in bottom-left corner (flags from SMSG_GROUP_LIST / SMSG_REAL_GROUP_UPDATE)
// 0x01=Assistant, 0x02=Main Tank, 0x04=Main Assist
if (m.flags & 0x02)
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(255, 140, 0, 230), "MT");
else if (m.flags & 0x04)
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(100, 180, 255, 230), "MA");
else if (m.flags & 0x01)
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(180, 215, 255, 180), "A");
// Health bar // Health bar
uint32_t hp = m.hasPartyStats ? m.curHealth : 0; uint32_t hp = m.hasPartyStats ? m.curHealth : 0;
uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0; uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0;
@ -9520,6 +9639,18 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); } if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); }
} }
// Tactical role badge (MT/MA/Asst) from group flags
if (member.flags & 0x02) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.0f, 0.9f), "[MT]");
} else if (member.flags & 0x04) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 0.9f), "[MA]");
} else if (member.flags & 0x01) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 0.7f), "[A]");
}
// Raid mark symbol — shown on same line as name when this party member has a mark // Raid mark symbol — shown on same line as name when this party member has a mark
{ {
static const struct { const char* sym; ImU32 col; } kPartyMarks[] = { static const struct { const char* sym; ImU32 col; } kPartyMarks[] = {
@ -13618,6 +13749,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
} }
const auto& trainer = gameHandler.getTrainerSpells(); const auto& trainer = gameHandler.getTrainerSpells();
const bool isProfessionTrainer = (trainer.trainerType == 2);
// NPC name // NPC name
auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid); auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid);
@ -13838,11 +13970,21 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
logCount++; logCount++;
} }
if (!canTrain) ImGui::BeginDisabled(); if (isProfessionTrainer && alreadyKnown) {
if (ImGui::SmallButton("Train")) { // Profession trainer: known recipes show "Create" button to craft
gameHandler.trainSpell(spell->spellId); bool isCasting = gameHandler.isCasting();
if (isCasting) ImGui::BeginDisabled();
if (ImGui::SmallButton("Create")) {
gameHandler.castSpell(spell->spellId, 0);
}
if (isCasting) ImGui::EndDisabled();
} else {
if (!canTrain) ImGui::BeginDisabled();
if (ImGui::SmallButton("Train")) {
gameHandler.trainSpell(spell->spellId);
}
if (!canTrain) ImGui::EndDisabled();
} }
if (!canTrain) ImGui::EndDisabled();
ImGui::PopID(); ImGui::PopID();
} }
@ -13946,6 +14088,79 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
} }
} }
if (!hasTrainable) ImGui::EndDisabled(); if (!hasTrainable) ImGui::EndDisabled();
// Profession trainer: craft quantity controls
if (isProfessionTrainer) {
ImGui::Separator();
static int craftQuantity = 1;
static uint32_t selectedCraftSpell = 0;
// Show craft queue status if active
int queueRemaining = gameHandler.getCraftQueueRemaining();
if (queueRemaining > 0) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f),
"Crafting... %d remaining", queueRemaining);
ImGui::SameLine();
if (ImGui::SmallButton("Stop")) {
gameHandler.cancelCraftQueue();
gameHandler.cancelCast();
}
} else {
// Spell selector + quantity input
// Build list of known (craftable) spells
std::vector<const game::TrainerSpell*> craftable;
for (const auto& spell : trainer.spells) {
if (isKnown(spell.spellId)) {
craftable.push_back(&spell);
}
}
if (!craftable.empty()) {
// Combo box for recipe selection
const char* previewName = "Select recipe...";
for (const auto* sp : craftable) {
if (sp->spellId == selectedCraftSpell) {
const std::string& n = gameHandler.getSpellName(sp->spellId);
if (!n.empty()) previewName = n.c_str();
break;
}
}
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.55f);
if (ImGui::BeginCombo("##CraftSelect", previewName)) {
for (const auto* sp : craftable) {
const std::string& n = gameHandler.getSpellName(sp->spellId);
const std::string& r = gameHandler.getSpellRank(sp->spellId);
char label[128];
if (!r.empty())
snprintf(label, sizeof(label), "%s (%s)##%u",
n.empty() ? "???" : n.c_str(), r.c_str(), sp->spellId);
else
snprintf(label, sizeof(label), "%s##%u",
n.empty() ? "???" : n.c_str(), sp->spellId);
if (ImGui::Selectable(label, sp->spellId == selectedCraftSpell)) {
selectedCraftSpell = sp->spellId;
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(50.0f);
ImGui::InputInt("##CraftQty", &craftQuantity, 0, 0);
if (craftQuantity < 1) craftQuantity = 1;
if (craftQuantity > 99) craftQuantity = 99;
ImGui::SameLine();
bool canCraft = selectedCraftSpell != 0 && !gameHandler.isCasting();
if (!canCraft) ImGui::BeginDisabled();
if (ImGui::Button("Create")) {
if (craftQuantity == 1) {
gameHandler.castSpell(selectedCraftSpell, 0);
} else {
gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity);
}
}
if (!canCraft) ImGui::EndDisabled();
}
}
}
} }
} }
ImGui::End(); ImGui::End();
@ -14894,6 +15109,16 @@ void GameScreen::renderSettingsWindow() {
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::SetNextItemWidth(200.0f);
if (ImGui::SliderInt("Brightness", &pendingBrightness, 0, 100, "%d%%")) {
if (renderer) renderer->setBrightness(static_cast<float>(pendingBrightness) / 50.0f);
saveSettings();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) { if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) {
pendingFullscreen = kDefaultFullscreen; pendingFullscreen = kDefaultFullscreen;
pendingVsync = kDefaultVsync; pendingVsync = kDefaultVsync;
@ -14906,9 +15131,11 @@ void GameScreen::renderSettingsWindow() {
pendingPOM = true; pendingPOM = true;
pendingPOMQuality = 1; pendingPOMQuality = 1;
pendingResIndex = defaultResIndex; pendingResIndex = defaultResIndex;
pendingBrightness = 50;
window->setFullscreen(pendingFullscreen); window->setFullscreen(pendingFullscreen);
window->setVsync(pendingVsync); window->setVsync(pendingVsync);
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
if (renderer) renderer->setBrightness(1.0f);
pendingWaterRefraction = false; pendingWaterRefraction = false;
if (renderer) { if (renderer) {
renderer->setShadowsEnabled(pendingShadows); renderer->setShadowsEnabled(pendingShadows);
@ -15320,6 +15547,10 @@ void GameScreen::renderSettingsWindow() {
inventoryScreen.setSeparateBags(pendingSeparateBags); inventoryScreen.setSeparateBags(pendingSeparateBags);
saveSettings(); saveSettings();
} }
if (ImGui::Checkbox("Show Key Ring", &pendingShowKeyring)) {
inventoryScreen.setShowKeyring(pendingShowKeyring);
saveSettings();
}
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
@ -15335,6 +15566,8 @@ void GameScreen::renderSettingsWindow() {
pendingMinimapNpcDots = false; pendingMinimapNpcDots = false;
pendingSeparateBags = true; pendingSeparateBags = true;
inventoryScreen.setSeparateBags(true); inventoryScreen.setSeparateBags(true);
pendingShowKeyring = true;
inventoryScreen.setShowKeyring(true);
uiOpacity_ = 0.65f; uiOpacity_ = 0.65f;
minimapRotate_ = false; minimapRotate_ = false;
minimapSquare_ = false; minimapSquare_ = false;
@ -17238,6 +17471,7 @@ void GameScreen::saveSettings() {
out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n"; out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n";
out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n"; out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n";
out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n";
out << "show_keyring=" << (pendingShowKeyring ? 1 : 0) << "\n";
out << "action_bar_scale=" << pendingActionBarScale << "\n"; out << "action_bar_scale=" << pendingActionBarScale << "\n";
out << "nameplate_scale=" << nameplateScale_ << "\n"; out << "nameplate_scale=" << nameplateScale_ << "\n";
out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n"; out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n";
@ -17271,6 +17505,7 @@ void GameScreen::saveSettings() {
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
out << "shadows=" << (pendingShadows ? 1 : 0) << "\n"; out << "shadows=" << (pendingShadows ? 1 : 0) << "\n";
out << "shadow_distance=" << pendingShadowDistance << "\n"; out << "shadow_distance=" << pendingShadowDistance << "\n";
out << "brightness=" << pendingBrightness << "\n";
out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n"; out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n";
out << "antialiasing=" << pendingAntiAliasing << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n";
out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n"; out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n";
@ -17359,6 +17594,9 @@ void GameScreen::loadSettings() {
} else if (key == "separate_bags") { } else if (key == "separate_bags") {
pendingSeparateBags = (std::stoi(val) != 0); pendingSeparateBags = (std::stoi(val) != 0);
inventoryScreen.setSeparateBags(pendingSeparateBags); inventoryScreen.setSeparateBags(pendingSeparateBags);
} else if (key == "show_keyring") {
pendingShowKeyring = (std::stoi(val) != 0);
inventoryScreen.setShowKeyring(pendingShowKeyring);
} else if (key == "action_bar_scale") { } else if (key == "action_bar_scale") {
pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f); pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f);
} else if (key == "nameplate_scale") { } else if (key == "nameplate_scale") {
@ -17412,6 +17650,11 @@ void GameScreen::loadSettings() {
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150); else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
else if (key == "shadows") pendingShadows = (std::stoi(val) != 0); else if (key == "shadows") pendingShadows = (std::stoi(val) != 0);
else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f); else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f);
else if (key == "brightness") {
pendingBrightness = std::clamp(std::stoi(val), 0, 100);
if (auto* r = core::Application::getInstance().getRenderer())
r->setBrightness(static_cast<float>(pendingBrightness) / 50.0f);
}
else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0); else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0);
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0); else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0);
@ -20372,24 +20615,54 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src); snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src);
color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f); color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f);
break; break;
case T::ENVIRONMENTAL: case T::ENVIRONMENTAL: {
snprintf(desc, sizeof(desc), "Environmental damage: %d", e.amount); const char* envName = "Environmental";
switch (e.powerType) {
case 0: envName = "Fatigue"; break;
case 1: envName = "Drowning"; break;
case 2: envName = "Falling"; break;
case 3: envName = "Lava"; break;
case 4: envName = "Slime"; break;
case 5: envName = "Fire"; break;
}
snprintf(desc, sizeof(desc), "%s damage: %d", envName, e.amount);
color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f); color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f);
break; break;
case T::ENERGIZE: }
case T::ENERGIZE: {
const char* pwrName = "power";
switch (e.powerType) {
case 0: pwrName = "Mana"; break;
case 1: pwrName = "Rage"; break;
case 2: pwrName = "Focus"; break;
case 3: pwrName = "Energy"; break;
case 4: pwrName = "Happiness"; break;
case 6: pwrName = "Runic Power"; break;
}
if (spell) if (spell)
snprintf(desc, sizeof(desc), "%s gains %d power (%s)", tgt, e.amount, spell); snprintf(desc, sizeof(desc), "%s gains %d %s (%s)", tgt, e.amount, pwrName, spell);
else else
snprintf(desc, sizeof(desc), "%s gains %d power", tgt, e.amount); snprintf(desc, sizeof(desc), "%s gains %d %s", tgt, e.amount, pwrName);
color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f);
break; break;
case T::POWER_DRAIN: }
case T::POWER_DRAIN: {
const char* drainName = "power";
switch (e.powerType) {
case 0: drainName = "Mana"; break;
case 1: drainName = "Rage"; break;
case 2: drainName = "Focus"; break;
case 3: drainName = "Energy"; break;
case 4: drainName = "Happiness"; break;
case 6: drainName = "Runic Power"; break;
}
if (spell) if (spell)
snprintf(desc, sizeof(desc), "%s loses %d power to %s's %s", tgt, e.amount, src, spell); snprintf(desc, sizeof(desc), "%s loses %d %s to %s's %s", tgt, e.amount, drainName, src, spell);
else else
snprintf(desc, sizeof(desc), "%s loses %d power", tgt, e.amount); snprintf(desc, sizeof(desc), "%s loses %d %s", tgt, e.amount, drainName);
color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f); color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f);
break; break;
}
case T::XP_GAIN: case T::XP_GAIN:
snprintf(desc, sizeof(desc), "You gain %d experience", e.amount); snprintf(desc, sizeof(desc), "You gain %d experience", e.amount);
color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f); color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f);

View file

@ -903,10 +903,10 @@ void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t m
float posX = screenW - windowW - 10.0f; float posX = screenW - windowW - 10.0f;
float posY = screenH - windowH - 60.0f; float posY = screenH - windowH - 60.0f;
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
if (!ImGui::Begin("Bags", &open, flags)) { if (!ImGui::Begin("Bags", &open, flags)) {
ImGui::End(); ImGui::End();
return; return;
@ -1030,8 +1030,7 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
float windowW = std::max(gridW, titleW); float windowW = std::max(gridW, titleW);
float windowH = contentH + 40.0f; // title bar + padding float windowH = contentH + 40.0f; // title bar + padding
// Keep separate bag windows anchored to the bag-bar stack. ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
@ -1069,20 +1068,29 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
ImGui::PopID(); ImGui::PopID();
} }
if (bagIndex < 0) { if (bagIndex < 0 && showKeyring_) {
ImGui::Spacing(); constexpr float keySlotSize = 24.0f;
ImGui::Separator(); constexpr int keyCols = 8;
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); // Only show rows that contain items (round up to full row)
for (int i = 0; i < inventory.getKeyringSize(); ++i) { int lastOccupied = -1;
if (i % columns != 0) ImGui::SameLine(); for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) {
const auto& slot = inventory.getKeyringSlot(i); if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; }
char id[32]; }
snprintf(id, sizeof(id), "##skr_%d", i); int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols;
ImGui::PushID(id); if (visibleSlots > 0) {
// Keyring is display-only for now. ImGui::Spacing();
renderItemSlot(inventory, slot, slotSize, nullptr, ImGui::Separator();
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
ImGui::PopID(); for (int i = 0; i < visibleSlots; ++i) {
if (i % keyCols != 0) ImGui::SameLine();
const auto& slot = inventory.getKeyringSlot(i);
char id[32];
snprintf(id, sizeof(id), "##skr_%d", i);
ImGui::PushID(id);
renderItemSlot(inventory, slot, keySlotSize, nullptr,
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
ImGui::PopID();
}
} }
} }
@ -2042,27 +2050,28 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool colla
} }
} }
bool keyringHasAnyItems = false; if (showKeyring_) {
for (int i = 0; i < inventory.getKeyringSize(); ++i) { constexpr float keySlotSize = 24.0f;
if (!inventory.getKeyringSlot(i).empty()) { constexpr int keyCols = 8;
keyringHasAnyItems = true; int lastOccupied = -1;
break; for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) {
if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; }
} }
} int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols;
if (!collapseEmptySections || keyringHasAnyItems) { if (visibleSlots > 0) {
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
for (int i = 0; i < inventory.getKeyringSize(); ++i) { for (int i = 0; i < visibleSlots; ++i) {
if (i % columns != 0) ImGui::SameLine(); if (i % keyCols != 0) ImGui::SameLine();
const auto& slot = inventory.getKeyringSlot(i); const auto& slot = inventory.getKeyringSlot(i);
char sid[32]; char sid[32];
snprintf(sid, sizeof(sid), "##keyring_%d", i); snprintf(sid, sizeof(sid), "##keyring_%d", i);
ImGui::PushID(sid); ImGui::PushID(sid);
// Keyring is display-only for now. renderItemSlot(inventory, slot, keySlotSize, nullptr,
renderItemSlot(inventory, slot, slotSize, nullptr, SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); ImGui::PopID();
ImGui::PopID(); }
} }
} }
} }
@ -2952,17 +2961,26 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
// Find a label for this stat type // Find a label for this stat type
const char* lbl = nullptr; const char* lbl = nullptr;
switch (t) { switch (t) {
case 31: lbl = "Hit"; break; case 0: lbl = "Mana"; break;
case 32: lbl = "Crit"; break; case 1: lbl = "Health"; break;
case 12: lbl = "Defense"; break;
case 13: lbl = "Dodge"; break;
case 14: lbl = "Parry"; break;
case 15: lbl = "Block Rating"; break;
case 16: case 17: case 18: case 31: lbl = "Hit"; break;
case 19: case 20: case 21: case 32: lbl = "Crit"; break;
case 28: case 29: case 30: case 36: lbl = "Haste"; break;
case 35: lbl = "Resilience"; break; case 35: lbl = "Resilience"; break;
case 36: lbl = "Haste"; break;
case 37: lbl = "Expertise"; break; case 37: lbl = "Expertise"; break;
case 38: lbl = "Attack Power"; break; case 38: lbl = "Attack Power"; break;
case 39: lbl = "Ranged AP"; break; case 39: lbl = "Ranged AP"; break;
case 41: lbl = "Healing"; break;
case 42: lbl = "Spell Damage"; break;
case 43: lbl = "MP5"; break; case 43: lbl = "MP5"; break;
case 44: lbl = "Armor Pen"; break; case 44: lbl = "Armor Pen"; break;
case 45: lbl = "Spell Power"; break; case 45: lbl = "Spell Power"; break;
case 46: lbl = "HP5"; break; case 46: lbl = "HP5"; break;
case 47: lbl = "Spell Pen"; break;
case 48: lbl = "Block Value"; break; case 48: lbl = "Block Value"; break;
default: lbl = nullptr; break; default: lbl = nullptr; break;
} }
@ -2970,6 +2988,10 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
showDiff(lbl, static_cast<float>(nv), static_cast<float>(ev)); showDiff(lbl, static_cast<float>(nv), static_cast<float>(ev));
} }
} }
} else if (inventory && !ImGui::GetIO().KeyShift && item.inventoryType > 0) {
if (findComparableEquipped(*inventory, item.inventoryType)) {
ImGui::TextDisabled("Hold Shift to compare");
}
} }
// Destroy hint (not shown for quest items) // Destroy hint (not shown for quest items)
@ -3502,6 +3524,16 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
ImGui::TextColored(ic, "%s", ilvlBuf); ImGui::TextColored(ic, "%s", ilvlBuf);
} }
// DPS comparison for weapons
if (isWeaponInvType(info.inventoryType) && isWeaponInvType(eq->item.inventoryType)) {
float newDps = 0.0f, eqDps = 0.0f;
if (info.damageMax > 0.0f && info.delayMs > 0)
newDps = ((info.damageMin + info.damageMax) * 0.5f) / (info.delayMs / 1000.0f);
if (eq->item.damageMax > 0.0f && eq->item.delayMs > 0)
eqDps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / (eq->item.delayMs / 1000.0f);
showDiff("DPS", newDps, eqDps);
}
showDiff("Armor", static_cast<float>(info.armor), static_cast<float>(eq->item.armor)); showDiff("Armor", static_cast<float>(info.armor), static_cast<float>(eq->item.armor));
showDiff("Str", static_cast<float>(info.strength), static_cast<float>(eq->item.strength)); showDiff("Str", static_cast<float>(info.strength), static_cast<float>(eq->item.strength));
showDiff("Agi", static_cast<float>(info.agility), static_cast<float>(eq->item.agility)); showDiff("Agi", static_cast<float>(info.agility), static_cast<float>(eq->item.agility));
@ -3509,8 +3541,50 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
showDiff("Int", static_cast<float>(info.intellect), static_cast<float>(eq->item.intellect)); showDiff("Int", static_cast<float>(info.intellect), static_cast<float>(eq->item.intellect));
showDiff("Spi", static_cast<float>(info.spirit), static_cast<float>(eq->item.spirit)); showDiff("Spi", static_cast<float>(info.spirit), static_cast<float>(eq->item.spirit));
// Hint text // Extra stats diff — union of stat types from both items
ImGui::TextDisabled("Hold Shift to compare"); auto findExtraStat = [](const auto& it, uint32_t type) -> int32_t {
for (const auto& es : it.extraStats)
if (es.statType == type) return es.statValue;
return 0;
};
std::vector<uint32_t> allTypes;
for (const auto& es : info.extraStats) allTypes.push_back(es.statType);
for (const auto& es : eq->item.extraStats) {
bool found = false;
for (uint32_t t : allTypes) if (t == es.statType) { found = true; break; }
if (!found) allTypes.push_back(es.statType);
}
for (uint32_t t : allTypes) {
int32_t nv = findExtraStat(info, t);
int32_t ev = findExtraStat(eq->item, t);
const char* lbl = nullptr;
switch (t) {
case 0: lbl = "Mana"; break;
case 1: lbl = "Health"; break;
case 12: lbl = "Defense"; break;
case 13: lbl = "Dodge"; break;
case 14: lbl = "Parry"; break;
case 15: lbl = "Block Rating"; break;
case 16: case 17: case 18: case 31: lbl = "Hit"; break;
case 19: case 20: case 21: case 32: lbl = "Crit"; break;
case 28: case 29: case 30: case 36: lbl = "Haste"; break;
case 35: lbl = "Resilience"; break;
case 37: lbl = "Expertise"; break;
case 38: lbl = "Attack Power"; break;
case 39: lbl = "Ranged AP"; break;
case 41: lbl = "Healing"; break;
case 42: lbl = "Spell Damage"; break;
case 43: lbl = "MP5"; break;
case 44: lbl = "Armor Pen"; break;
case 45: lbl = "Spell Power"; break;
case 46: lbl = "HP5"; break;
case 47: lbl = "Spell Pen"; break;
case 48: lbl = "Block Value"; break;
default: lbl = nullptr; break;
}
if (!lbl) continue;
showDiff(lbl, static_cast<float>(nv), static_cast<float>(ev));
}
} }
} else if (info.inventoryType > 0) { } else if (info.inventoryType > 0) {
ImGui::TextDisabled("Hold Shift to compare"); ImGui::TextDisabled("Hold Shift to compare");

View file

@ -1,7 +1,7 @@
#include "ui/keybinding_manager.hpp" #include "ui/keybinding_manager.hpp"
#include "core/logger.hpp"
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <iostream>
namespace wowee::ui { namespace wowee::ui {
@ -101,7 +101,7 @@ const char* KeybindingManager::getActionName(Action action) {
void KeybindingManager::loadFromConfigFile(const std::string& filePath) { void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
std::ifstream file(filePath); std::ifstream file(filePath);
if (!file.is_open()) { if (!file.is_open()) {
std::cerr << "[KeybindingManager] Failed to open config file: " << filePath << std::endl; LOG_ERROR("KeybindingManager: Failed to open config file: ", filePath);
return; return;
} }
@ -206,7 +206,7 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
} }
file.close(); file.close();
std::cout << "[KeybindingManager] Loaded keybindings from " << filePath << std::endl; LOG_INFO("KeybindingManager: Loaded keybindings from ", filePath);
} }
void KeybindingManager::saveToConfigFile(const std::string& filePath) const { void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
@ -301,9 +301,9 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
if (outFile.is_open()) { if (outFile.is_open()) {
outFile << content; outFile << content;
outFile.close(); outFile.close();
std::cout << "[KeybindingManager] Saved keybindings to " << filePath << std::endl; LOG_INFO("KeybindingManager: Saved keybindings to ", filePath);
} else { } else {
std::cerr << "[KeybindingManager] Failed to write config file: " << filePath << std::endl; LOG_ERROR("KeybindingManager: Failed to write config file: ", filePath);
} }
} }

View file

@ -593,15 +593,27 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
if (!dbc || !dbc->isLoaded()) return; if (!dbc || !dbc->isLoaded()) return;
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t fieldCount = dbc->getFieldCount();
// Detect DBC/layout mismatch: Classic layout expects ~173 fields but we may
// load the WotLK base DBC (234 fields). Use WotLK field indices in that case.
uint32_t idField = 0, iconField = 133, tooltipField = 139;
if (spellL) {
uint32_t layoutIcon = (*spellL)["IconID"];
if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) {
idField = (*spellL)["ID"];
iconField = layoutIcon;
try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {}
}
}
uint32_t count = dbc->getRecordCount(); uint32_t count = dbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) { for (uint32_t i = 0; i < count; ++i) {
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); uint32_t spellId = dbc->getUInt32(i, idField);
if (spellId == 0) continue; if (spellId == 0) continue;
uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133); uint32_t iconId = dbc->getUInt32(i, iconField);
spellIconIds[spellId] = iconId; spellIconIds[spellId] = iconId;
std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139); std::string tooltip = dbc->getString(i, tooltipField);
if (!tooltip.empty()) { if (!tooltip.empty()) {
spellTooltips[spellId] = tooltip; spellTooltips[spellId] = tooltip;
} }