mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
42 commits
ad511dad5e
...
5513c4aad5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5513c4aad5 | ||
|
|
39f4162ec1 | ||
|
|
8b9d626aec | ||
|
|
b23dbc9ab7 | ||
|
|
5031351736 | ||
|
|
ae40d393c3 | ||
|
|
f70df191a9 | ||
|
|
1daead3767 | ||
|
|
a43a43ed8e | ||
|
|
217edc81d9 | ||
|
|
6260ac281e | ||
|
|
29b5b6f959 | ||
|
|
4049f73ca6 | ||
|
|
bf5219c822 | ||
|
|
8169f5d5c0 | ||
|
|
119002626e | ||
|
|
6fbf5b5797 | ||
|
|
a0b978f95b | ||
|
|
8c3060f261 | ||
|
|
b80d88bded | ||
|
|
1c3f2f4ae3 | ||
|
|
67e6c9a984 | ||
|
|
9750110436 | ||
|
|
c017c61d2c | ||
|
|
ef5532cf15 | ||
|
|
e1be8667ed | ||
|
|
1b86f76d31 | ||
|
|
dc8619464a | ||
|
|
a7f7c4aa93 | ||
|
|
01685cc0bb | ||
|
|
2d53ff0c07 | ||
|
|
1152a70201 | ||
|
|
f5297f9945 | ||
|
|
9aed192503 | ||
|
|
7b03d5363b | ||
|
|
502d506a44 | ||
|
|
192c6175b8 | ||
|
|
cf3fe70f1f | ||
|
|
3667ff4998 | ||
|
|
203514abc7 | ||
|
|
e38324619e | ||
|
|
8378eb9232 |
28 changed files with 1804 additions and 510 deletions
|
|
@ -40,7 +40,7 @@ void main() {
|
|||
float cs = cos(push.rotation);
|
||||
float sn = sin(push.rotation);
|
||||
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);
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -786,6 +786,12 @@ public:
|
|||
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
|
||||
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)
|
||||
struct UnitCastState {
|
||||
bool casting = false;
|
||||
|
|
@ -970,6 +976,7 @@ public:
|
|||
const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; }
|
||||
const std::string& getSkillName(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)
|
||||
// Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect
|
||||
|
|
@ -2669,6 +2676,9 @@ private:
|
|||
bool castIsChannel = false;
|
||||
uint32_t currentCastSpellId = 0;
|
||||
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)
|
||||
std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
|
||||
uint64_t pendingGameObjectInteractGuid_ = 0;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ public:
|
|||
// Classic: none, TBC: u8, WotLK: u16.
|
||||
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 ---
|
||||
|
||||
/** Parse movement block from SMSG_UPDATE_OBJECT */
|
||||
|
|
@ -361,6 +365,20 @@ public:
|
|||
// TBC/Classic SMSG_QUESTGIVER_QUEST_DETAILS lacks informUnit(u64), flags(u32),
|
||||
// isFinished(u8) that WotLK added; uses variable item counts + emote section.
|
||||
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 {
|
||||
public:
|
||||
uint8_t movementFlags2Size() const override { return 0; }
|
||||
uint32_t wireOnTransportFlag() const override { return 0x02000000; }
|
||||
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ struct CombatLogEntry {
|
|||
int32_t amount = 0;
|
||||
uint32_t spellId = 0;
|
||||
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))
|
||||
std::string sourceName; // Resolved display name of attacker/caster
|
||||
std::string targetName; // Resolved display name of victim/target
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ private:
|
|||
|
||||
// Memory allocation tracking
|
||||
std::map<uint32_t, size_t> allocations_;
|
||||
std::map<uint32_t, size_t> freeBlocks_; // free-list keyed by base address
|
||||
uint32_t nextHeapAddr_;
|
||||
|
||||
// Hook handles for cleanup
|
||||
|
|
|
|||
|
|
@ -41,10 +41,12 @@ public:
|
|||
* @param expectedHash 20-byte expected HMAC-SHA1 digest
|
||||
* @param patternLen Length of the pattern to search for
|
||||
* @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
|
||||
*/
|
||||
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. */
|
||||
void writeLE32(uint32_t va, uint32_t value);
|
||||
|
|
|
|||
|
|
@ -2060,8 +2060,9 @@ public:
|
|||
/** SMSG_LOOT_RESPONSE parser */
|
||||
class LootResponseParser {
|
||||
public:
|
||||
// isWotlkFormat: true for WotLK 3.3.5a (22 bytes/item with randomSuffix+randomProp),
|
||||
// false for Classic 1.12 and TBC 2.4.3 (14 bytes/item).
|
||||
// isWotlkFormat: true for WotLK (has trailing quest item section),
|
||||
// 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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <deque>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -73,7 +74,10 @@ private:
|
|||
bool trsParsed = false;
|
||||
|
||||
// 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::deque<std::string> tileInsertionOrder; // hashes of successfully loaded tiles, oldest first
|
||||
std::unique_ptr<VkTexture> noDataTexture;
|
||||
|
||||
// Composite render target (3x3 tiles = 768x768)
|
||||
|
|
|
|||
|
|
@ -381,6 +381,13 @@ private:
|
|||
void initOverlayPipeline();
|
||||
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
|
||||
struct FSRState {
|
||||
bool enabled = false;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ public:
|
|||
void compositePass(VkCommandBuffer cmd);
|
||||
|
||||
/// 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 setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||
|
|
@ -71,7 +72,8 @@ private:
|
|||
float& top, float& bottom) const;
|
||||
void loadZoneTextures(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 zoomIn(const glm::vec3& playerRenderPos);
|
||||
void zoomOut();
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ private:
|
|||
bool pendingShadows = true;
|
||||
float pendingShadowDistance = 300.0f;
|
||||
bool pendingWaterRefraction = false;
|
||||
int pendingBrightness = 50; // 0-100, maps to 0.0-2.0 (50 = 1.0 default)
|
||||
int pendingMasterVolume = 100;
|
||||
int pendingMusicVolume = 30;
|
||||
int pendingAmbientVolume = 100;
|
||||
|
|
@ -192,6 +193,7 @@ private:
|
|||
bool pendingMinimapNpcDots = false;
|
||||
bool pendingShowLatencyMeter = true;
|
||||
bool pendingSeparateBags = true;
|
||||
bool pendingShowKeyring = true;
|
||||
bool pendingAutoLoot = false;
|
||||
|
||||
// Keybinding customization
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ public:
|
|||
bool isSeparateBags() const { return separateBags_; }
|
||||
void toggleCompactBags() { compactBags_ = !compactBags_; }
|
||||
bool isCompactBags() const { return compactBags_; }
|
||||
void setShowKeyring(bool show) { showKeyring_ = show; }
|
||||
bool isShowKeyring() const { return showKeyring_; }
|
||||
bool isBackpackOpen() const { return backpackOpen_; }
|
||||
bool isBagOpen(int idx) const { return idx >= 0 && idx < 4 ? bagOpen_[idx] : false; }
|
||||
|
||||
|
|
@ -79,6 +81,7 @@ private:
|
|||
bool bKeyWasDown = false;
|
||||
bool separateBags_ = true;
|
||||
bool compactBags_ = false;
|
||||
bool showKeyring_ = true;
|
||||
bool backpackOpen_ = false;
|
||||
std::array<bool, 4> bagOpen_{};
|
||||
bool cKeyWasDown = false;
|
||||
|
|
|
|||
|
|
@ -576,6 +576,12 @@ void Application::run() {
|
|||
void Application::shutdown() {
|
||||
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
|
||||
cancelWorldPreload();
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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 wowee
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#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) {
|
||||
auto pathIt = paths_.find(pathId);
|
||||
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;
|
||||
}
|
||||
|
||||
const auto& path = pathIt->second;
|
||||
if (path.points.empty()) {
|
||||
std::cerr << "TransportManager: Path " << pathId << " has no waypoints" << std::endl;
|
||||
LOG_ERROR("TransportManager: Path ", pathId, " has no waypoints");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +127,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
|
||||
void TransportManager::unregisterTransport(uint64_t guid) {
|
||||
transports_.erase(guid);
|
||||
std::cout << "TransportManager: Unregistered transport " << guid << std::endl;
|
||||
LOG_INFO("TransportManager: Unregistered transport ", 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) {
|
||||
if (waypoints.empty()) {
|
||||
std::cerr << "TransportManager: Cannot load empty path " << pathId << std::endl;
|
||||
LOG_ERROR("TransportManager: Cannot load empty path ", pathId);
|
||||
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) {
|
||||
auto* transport = getTransport(guid);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
#include "game/warden_emulator.hpp"
|
||||
#include <iostream>
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
#include <chrono>
|
||||
#include <iterator>
|
||||
|
||||
#ifdef HAVE_UNICORN
|
||||
// Unicorn Engine headers
|
||||
|
|
@ -43,17 +44,27 @@ WardenEmulator::~WardenEmulator() {
|
|||
|
||||
bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint32_t baseAddress) {
|
||||
if (uc_) {
|
||||
std::cerr << "[WardenEmulator] Already initialized" << '\n';
|
||||
LOG_ERROR("WardenEmulator: Already initialized");
|
||||
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
|
||||
uc_err err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc_);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -63,9 +74,12 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
// Detect overlap between module and heap/stack regions early.
|
||||
uint32_t modEnd = moduleBase_ + moduleSize_;
|
||||
if (modEnd > heapBase_ && moduleBase_ < heapBase_ + heapSize_) {
|
||||
std::cerr << "[WardenEmulator] Module [0x" << std::hex << moduleBase_
|
||||
<< ", 0x" << modEnd << ") overlaps heap [0x" << heapBase_
|
||||
<< ", 0x" << (heapBase_ + heapSize_) << ") — adjust HEAP_BASE\n" << std::dec;
|
||||
{
|
||||
char buf[256];
|
||||
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_ = nullptr;
|
||||
return false;
|
||||
|
|
@ -74,7 +88,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
// Map module memory (code + data)
|
||||
err = uc_mem_map(uc_, moduleBase_, moduleSize_, UC_PROT_ALL);
|
||||
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_ = nullptr;
|
||||
return false;
|
||||
|
|
@ -83,7 +97,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
// Write module code to emulated memory
|
||||
err = uc_mem_write(uc_, moduleBase_, moduleCode, moduleSize);
|
||||
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_ = nullptr;
|
||||
return false;
|
||||
|
|
@ -92,7 +106,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
// Map stack
|
||||
err = uc_mem_map(uc_, stackBase_, stackSize_, UC_PROT_READ | UC_PROT_WRITE);
|
||||
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_ = nullptr;
|
||||
return false;
|
||||
|
|
@ -106,7 +120,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
// Map heap
|
||||
err = uc_mem_map(uc_, heapBase_, heapSize_, UC_PROT_READ | UC_PROT_WRITE);
|
||||
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_ = nullptr;
|
||||
return false;
|
||||
|
|
@ -115,7 +129,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
|||
// Map API stub area
|
||||
err = uc_mem_map(uc_, apiStubBase_, 0x10000, UC_PROT_ALL);
|
||||
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_ = nullptr;
|
||||
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);
|
||||
if (err != UC_ERR_OK) {
|
||||
// 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
|
||||
|
|
@ -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);
|
||||
hooks_.push_back(hh);
|
||||
|
||||
std::cout << "[WardenEmulator] ✓ Emulator initialized successfully" << '\n';
|
||||
std::cout << "[WardenEmulator] Stack: 0x" << std::hex << stackBase_ << " - 0x" << (stackBase_ + stackSize_) << '\n';
|
||||
std::cout << "[WardenEmulator] Heap: 0x" << heapBase_ << " - 0x" << (heapBase_ + heapSize_) << std::dec << '\n';
|
||||
{
|
||||
char sBuf[128];
|
||||
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;
|
||||
}
|
||||
|
|
@ -153,8 +170,11 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName,
|
|||
// Store mapping
|
||||
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
|
||||
// For now, just return the address for IAT patching
|
||||
|
|
@ -163,7 +183,7 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName,
|
|||
}
|
||||
|
||||
void WardenEmulator::setupCommonAPIHooks() {
|
||||
std::cout << "[WardenEmulator] Setting up common Windows API hooks..." << '\n';
|
||||
LOG_INFO("WardenEmulator: Setting up common Windows API hooks...");
|
||||
|
||||
// kernel32.dll
|
||||
hookAPI("kernel32.dll", "VirtualAlloc", apiVirtualAlloc);
|
||||
|
|
@ -174,7 +194,7 @@ void WardenEmulator::setupCommonAPIHooks() {
|
|||
hookAPI("kernel32.dll", "GetCurrentProcessId", apiGetCurrentProcessId);
|
||||
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) {
|
||||
|
|
@ -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) {
|
||||
if (!uc_) {
|
||||
std::cerr << "[WardenEmulator] Not initialized" << '\n';
|
||||
LOG_ERROR("WardenEmulator: Not initialized");
|
||||
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
|
||||
uint32_t esp;
|
||||
|
|
@ -227,7 +250,7 @@ uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector<uint32
|
|||
// Execute until return address
|
||||
uc_err err = uc_emu_start(uc_, address, retAddr, 0, 0);
|
||||
if (err != UC_ERR_OK) {
|
||||
std::cerr << "[WardenEmulator] Execution failed: " << uc_strerror(err) << '\n';
|
||||
LOG_ERROR("WardenEmulator: Execution failed: ", uc_strerror(err));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +258,11 @@ uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector<uint32
|
|||
uint32_t 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
if (size == 0) return 0;
|
||||
|
||||
// Align to 4KB
|
||||
size = (size + 0xFFF) & ~0xFFF;
|
||||
const uint32_t allocSize = static_cast<uint32_t>(size);
|
||||
|
||||
if (nextHeapAddr_ + size > heapBase_ + heapSize_) {
|
||||
std::cerr << "[WardenEmulator] Heap exhausted" << '\n';
|
||||
// First-fit from free list so released blocks can be reused.
|
||||
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;
|
||||
}
|
||||
|
||||
uint32_t addr = nextHeapAddr_;
|
||||
nextHeapAddr_ += size;
|
||||
|
||||
nextHeapAddr_ += allocSize;
|
||||
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;
|
||||
}
|
||||
|
|
@ -283,12 +334,54 @@ uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t p
|
|||
bool WardenEmulator::freeMemory(uint32_t address) {
|
||||
auto it = allocations_.find(address);
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
@ -319,8 +412,12 @@ uint32_t WardenEmulator::apiVirtualAlloc(WardenEmulator& emu, const std::vector<
|
|||
uint32_t flAllocationType = args[2];
|
||||
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
|
||||
return emu.allocateMemory(dwSize, flProtect);
|
||||
|
|
@ -332,7 +429,11 @@ uint32_t WardenEmulator::apiVirtualFree(WardenEmulator& emu, const std::vector<u
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
uint32_t ticks = static_cast<uint32_t>(ms & 0xFFFFFFFF);
|
||||
|
||||
std::cout << "[WinAPI] GetTickCount() = " << ticks << '\n';
|
||||
LOG_DEBUG("WinAPI: GetTickCount() = ", ticks);
|
||||
return ticks;
|
||||
}
|
||||
|
||||
|
|
@ -350,18 +451,18 @@ uint32_t WardenEmulator::apiSleep([[maybe_unused]] WardenEmulator& emu, const st
|
|||
if (args.size() < 1) return 0;
|
||||
uint32_t dwMilliseconds = args[0];
|
||||
|
||||
std::cout << "[WinAPI] Sleep(" << dwMilliseconds << ")" << '\n';
|
||||
LOG_DEBUG("WinAPI: Sleep(", dwMilliseconds, ")");
|
||||
// Don't actually sleep in emulator
|
||||
return 0;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -375,8 +476,11 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
|
|||
uint32_t nSize = args[3];
|
||||
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
|
||||
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) {
|
||||
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) {
|
||||
|
|
@ -415,9 +519,12 @@ void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, ui
|
|||
case UC_MEM_FETCH_PROT: typeStr = "FETCH_PROT"; break;
|
||||
}
|
||||
|
||||
std::cerr << "[WardenEmulator] Invalid memory access: " << typeStr
|
||||
<< " at 0x" << std::hex << address << std::dec
|
||||
<< " (size=" << size << ")" << '\n';
|
||||
{
|
||||
char mBuf[128];
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "game/warden_memory.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
|
@ -406,10 +407,31 @@ void WardenMemory::patchRuntimeGlobals() {
|
|||
writeLE32(WORLD_ENABLES, enables);
|
||||
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;
|
||||
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);
|
||||
|
||||
// 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() {
|
||||
|
|
@ -837,7 +859,8 @@ void WardenMemory::verifyWardenScanEntries() {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
if (cacheIt != codePatternCache_.end()) {
|
||||
LOG_WARNING("WardenMemory: Code pattern cache HIT → ",
|
||||
cacheIt->second ? "found" : "not found");
|
||||
return cacheIt->second;
|
||||
}
|
||||
|
||||
// FIND_MEM_IMAGE_CODE_BY_HASH (imageOnly=true) searches ALL sections of
|
||||
// the PE image — not just executable ones. The original Warden module
|
||||
// walks every PE section when scanning the WoW.exe memory image.
|
||||
// FIND_CODE_BY_HASH (imageOnly=false) searches all process memory; since
|
||||
// we only have the PE image, both cases search the full image.
|
||||
// --- Fast path: check the hint offset directly (single HMAC) ---
|
||||
// The PAGE_A offset field is the RVA where the server expects the pattern.
|
||||
if (hintOffset > 0 && hintOffset + patternLen <= imageSize_) {
|
||||
uint8_t hmacOut[20];
|
||||
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; };
|
||||
std::vector<Range> ranges;
|
||||
|
||||
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(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24);
|
||||
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()) {
|
||||
// Fallback: search entire image
|
||||
if (patternLen <= 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;
|
||||
for (const auto& r : ranges) {
|
||||
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,
|
||||
hmacOut, &hmacLen);
|
||||
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,
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -909,8 +973,10 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
|
|||
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 ",
|
||||
ranges.size(), " section(s)");
|
||||
ranges.size(), " section(s), took ", elapsed, "s");
|
||||
codePatternCache_[cacheKey] = false;
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#include "game/warden_module.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <zlib.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/bn.h>
|
||||
|
|
@ -51,28 +51,30 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
|||
moduleData_ = moduleData;
|
||||
md5Hash_ = md5Hash;
|
||||
|
||||
std::cout << "[WardenModule] Loading module (MD5: ";
|
||||
for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) {
|
||||
printf("%02X", md5Hash[i]);
|
||||
{
|
||||
char hexBuf[17] = {};
|
||||
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
|
||||
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)
|
||||
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;
|
||||
}
|
||||
std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n';
|
||||
LOG_INFO("WardenModule: RC4 decrypted (", decryptedData_.size(), " bytes)");
|
||||
|
||||
// Step 3: Verify RSA signature
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -84,42 +86,42 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
|||
dataWithoutSig = decryptedData_;
|
||||
}
|
||||
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_;
|
||||
}
|
||||
|
||||
// Step 5: Parse custom executable format
|
||||
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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// Step 8: Initialize module
|
||||
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!
|
||||
// Note: Steps 6-8 are stubs/platform-limited, but infrastructure is ready
|
||||
loaded_ = true; // Mark as loaded (infrastructure complete)
|
||||
|
||||
std::cout << "[WardenModule] ✓ Module loading pipeline COMPLETE" << '\n';
|
||||
std::cout << "[WardenModule] Status: Infrastructure ready, execution stubs in place" << '\n';
|
||||
std::cout << "[WardenModule] Limitations:" << '\n';
|
||||
std::cout << "[WardenModule] - Relocations: needs real module data" << '\n';
|
||||
std::cout << "[WardenModule] - API Binding: Windows only (or Wine on Linux)" << '\n';
|
||||
std::cout << "[WardenModule] - Execution: disabled (unsafe without validation)" << '\n';
|
||||
std::cout << "[WardenModule] For strict servers: Would need to enable actual x86 execution" << '\n';
|
||||
LOG_INFO("WardenModule: Module loading pipeline COMPLETE");
|
||||
LOG_INFO("WardenModule: Status: Infrastructure ready, execution stubs in place");
|
||||
LOG_INFO("WardenModule: Limitations:");
|
||||
LOG_INFO("WardenModule: - Relocations: needs real module data");
|
||||
LOG_INFO("WardenModule: - API Binding: Windows only (or Wine on Linux)");
|
||||
LOG_INFO("WardenModule: - Execution: disabled (unsafe without validation)");
|
||||
LOG_INFO("WardenModule: For strict servers: Would need to enable actual x86 execution");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -127,25 +129,25 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
|||
bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
||||
[[maybe_unused]] std::vector<uint8_t>& responseOut) {
|
||||
if (!loaded_) {
|
||||
std::cerr << "[WardenModule] Module not loaded, cannot process checks" << '\n';
|
||||
LOG_ERROR("WardenModule: Module not loaded, cannot process checks");
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_UNICORN
|
||||
if (emulator_ && emulator_->isInitialized() && funcList_.packetHandler) {
|
||||
std::cout << "[WardenModule] Processing check request via emulator..." << '\n';
|
||||
std::cout << "[WardenModule] Check data: " << checkData.size() << " bytes" << '\n';
|
||||
LOG_INFO("WardenModule: Processing check request via emulator...");
|
||||
LOG_INFO("WardenModule: Check data: ", checkData.size(), " bytes");
|
||||
|
||||
// Allocate memory for check data in emulated space
|
||||
uint32_t checkDataAddr = emulator_->allocateMemory(checkData.size(), 0x04);
|
||||
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;
|
||||
}
|
||||
|
||||
// Write check data to emulated memory
|
||||
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);
|
||||
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)
|
||||
uint32_t responseAddr = emulator_->allocateMemory(1024, 0x04);
|
||||
if (responseAddr == 0) {
|
||||
std::cerr << "[WardenModule] Failed to allocate response buffer" << '\n';
|
||||
LOG_ERROR("WardenModule: Failed to allocate response buffer");
|
||||
emulator_->freeMemory(checkDataAddr);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -162,13 +164,13 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
|||
// Call module's PacketHandler
|
||||
// void PacketHandler(uint8_t* checkData, size_t checkSize,
|
||||
// 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
|
||||
// the module's exact function signature
|
||||
std::cout << "[WardenModule] ⚠ PacketHandler execution stubbed" << '\n';
|
||||
std::cout << "[WardenModule] Would call emulated function to process checks" << '\n';
|
||||
std::cout << "[WardenModule] This would generate REAL responses (not fakes!)" << '\n';
|
||||
LOG_WARNING("WardenModule: PacketHandler execution stubbed");
|
||||
LOG_INFO("WardenModule: Would call emulated function to process checks");
|
||||
LOG_INFO("WardenModule: This would generate REAL responses (not fakes!)");
|
||||
|
||||
// Clean up
|
||||
emulator_->freeMemory(checkDataAddr);
|
||||
|
|
@ -179,7 +181,7 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
|||
return false;
|
||||
|
||||
} 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(responseAddr);
|
||||
return false;
|
||||
|
|
@ -187,8 +189,8 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
|||
}
|
||||
#endif
|
||||
|
||||
std::cout << "[WardenModule] ⚠ processCheckRequest NOT IMPLEMENTED" << '\n';
|
||||
std::cout << "[WardenModule] Would call module->PacketHandler() here" << '\n';
|
||||
LOG_WARNING("WardenModule: processCheckRequest NOT IMPLEMENTED");
|
||||
LOG_INFO("WardenModule: Would call module->PacketHandler() here");
|
||||
|
||||
// For now, return false to fall back to fake responses in GameHandler
|
||||
return false;
|
||||
|
|
@ -219,13 +221,13 @@ void WardenModule::unload() {
|
|||
if (moduleMemory_) {
|
||||
// Call module's Unload() function if loaded
|
||||
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
|
||||
// funcList_.unload(nullptr);
|
||||
}
|
||||
|
||||
// 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
|
||||
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
|
||||
#else
|
||||
|
|
@ -264,7 +266,7 @@ bool WardenModule::decryptRC4(const std::vector<uint8_t>& encrypted,
|
|||
const std::vector<uint8_t>& key,
|
||||
std::vector<uint8_t>& decryptedOut) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +301,7 @@ bool WardenModule::decryptRC4(const std::vector<uint8_t>& encrypted,
|
|||
bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
|
||||
// RSA-2048 signature is last 256 bytes
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +387,7 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
|
|||
if (pkey) EVP_PKEY_free(pkey);
|
||||
|
||||
if (decryptedLen < 0) {
|
||||
std::cerr << "[WardenModule] RSA public decrypt failed" << '\n';
|
||||
LOG_ERROR("WardenModule: RSA public decrypt failed");
|
||||
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());
|
||||
|
||||
if (std::memcmp(actualHash.data(), expectedHash.data(), 20) == 0) {
|
||||
std::cout << "[WardenModule] ✓ RSA signature verified" << '\n';
|
||||
LOG_INFO("WardenModule: RSA signature verified");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
std::cerr << "[WardenModule] RSA signature verification FAILED (hash mismatch)" << '\n';
|
||||
std::cerr << "[WardenModule] NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification" << '\n';
|
||||
LOG_ERROR("WardenModule: RSA signature verification FAILED (hash mismatch)");
|
||||
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)
|
||||
// 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
|
||||
}
|
||||
|
||||
bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
|
||||
std::vector<uint8_t>& decompressedOut) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -426,11 +428,11 @@ bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
|
|||
(compressed[2] << 16) |
|
||||
(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)
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -447,7 +449,7 @@ bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
|
|||
// Initialize inflater
|
||||
int ret = inflateInit(&stream);
|
||||
if (ret != Z_OK) {
|
||||
std::cerr << "[WardenModule] inflateInit failed: " << ret << '\n';
|
||||
LOG_ERROR("WardenModule: inflateInit failed: ", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -458,19 +460,18 @@ bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
|
|||
inflateEnd(&stream);
|
||||
|
||||
if (ret != Z_STREAM_END) {
|
||||
std::cerr << "[WardenModule] inflate failed: " << ret << '\n';
|
||||
LOG_ERROR("WardenModule: inflate failed: ", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "[WardenModule] ✓ zlib decompression successful ("
|
||||
<< stream.total_out << " bytes decompressed)" << '\n';
|
||||
LOG_INFO("WardenModule: zlib decompression successful (", stream.total_out, " bytes decompressed)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -481,11 +482,11 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
|||
(exeData[2] << 16) |
|
||||
(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)
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -500,7 +501,7 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
|||
PAGE_EXECUTE_READWRITE
|
||||
);
|
||||
if (!moduleMemory_) {
|
||||
std::cerr << "[WardenModule] VirtualAlloc failed" << '\n';
|
||||
LOG_ERROR("WardenModule: VirtualAlloc failed");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
|
|
@ -513,7 +514,7 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
|||
0
|
||||
);
|
||||
if (moduleMemory_ == MAP_FAILED) {
|
||||
std::cerr << "[WardenModule] mmap failed: " << strerror(errno) << '\n';
|
||||
LOG_ERROR("WardenModule: mmap failed: ", strerror(errno));
|
||||
moduleMemory_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -522,8 +523,7 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
|||
moduleSize_ = finalCodeSize;
|
||||
std::memset(moduleMemory_, 0, moduleSize_); // Zero-initialize
|
||||
|
||||
std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at "
|
||||
<< moduleMemory_ << '\n';
|
||||
LOG_INFO("WardenModule: Allocated ", moduleSize_, " bytes of executable memory");
|
||||
|
||||
auto readU16LE = [&](size_t at) -> uint16_t {
|
||||
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::CopySkipData) formatName = "copy/skip/data";
|
||||
|
||||
std::cout << "[WardenModule] Parsed " << parsedPairCount << " pairs using format "
|
||||
<< formatName << ", final offset: " << parsedFinalOffset << "/" << finalCodeSize << '\n';
|
||||
std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_
|
||||
<< " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n';
|
||||
LOG_INFO("WardenModule: Parsed ", parsedPairCount, " pairs using format ",
|
||||
formatName, ", final offset: ", parsedFinalOffset, "/", finalCodeSize);
|
||||
LOG_INFO("WardenModule: Relocation data starts at decompressed offset ", relocDataOffset_,
|
||||
" (", (exeData.size() - relocDataOffset_), " bytes remaining)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -683,13 +683,13 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
|||
std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
bool WardenModule::applyRelocations() {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -698,7 +698,7 @@ bool WardenModule::applyRelocations() {
|
|||
// Each offset in the module image has moduleBase_ added to the 32-bit value there
|
||||
|
||||
if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) {
|
||||
std::cout << "[WardenModule] No relocation data available" << '\n';
|
||||
LOG_INFO("WardenModule: No relocation data available");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -722,24 +722,27 @@ bool WardenModule::applyRelocations() {
|
|||
std::memcpy(addr, &val, sizeof(uint32_t));
|
||||
relocCount++;
|
||||
} else {
|
||||
std::cerr << "[WardenModule] Relocation offset " << currentOffset
|
||||
<< " out of bounds (moduleSize=" << moduleSize_ << ")" << '\n';
|
||||
LOG_ERROR("WardenModule: Relocation offset ", currentOffset,
|
||||
" 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;
|
||||
}
|
||||
|
||||
bool WardenModule::bindAPIs() {
|
||||
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;
|
||||
}
|
||||
|
||||
std::cout << "[WardenModule] Binding Windows APIs for module..." << '\n';
|
||||
LOG_INFO("WardenModule: Binding Windows APIs for module...");
|
||||
|
||||
// Common Windows APIs used by Warden modules:
|
||||
//
|
||||
|
|
@ -759,14 +762,14 @@ bool WardenModule::bindAPIs() {
|
|||
|
||||
#ifdef _WIN32
|
||||
// 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 user32 = GetModuleHandleA("user32.dll");
|
||||
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
|
||||
|
||||
if (!kernel32 || !user32 || !ntdll) {
|
||||
std::cerr << "[WardenModule] Failed to get module handles" << '\n';
|
||||
LOG_ERROR("WardenModule: Failed to get module handles");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -777,8 +780,8 @@ bool WardenModule::bindAPIs() {
|
|||
// - Resolve address using GetProcAddress
|
||||
// - Write address to Import Address Table (IAT)
|
||||
|
||||
std::cout << "[WardenModule] ⚠ Windows API binding is STUB (needs PE import table parsing)" << '\n';
|
||||
std::cout << "[WardenModule] Would parse PE headers and patch IAT with resolved addresses" << '\n';
|
||||
LOG_WARNING("WardenModule: Windows API binding is STUB (needs PE import table parsing)");
|
||||
LOG_INFO("WardenModule: Would parse PE headers and patch IAT with resolved addresses");
|
||||
|
||||
#else
|
||||
// On Linux: Cannot directly execute Windows code
|
||||
|
|
@ -787,15 +790,15 @@ bool WardenModule::bindAPIs() {
|
|||
// 2. Implement Windows API stubs (limited functionality)
|
||||
// 3. Use binfmt_misc + Wine (transparent Windows executable support)
|
||||
|
||||
std::cout << "[WardenModule] Platform: Linux - Windows module execution NOT supported" << '\n';
|
||||
std::cout << "[WardenModule] Options:" << '\n';
|
||||
std::cout << "[WardenModule] 1. Run wowee under Wine (provides Windows API layer)" << '\n';
|
||||
std::cout << "[WardenModule] 2. Use a Windows VM" << '\n';
|
||||
std::cout << "[WardenModule] 3. Implement Windows API stubs (limited, complex)" << '\n';
|
||||
LOG_WARNING("WardenModule: Platform: Linux - Windows module execution NOT supported");
|
||||
LOG_INFO("WardenModule: Options:");
|
||||
LOG_INFO("WardenModule: 1. Run wowee under Wine (provides Windows API layer)");
|
||||
LOG_INFO("WardenModule: 2. Use a Windows VM");
|
||||
LOG_INFO("WardenModule: 3. Implement Windows API stubs (limited, complex)");
|
||||
|
||||
// For now, we'll return true to continue the loading pipeline
|
||||
// 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
|
||||
|
||||
return true; // Return true to continue (stub implementation)
|
||||
|
|
@ -803,11 +806,11 @@ bool WardenModule::bindAPIs() {
|
|||
|
||||
bool WardenModule::initializeModule() {
|
||||
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;
|
||||
}
|
||||
|
||||
std::cout << "[WardenModule] Initializing Warden module..." << '\n';
|
||||
LOG_INFO("WardenModule: Initializing Warden module...");
|
||||
|
||||
// Module initialization protocol:
|
||||
//
|
||||
|
|
@ -844,27 +847,27 @@ bool WardenModule::initializeModule() {
|
|||
|
||||
// Stub callbacks (would need real implementations)
|
||||
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
|
||||
};
|
||||
|
||||
callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) {
|
||||
std::cout << "[WardenModule Callback] validateModule()" << '\n';
|
||||
LOG_DEBUG("WardenModule Callback: validateModule()");
|
||||
// TODO: Validate module hash
|
||||
};
|
||||
|
||||
callbacks.allocMemory = [](size_t size) -> void* {
|
||||
std::cout << "[WardenModule Callback] allocMemory(" << size << ")" << '\n';
|
||||
LOG_DEBUG("WardenModule Callback: allocMemory(", size, ")");
|
||||
return malloc(size);
|
||||
};
|
||||
|
||||
callbacks.freeMemory = [](void* ptr) {
|
||||
std::cout << "[WardenModule Callback] freeMemory()" << '\n';
|
||||
LOG_DEBUG("WardenModule Callback: freeMemory()");
|
||||
free(ptr);
|
||||
};
|
||||
|
||||
callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) {
|
||||
std::cout << "[WardenModule Callback] generateRC4()" << '\n';
|
||||
LOG_DEBUG("WardenModule Callback: generateRC4()");
|
||||
// TODO: Re-key RC4 cipher
|
||||
};
|
||||
|
||||
|
|
@ -873,7 +876,7 @@ bool WardenModule::initializeModule() {
|
|||
};
|
||||
|
||||
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)
|
||||
|
|
@ -881,24 +884,28 @@ bool WardenModule::initializeModule() {
|
|||
|
||||
#ifdef HAVE_UNICORN
|
||||
// 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>();
|
||||
if (!emulator_->initialize(moduleMemory_, moduleSize_, moduleBase_)) {
|
||||
std::cerr << "[WardenModule] Failed to initialize emulator" << '\n';
|
||||
LOG_ERROR("WardenModule: Failed to initialize emulator");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup Windows API hooks
|
||||
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
|
||||
uint32_t callbackStructAddr = emulator_->allocateMemory(sizeof(ClientCallbacks), 0x04);
|
||||
if (callbackStructAddr == 0) {
|
||||
std::cerr << "[WardenModule] Failed to allocate memory for callbacks" << '\n';
|
||||
LOG_ERROR("WardenModule: Failed to allocate memory for callbacks");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -921,13 +928,21 @@ bool WardenModule::initializeModule() {
|
|||
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
|
||||
// Entry point is typically at module base (offset 0)
|
||||
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 {
|
||||
// Call: WardenFuncList* InitModule(ClientCallbacks* callbacks)
|
||||
|
|
@ -935,21 +950,28 @@ bool WardenModule::initializeModule() {
|
|||
uint32_t result = emulator_->callFunction(entryPoint, args);
|
||||
|
||||
if (result == 0) {
|
||||
std::cerr << "[WardenModule] Module entry returned NULL" << '\n';
|
||||
LOG_ERROR("WardenModule: Module entry returned NULL");
|
||||
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
|
||||
// Structure has 4 function pointers (16 bytes)
|
||||
uint32_t funcAddrs[4] = {};
|
||||
if (emulator_->readMemory(result, funcAddrs, 16)) {
|
||||
std::cout << "[WardenModule] Module exported functions:" << '\n';
|
||||
std::cout << "[WardenModule] generateRC4Keys: 0x" << std::hex << funcAddrs[0] << std::dec << '\n';
|
||||
std::cout << "[WardenModule] unload: 0x" << std::hex << funcAddrs[1] << std::dec << '\n';
|
||||
std::cout << "[WardenModule] packetHandler: 0x" << std::hex << funcAddrs[2] << std::dec << '\n';
|
||||
std::cout << "[WardenModule] tick: 0x" << std::hex << funcAddrs[3] << std::dec << '\n';
|
||||
char fb[4][32];
|
||||
for (int fi = 0; fi < 4; ++fi)
|
||||
std::snprintf(fb[fi], sizeof(fb[fi]), "0x%X", funcAddrs[fi]);
|
||||
LOG_INFO("WardenModule: Module exported functions:");
|
||||
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
|
||||
// funcList_.generateRC4Keys = ... (would wrap emulator calls)
|
||||
|
|
@ -958,10 +980,10 @@ bool WardenModule::initializeModule() {
|
|||
// funcList_.tick = ...
|
||||
}
|
||||
|
||||
std::cout << "[WardenModule] ✓ Module fully initialized and ready!" << '\n';
|
||||
LOG_INFO("WardenModule: Module fully initialized and ready!");
|
||||
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
|
@ -970,14 +992,14 @@ bool WardenModule::initializeModule() {
|
|||
typedef void* (*ModuleEntryPoint)(ClientCallbacks*);
|
||||
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
|
||||
// Extremely dangerous without proper validation!
|
||||
// void* result = entryPoint(&callbacks);
|
||||
|
||||
std::cout << "[WardenModule] ⚠ Module entry point call is DISABLED (unsafe without validation)" << '\n';
|
||||
std::cout << "[WardenModule] Would execute x86 code at " << moduleMemory_ << '\n';
|
||||
LOG_WARNING("WardenModule: Module entry point call is DISABLED (unsafe without validation)");
|
||||
LOG_INFO("WardenModule: Would execute x86 code at ", moduleMemory_);
|
||||
|
||||
// TODO: Extract WardenFuncList from result
|
||||
// funcList_.packetHandler = ...
|
||||
|
|
@ -986,9 +1008,9 @@ bool WardenModule::initializeModule() {
|
|||
// funcList_.unload = ...
|
||||
|
||||
#else
|
||||
std::cout << "[WardenModule] ⚠ Cannot execute Windows x86 code on Linux" << '\n';
|
||||
std::cout << "[WardenModule] Module entry point: " << moduleMemory_ << '\n';
|
||||
std::cout << "[WardenModule] Would call entry point with ClientCallbacks struct" << '\n';
|
||||
LOG_WARNING("WardenModule: Cannot execute Windows x86 code on Linux");
|
||||
LOG_INFO("WardenModule: Module entry point: ", moduleMemory_);
|
||||
LOG_INFO("WardenModule: Would call entry point with ClientCallbacks struct");
|
||||
#endif
|
||||
|
||||
// For now, return true to mark module as "loaded" at infrastructure level
|
||||
|
|
@ -998,7 +1020,7 @@ bool WardenModule::initializeModule() {
|
|||
// 3. Exception handling for crashes
|
||||
// 4. Sandboxing for security
|
||||
|
||||
std::cout << "[WardenModule] ⚠ Module initialization is STUB" << '\n';
|
||||
LOG_WARNING("WardenModule: Module initialization is STUB");
|
||||
return true; // Stub implementation
|
||||
}
|
||||
|
||||
|
|
@ -1023,7 +1045,7 @@ WardenModuleManager::WardenModuleManager() {
|
|||
// Create cache directory if it doesn't exist
|
||||
std::filesystem::create_directories(cacheDirectory_);
|
||||
|
||||
std::cout << "[WardenModuleManager] Cache directory: " << cacheDirectory_ << '\n';
|
||||
LOG_INFO("WardenModuleManager: Cache directory: ", cacheDirectory_);
|
||||
}
|
||||
|
||||
WardenModuleManager::~WardenModuleManager() {
|
||||
|
|
@ -1060,12 +1082,11 @@ bool WardenModuleManager::receiveModuleChunk(const std::vector<uint8_t>& md5Hash
|
|||
std::vector<uint8_t>& buffer = downloadBuffer_[md5Hash];
|
||||
buffer.insert(buffer.end(), chunkData.begin(), chunkData.end());
|
||||
|
||||
std::cout << "[WardenModuleManager] Received chunk (" << chunkData.size()
|
||||
<< " bytes, total: " << buffer.size() << ")" << '\n';
|
||||
LOG_INFO("WardenModuleManager: Received chunk (", chunkData.size(),
|
||||
" bytes, total: ", buffer.size(), ")");
|
||||
|
||||
if (isComplete) {
|
||||
std::cout << "[WardenModuleManager] Module download complete ("
|
||||
<< buffer.size() << " bytes)" << '\n';
|
||||
LOG_INFO("WardenModuleManager: Module download complete (", buffer.size(), " bytes)");
|
||||
|
||||
// Cache to disk
|
||||
cacheModule(md5Hash, buffer);
|
||||
|
|
@ -1085,14 +1106,14 @@ bool WardenModuleManager::cacheModule(const std::vector<uint8_t>& md5Hash,
|
|||
|
||||
std::ofstream file(cachePath, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "[WardenModuleManager] Failed to write cache: " << cachePath << '\n';
|
||||
LOG_ERROR("WardenModuleManager: Failed to write cache: ", cachePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(reinterpret_cast<const char*>(moduleData.data()), moduleData.size());
|
||||
file.close();
|
||||
|
||||
std::cout << "[WardenModuleManager] Cached module to: " << cachePath << '\n';
|
||||
LOG_INFO("WardenModuleManager: Cached module to: ", cachePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1116,7 +1137,7 @@ bool WardenModuleManager::loadCachedModule(const std::vector<uint8_t>& md5Hash,
|
|||
file.read(reinterpret_cast<char*>(moduleDataOut.data()), fileSize);
|
||||
file.close();
|
||||
|
||||
std::cout << "[WardenModuleManager] Loaded cached module (" << fileSize << " bytes)" << '\n';
|
||||
LOG_INFO("WardenModuleManager: Loaded cached module (", fileSize, " bytes)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4257,10 +4257,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
|
|||
data.gold = packet.readUInt32();
|
||||
uint8_t itemCount = packet.readUInt8();
|
||||
|
||||
// Item wire size:
|
||||
// WotLK 3.3.5a: 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
|
||||
const size_t kItemSize = isWotlkFormat ? 22u : 14u;
|
||||
// Per-item wire size is 22 bytes across all expansions:
|
||||
// slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22
|
||||
constexpr size_t kItemSize = 22u;
|
||||
|
||||
auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool {
|
||||
for (uint8_t i = 0; i < listCount; ++i) {
|
||||
|
|
@ -4270,21 +4269,14 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
|
|||
}
|
||||
|
||||
LootItem item;
|
||||
item.slotIndex = packet.readUInt8();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
|
||||
if (isWotlkFormat) {
|
||||
item.randomSuffix = packet.readUInt32();
|
||||
item.randomPropertyId = packet.readUInt32();
|
||||
} else {
|
||||
item.randomSuffix = 0;
|
||||
item.randomPropertyId = 0;
|
||||
}
|
||||
|
||||
item.lootSlotType = packet.readUInt8();
|
||||
item.isQuestItem = markQuestItems;
|
||||
item.slotIndex = packet.readUInt8();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
item.randomSuffix = packet.readUInt32();
|
||||
item.randomPropertyId = packet.readUInt32();
|
||||
item.lootSlotType = packet.readUInt8();
|
||||
item.isQuestItem = markQuestItems;
|
||||
data.items.push_back(item);
|
||||
}
|
||||
return true;
|
||||
|
|
@ -4296,8 +4288,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
|
|||
return false;
|
||||
}
|
||||
|
||||
// Quest item section only present in WotLK 3.3.5a
|
||||
uint8_t questItemCount = 0;
|
||||
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||
if (isWotlkFormat && packet.getSize() - packet.getReadPos() >= 1) {
|
||||
questItemCount = packet.readUInt8();
|
||||
data.items.reserve(data.items.size() + questItemCount);
|
||||
if (!parseLootItemList(questItemCount, true)) {
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ void Minimap::shutdown() {
|
|||
if (tex) tex->destroy(device, alloc);
|
||||
}
|
||||
tileTextureCache.clear();
|
||||
tileInsertionOrder.clear();
|
||||
|
||||
if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); }
|
||||
if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); }
|
||||
|
|
@ -362,6 +363,15 @@ VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) {
|
|||
|
||||
VkTexture* ptr = tex.get();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -513,14 +523,15 @@ void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera,
|
|||
|
||||
float arrowRotation = 0.0f;
|
||||
if (!rotateWithCamera) {
|
||||
// Prefer authoritative orientation if provided. This value is expected
|
||||
// to already match minimap shader rotation convention.
|
||||
if (hasPlayerOrientation) {
|
||||
arrowRotation = playerOrientation;
|
||||
} else {
|
||||
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{};
|
||||
|
|
|
|||
|
|
@ -5287,6 +5287,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
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) {
|
||||
glm::vec3 minimapCenter = camera->getPosition();
|
||||
if (cameraController && cameraController->isThirdPerson())
|
||||
|
|
@ -5421,6 +5432,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
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) {
|
||||
glm::vec3 minimapCenter = camera->getPosition();
|
||||
if (cameraController && cameraController->isThirdPerson())
|
||||
|
|
|
|||
|
|
@ -835,7 +835,8 @@ void WorldMap::zoomOut() {
|
|||
// 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;
|
||||
|
||||
auto& input = core::Input::getInstance();
|
||||
|
|
@ -886,14 +887,14 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr
|
|||
}
|
||||
|
||||
if (!open) return;
|
||||
renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight);
|
||||
renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight, playerYawDeg);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 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 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) {
|
||||
float px = imgMin.x + playerUV.x * displayW;
|
||||
float py = imgMin.y + playerUV.y * displayH;
|
||||
drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255));
|
||||
drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f);
|
||||
// Directional arrow: render-space (cos,sin) maps to screen (-dx,-dy)
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2091,18 +2091,20 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
std::string cmd = buf.substr(1, sp - 1);
|
||||
for (char& c : cmd) c = std::tolower(c);
|
||||
int detected = -1;
|
||||
bool isReply = false;
|
||||
if (cmd == "s" || cmd == "say") detected = 0;
|
||||
else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1;
|
||||
else if (cmd == "p" || cmd == "party") detected = 2;
|
||||
else if (cmd == "g" || cmd == "guild") detected = 3;
|
||||
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 == "o" || cmd == "officer" || cmd == "osay") detected = 6;
|
||||
else if (cmd == "bg" || cmd == "battleground") detected = 7;
|
||||
else if (cmd == "rw" || cmd == "raidwarning") detected = 8;
|
||||
else if (cmd == "i" || cmd == "instance") detected = 9;
|
||||
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
|
||||
if (detected == 10) {
|
||||
int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc.
|
||||
|
|
@ -2114,8 +2116,16 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
selectedChatType = detected;
|
||||
// Strip the prefix, keep only the message part
|
||||
std::string remaining = buf.substr(sp + 1);
|
||||
// For whisper, first word after /w is the target
|
||||
if (detected == 4) {
|
||||
// /r reply: pre-fill whisper target from last whisper sender
|
||||
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(' ');
|
||||
if (msgStart != std::string::npos) {
|
||||
std::string wTarget = remaining.substr(0, msgStart);
|
||||
|
|
@ -2576,6 +2586,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
uint64_t closestHostileUnitGuid = 0;
|
||||
float closestQuestGoT = 1e30f;
|
||||
uint64_t closestQuestGoGuid = 0;
|
||||
float closestGoT = 1e30f;
|
||||
uint64_t closestGoGuid = 0;
|
||||
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
|
|
@ -2598,16 +2610,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
heightOffset = 0.3f;
|
||||
}
|
||||
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||
// For GOs with no renderer instance yet, use a tight fallback
|
||||
// sphere so invisible/unloaded doodads aren't accidentally clicked.
|
||||
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;
|
||||
}
|
||||
hitRadius = 2.5f;
|
||||
heightOffset = 1.2f;
|
||||
}
|
||||
hitCenter = core::coords::canonicalToRender(
|
||||
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
|
|
@ -2626,12 +2630,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
closestHostileUnitGuid = guid;
|
||||
}
|
||||
}
|
||||
if (t == game::ObjectType::GAMEOBJECT && !questObjectiveGoEntries.empty()) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (questObjectiveGoEntries.count(go->getEntry())) {
|
||||
if (hitT < closestQuestGoT) {
|
||||
closestQuestGoT = hitT;
|
||||
closestQuestGoGuid = guid;
|
||||
if (t == game::ObjectType::GAMEOBJECT) {
|
||||
if (hitT < closestGoT) {
|
||||
closestGoT = hitT;
|
||||
closestGoGuid = 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) {
|
||||
closestGuid = closestQuestGoGuid;
|
||||
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) {
|
||||
// Prefer hostile monsters over nearby gameobjects/others when right-click picking.
|
||||
closestGuid = closestHostileUnitGuid;
|
||||
closestType = game::ObjectType::UNIT;
|
||||
}
|
||||
|
|
@ -3536,17 +3557,22 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
// WoW level-based color for hostile mobs
|
||||
uint32_t playerLv = gameHandler.getPlayerLevel();
|
||||
uint32_t mobLv = u->getLevel();
|
||||
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
|
||||
if (mobLv == 0) {
|
||||
// Level 0 = unknown/?? (e.g. high-level raid bosses) — always skull red
|
||||
hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
|
||||
} 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 {
|
||||
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) {
|
||||
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
|
||||
if (target->getType() == game::ObjectType::UNIT) {
|
||||
int rank = gameHandler.getCreatureRank(unit->getEntry());
|
||||
|
|
@ -4315,17 +4344,21 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
|
|||
} else if (u->isHostile()) {
|
||||
uint32_t playerLv = gameHandler.getPlayerLevel();
|
||||
uint32_t mobLv = u->getLevel();
|
||||
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
||||
if (game::GameHandler::killXp(playerLv, mobLv) == 0)
|
||||
focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f);
|
||||
else if (diff >= 10)
|
||||
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
|
||||
else if (diff >= 5)
|
||||
focusColor = ImVec4(1.0f, 0.5f, 0.1f, 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);
|
||||
if (mobLv == 0) {
|
||||
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // ?? level = skull red
|
||||
} else {
|
||||
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
||||
if (game::GameHandler::killXp(playerLv, mobLv) == 0)
|
||||
focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f);
|
||||
else if (diff >= 10)
|
||||
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
|
||||
else if (diff >= 5)
|
||||
focusColor = ImVec4(1.0f, 0.5f, 0.1f, 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 {
|
||||
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
|
||||
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 maxHp = unit->getMaxHealth();
|
||||
|
|
@ -5951,6 +5987,28 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
message = "";
|
||||
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
|
||||
|
|
@ -6496,10 +6554,11 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
glm::vec3 playerPos = renderer->getCharacterPosition();
|
||||
float playerYaw = renderer->getCharacterYaw();
|
||||
auto* window = app.getWindow();
|
||||
int screenW = window ? window->getWidth() : 1280;
|
||||
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).
|
||||
if (!wm->isOpen()) showWorldMap_ = false;
|
||||
|
|
@ -6551,17 +6610,23 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage
|
|||
}
|
||||
};
|
||||
|
||||
// Always use expansion-aware layout if available
|
||||
// Field indices vary by expansion: Classic=117, TBC=124, WotLK=133
|
||||
// Use expansion-aware layout if available AND the DBC field count
|
||||
// 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) {
|
||||
tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]);
|
||||
}
|
||||
|
||||
// Fallback if expansion layout missing or yielded nothing
|
||||
// Only use WotLK field 133 as last resort if we have no layout
|
||||
if (spellIconIds_.empty() && !spellL && fieldCount > 133) {
|
||||
tryLoadIcons(0, 133);
|
||||
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).
|
||||
if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) {
|
||||
iconField = layoutIcon;
|
||||
idField = (*spellL)["ID"];
|
||||
}
|
||||
}
|
||||
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.
|
||||
// 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;
|
||||
if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0
|
||||
&& !onCooldown && gameHandler.hasTarget()) {
|
||||
|
|
@ -6753,6 +6819,33 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|||
const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0
|
||||
&& 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;
|
||||
if (iconTex) {
|
||||
ImVec4 tintColor(1, 1, 1, 1);
|
||||
|
|
@ -7645,7 +7738,8 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
|
|||
uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0;
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience");
|
||||
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);
|
||||
if (restedXp > 0) {
|
||||
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
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
|
||||
|
||||
char overlay[64];
|
||||
char overlay[96];
|
||||
if (currentSpellId == 0) {
|
||||
snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining());
|
||||
} else {
|
||||
const std::string& spellName = gameHandler.getSpellName(currentSpellId);
|
||||
const char* verb = channeling ? "Channeling" : "Casting";
|
||||
if (!spellName.empty())
|
||||
snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining());
|
||||
else
|
||||
int queueLeft = gameHandler.getCraftQueueRemaining();
|
||||
if (!spellName.empty()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
if (iconTex) {
|
||||
|
|
@ -8413,10 +8512,21 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|||
snprintf(text, sizeof(text), "+%d", entry.amount);
|
||||
color = ImVec4(0.4f, 1.0f, 0.5f, alpha);
|
||||
break;
|
||||
case game::CombatTextEntry::ENVIRONMENTAL:
|
||||
snprintf(text, sizeof(text), "-%d", entry.amount);
|
||||
case game::CombatTextEntry::ENVIRONMENTAL: {
|
||||
const char* envLabel = "";
|
||||
switch (entry.powerType) {
|
||||
case 0: envLabel = "Fatigue "; break;
|
||||
case 1: envLabel = "Drowning "; break;
|
||||
case 2: envLabel = ""; break; // Fall: just show the number (WoW convention)
|
||||
case 3: envLabel = "Lava "; break;
|
||||
case 4: envLabel = "Slime "; break;
|
||||
case 5: envLabel = "Fire "; break;
|
||||
default: envLabel = ""; break;
|
||||
}
|
||||
snprintf(text, sizeof(text), "%s-%d", envLabel, entry.amount);
|
||||
color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental
|
||||
break;
|
||||
}
|
||||
case game::CombatTextEntry::ENERGIZE:
|
||||
snprintf(text, sizeof(text), "+%d", entry.amount);
|
||||
switch (entry.powerType) {
|
||||
|
|
@ -9285,6 +9395,15 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|||
else if (m.roles & 0x08)
|
||||
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
|
||||
uint32_t hp = m.hasPartyStats ? m.curHealth : 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]"); }
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
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 bool isProfessionTrainer = (trainer.trainerType == 2);
|
||||
|
||||
// NPC name
|
||||
auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid);
|
||||
|
|
@ -13838,11 +13970,21 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
logCount++;
|
||||
}
|
||||
|
||||
if (!canTrain) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton("Train")) {
|
||||
gameHandler.trainSpell(spell->spellId);
|
||||
if (isProfessionTrainer && alreadyKnown) {
|
||||
// Profession trainer: known recipes show "Create" button to craft
|
||||
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();
|
||||
}
|
||||
|
|
@ -13946,6 +14088,79 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
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();
|
||||
|
|
@ -14894,6 +15109,16 @@ void GameScreen::renderSettingsWindow() {
|
|||
ImGui::Separator();
|
||||
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))) {
|
||||
pendingFullscreen = kDefaultFullscreen;
|
||||
pendingVsync = kDefaultVsync;
|
||||
|
|
@ -14906,9 +15131,11 @@ void GameScreen::renderSettingsWindow() {
|
|||
pendingPOM = true;
|
||||
pendingPOMQuality = 1;
|
||||
pendingResIndex = defaultResIndex;
|
||||
pendingBrightness = 50;
|
||||
window->setFullscreen(pendingFullscreen);
|
||||
window->setVsync(pendingVsync);
|
||||
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
||||
if (renderer) renderer->setBrightness(1.0f);
|
||||
pendingWaterRefraction = false;
|
||||
if (renderer) {
|
||||
renderer->setShadowsEnabled(pendingShadows);
|
||||
|
|
@ -15320,6 +15547,10 @@ void GameScreen::renderSettingsWindow() {
|
|||
inventoryScreen.setSeparateBags(pendingSeparateBags);
|
||||
saveSettings();
|
||||
}
|
||||
if (ImGui::Checkbox("Show Key Ring", &pendingShowKeyring)) {
|
||||
inventoryScreen.setShowKeyring(pendingShowKeyring);
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
|
@ -15335,6 +15566,8 @@ void GameScreen::renderSettingsWindow() {
|
|||
pendingMinimapNpcDots = false;
|
||||
pendingSeparateBags = true;
|
||||
inventoryScreen.setSeparateBags(true);
|
||||
pendingShowKeyring = true;
|
||||
inventoryScreen.setShowKeyring(true);
|
||||
uiOpacity_ = 0.65f;
|
||||
minimapRotate_ = false;
|
||||
minimapSquare_ = false;
|
||||
|
|
@ -17238,6 +17471,7 @@ void GameScreen::saveSettings() {
|
|||
out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n";
|
||||
out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n";
|
||||
out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n";
|
||||
out << "show_keyring=" << (pendingShowKeyring ? 1 : 0) << "\n";
|
||||
out << "action_bar_scale=" << pendingActionBarScale << "\n";
|
||||
out << "nameplate_scale=" << nameplateScale_ << "\n";
|
||||
out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n";
|
||||
|
|
@ -17271,6 +17505,7 @@ void GameScreen::saveSettings() {
|
|||
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
|
||||
out << "shadows=" << (pendingShadows ? 1 : 0) << "\n";
|
||||
out << "shadow_distance=" << pendingShadowDistance << "\n";
|
||||
out << "brightness=" << pendingBrightness << "\n";
|
||||
out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n";
|
||||
out << "antialiasing=" << pendingAntiAliasing << "\n";
|
||||
out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n";
|
||||
|
|
@ -17359,6 +17594,9 @@ void GameScreen::loadSettings() {
|
|||
} else if (key == "separate_bags") {
|
||||
pendingSeparateBags = (std::stoi(val) != 0);
|
||||
inventoryScreen.setSeparateBags(pendingSeparateBags);
|
||||
} else if (key == "show_keyring") {
|
||||
pendingShowKeyring = (std::stoi(val) != 0);
|
||||
inventoryScreen.setShowKeyring(pendingShowKeyring);
|
||||
} else if (key == "action_bar_scale") {
|
||||
pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f);
|
||||
} 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 == "shadows") pendingShadows = (std::stoi(val) != 0);
|
||||
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 == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
|
||||
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);
|
||||
color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f);
|
||||
break;
|
||||
case T::ENVIRONMENTAL:
|
||||
snprintf(desc, sizeof(desc), "Environmental damage: %d", e.amount);
|
||||
case T::ENVIRONMENTAL: {
|
||||
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);
|
||||
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)
|
||||
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
|
||||
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);
|
||||
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)
|
||||
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
|
||||
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);
|
||||
break;
|
||||
}
|
||||
case T::XP_GAIN:
|
||||
snprintf(desc, sizeof(desc), "You gain %d experience", e.amount);
|
||||
color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f);
|
||||
|
|
|
|||
|
|
@ -903,10 +903,10 @@ void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t m
|
|||
float posX = screenW - windowW - 10.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);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
||||
if (!ImGui::Begin("Bags", &open, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
|
|
@ -1030,8 +1030,7 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
|
|||
float windowW = std::max(gridW, titleW);
|
||||
float windowH = contentH + 40.0f; // title bar + padding
|
||||
|
||||
// Keep separate bag windows anchored to the bag-bar stack.
|
||||
ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
||||
|
|
@ -1069,20 +1068,29 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
|
|||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (bagIndex < 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
|
||||
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
|
||||
if (i % columns != 0) ImGui::SameLine();
|
||||
const auto& slot = inventory.getKeyringSlot(i);
|
||||
char id[32];
|
||||
snprintf(id, sizeof(id), "##skr_%d", i);
|
||||
ImGui::PushID(id);
|
||||
// Keyring is display-only for now.
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||
ImGui::PopID();
|
||||
if (bagIndex < 0 && showKeyring_) {
|
||||
constexpr float keySlotSize = 24.0f;
|
||||
constexpr int keyCols = 8;
|
||||
// Only show rows that contain items (round up to full row)
|
||||
int lastOccupied = -1;
|
||||
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 (visibleSlots > 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
|
||||
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;
|
||||
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
|
||||
if (!inventory.getKeyringSlot(i).empty()) {
|
||||
keyringHasAnyItems = true;
|
||||
break;
|
||||
if (showKeyring_) {
|
||||
constexpr float keySlotSize = 24.0f;
|
||||
constexpr int keyCols = 8;
|
||||
int lastOccupied = -1;
|
||||
for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) {
|
||||
if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; }
|
||||
}
|
||||
}
|
||||
if (!collapseEmptySections || keyringHasAnyItems) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
|
||||
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
|
||||
if (i % columns != 0) ImGui::SameLine();
|
||||
const auto& slot = inventory.getKeyringSlot(i);
|
||||
char sid[32];
|
||||
snprintf(sid, sizeof(sid), "##keyring_%d", i);
|
||||
ImGui::PushID(sid);
|
||||
// Keyring is display-only for now.
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||
ImGui::PopID();
|
||||
int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols;
|
||||
if (visibleSlots > 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
|
||||
for (int i = 0; i < visibleSlots; ++i) {
|
||||
if (i % keyCols != 0) ImGui::SameLine();
|
||||
const auto& slot = inventory.getKeyringSlot(i);
|
||||
char sid[32];
|
||||
snprintf(sid, sizeof(sid), "##keyring_%d", i);
|
||||
ImGui::PushID(sid);
|
||||
renderItemSlot(inventory, slot, keySlotSize, nullptr,
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2952,17 +2961,26 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
// Find a label for this stat type
|
||||
const char* lbl = nullptr;
|
||||
switch (t) {
|
||||
case 31: lbl = "Hit"; break;
|
||||
case 32: lbl = "Crit"; break;
|
||||
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 36: lbl = "Haste"; 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;
|
||||
}
|
||||
|
|
@ -2970,6 +2988,10 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
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)
|
||||
|
|
@ -3502,6 +3524,16 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
|
|||
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("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));
|
||||
|
|
@ -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("Spi", static_cast<float>(info.spirit), static_cast<float>(eq->item.spirit));
|
||||
|
||||
// Hint text
|
||||
ImGui::TextDisabled("Hold Shift to compare");
|
||||
// Extra stats diff — union of stat types from both items
|
||||
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) {
|
||||
ImGui::TextDisabled("Hold Shift to compare");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#include "ui/keybinding_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace wowee::ui {
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ const char* KeybindingManager::getActionName(Action action) {
|
|||
void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
|
||||
std::ifstream file(filePath);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -301,9 +301,9 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
|
|||
if (outFile.is_open()) {
|
||||
outFile << content;
|
||||
outFile.close();
|
||||
std::cout << "[KeybindingManager] Saved keybindings to " << filePath << std::endl;
|
||||
LOG_INFO("KeybindingManager: Saved keybindings to ", filePath);
|
||||
} else {
|
||||
std::cerr << "[KeybindingManager] Failed to write config file: " << filePath << std::endl;
|
||||
LOG_ERROR("KeybindingManager: Failed to write config file: ", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -593,15 +593,27 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
if (!dbc || !dbc->isLoaded()) return;
|
||||
|
||||
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();
|
||||
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;
|
||||
|
||||
uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
|
||||
uint32_t iconId = dbc->getUInt32(i, iconField);
|
||||
spellIconIds[spellId] = iconId;
|
||||
|
||||
std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139);
|
||||
std::string tooltip = dbc->getString(i, tooltipField);
|
||||
if (!tooltip.empty()) {
|
||||
spellTooltips[spellId] = tooltip;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue