mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add mailbox system and fix logging performance stutter
Implement full mail send/receive: SMSG_SHOW_MAILBOX, CMSG_GET_MAIL_LIST, SMSG_MAIL_LIST_RESULT, CMSG_SEND_MAIL, SMSG_SEND_MAIL_RESULT, mail take money/item/delete/mark-as-read, and inbox/compose UI windows. Fix periodic stuttering in Stormwind caused by synchronous per-line disk flushes in the logger — remove fileStream.flush() and std::endl, downgrade high-volume per-packet/per-model/per-texture LOG_INFO to LOG_DEBUG.
This commit is contained in:
parent
9bc8c5c85a
commit
8a468e9533
14 changed files with 782 additions and 22 deletions
|
|
@ -267,5 +267,16 @@
|
|||
"SMSG_CHANNEL_NOTIFY": "0x099",
|
||||
"CMSG_CHANNEL_LIST": "0x09A",
|
||||
"SMSG_CHANNEL_LIST": "0x09B",
|
||||
"SMSG_INSPECT_TALENT": "0x3F4"
|
||||
"SMSG_INSPECT_TALENT": "0x3F4",
|
||||
"SMSG_SHOW_MAILBOX": "0x24B",
|
||||
"CMSG_GET_MAIL_LIST": "0x23A",
|
||||
"SMSG_MAIL_LIST_RESULT": "0x23B",
|
||||
"CMSG_SEND_MAIL": "0x238",
|
||||
"SMSG_SEND_MAIL_RESULT": "0x239",
|
||||
"CMSG_MAIL_TAKE_MONEY": "0x245",
|
||||
"CMSG_MAIL_TAKE_ITEM": "0x244",
|
||||
"CMSG_MAIL_DELETE": "0x243",
|
||||
"CMSG_MAIL_MARK_AS_READ": "0x242",
|
||||
"SMSG_RECEIVED_MAIL": "0x285",
|
||||
"MSG_QUERY_NEXT_MAIL_TIME": "0x284"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -762,6 +762,23 @@ public:
|
|||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||
|
||||
// Mail
|
||||
bool isMailboxOpen() const { return mailboxOpen_; }
|
||||
const std::vector<MailMessage>& getMailInbox() const { return mailInbox_; }
|
||||
int getSelectedMailIndex() const { return selectedMailIndex_; }
|
||||
void setSelectedMailIndex(int idx) { selectedMailIndex_ = idx; }
|
||||
bool isMailComposeOpen() const { return showMailCompose_; }
|
||||
void openMailCompose() { showMailCompose_ = true; }
|
||||
void closeMailCompose() { showMailCompose_ = false; }
|
||||
void closeMailbox();
|
||||
void sendMail(const std::string& recipient, const std::string& subject,
|
||||
const std::string& body, uint32_t money, uint32_t cod = 0);
|
||||
void mailTakeMoney(uint32_t mailId);
|
||||
void mailTakeItem(uint32_t mailId, uint32_t itemIndex);
|
||||
void mailDelete(uint32_t mailId);
|
||||
void mailMarkAsRead(uint32_t mailId);
|
||||
void refreshMailList();
|
||||
|
||||
// Trainer
|
||||
bool isTrainerWindowOpen() const { return trainerWindowOpen_; }
|
||||
const TrainerListData& getTrainerSpells() const { return currentTrainerList_; }
|
||||
|
|
@ -987,6 +1004,12 @@ private:
|
|||
void handleArenaTeamEvent(network::Packet& packet);
|
||||
void handleArenaError(network::Packet& packet);
|
||||
|
||||
// ---- Mail handlers ----
|
||||
void handleShowMailbox(network::Packet& packet);
|
||||
void handleMailListResult(network::Packet& packet);
|
||||
void handleSendMailResult(network::Packet& packet);
|
||||
void handleReceivedMail(network::Packet& packet);
|
||||
|
||||
// ---- Taxi handlers ----
|
||||
void handleShowTaxiNodes(network::Packet& packet);
|
||||
void handleActivateTaxiReply(network::Packet& packet);
|
||||
|
|
@ -1322,6 +1345,13 @@ private:
|
|||
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
|
||||
void updateClientTaxi(float deltaTime);
|
||||
|
||||
// Mail
|
||||
bool mailboxOpen_ = false;
|
||||
uint64_t mailboxGuid_ = 0;
|
||||
std::vector<MailMessage> mailInbox_;
|
||||
int selectedMailIndex_ = -1;
|
||||
bool showMailCompose_ = false;
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
ListInventoryData currentVendorItems;
|
||||
|
|
|
|||
|
|
@ -365,6 +365,19 @@ enum class LogicalOpcode : uint16_t {
|
|||
CMSG_CHANNEL_LIST,
|
||||
SMSG_CHANNEL_LIST,
|
||||
|
||||
// ---- Mail ----
|
||||
SMSG_SHOW_MAILBOX,
|
||||
CMSG_GET_MAIL_LIST,
|
||||
SMSG_MAIL_LIST_RESULT,
|
||||
CMSG_SEND_MAIL,
|
||||
SMSG_SEND_MAIL_RESULT,
|
||||
CMSG_MAIL_TAKE_MONEY,
|
||||
CMSG_MAIL_TAKE_ITEM,
|
||||
CMSG_MAIL_DELETE,
|
||||
CMSG_MAIL_MARK_AS_READ,
|
||||
SMSG_RECEIVED_MAIL,
|
||||
MSG_QUERY_NEXT_MAIL_TIME,
|
||||
|
||||
// Sentinel
|
||||
COUNT
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2166,5 +2166,77 @@ public:
|
|||
static network::Packet build(uint64_t casterGuid, bool accept);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Mail System
|
||||
// ============================================================
|
||||
|
||||
struct MailAttachment {
|
||||
uint8_t slot = 0;
|
||||
uint32_t itemGuidLow = 0;
|
||||
uint32_t itemId = 0;
|
||||
uint32_t enchantId = 0;
|
||||
uint32_t randomPropertyId = 0;
|
||||
uint32_t randomSuffix = 0;
|
||||
uint32_t stackCount = 1;
|
||||
uint32_t chargesOrDurability = 0;
|
||||
uint32_t maxDurability = 0;
|
||||
};
|
||||
|
||||
struct MailMessage {
|
||||
uint32_t messageId = 0;
|
||||
uint8_t messageType = 0; // 0=normal, 2=auction, 3=creature, 4=gameobject
|
||||
uint64_t senderGuid = 0;
|
||||
uint32_t senderEntry = 0; // For non-player mail
|
||||
std::string senderName;
|
||||
std::string subject;
|
||||
std::string body;
|
||||
uint32_t stationeryId = 0;
|
||||
uint32_t money = 0;
|
||||
uint32_t cod = 0; // Cash on delivery
|
||||
uint32_t flags = 0;
|
||||
float expirationTime = 0.0f;
|
||||
uint32_t mailTemplateId = 0;
|
||||
bool read = false;
|
||||
std::vector<MailAttachment> attachments;
|
||||
};
|
||||
|
||||
/** CMSG_GET_MAIL_LIST packet builder */
|
||||
class GetMailListPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid);
|
||||
};
|
||||
|
||||
/** CMSG_SEND_MAIL packet builder */
|
||||
class SendMailPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint32_t money, uint32_t cod);
|
||||
};
|
||||
|
||||
/** CMSG_MAIL_TAKE_MONEY packet builder */
|
||||
class MailTakeMoneyPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId);
|
||||
};
|
||||
|
||||
/** CMSG_MAIL_TAKE_ITEM packet builder */
|
||||
class MailTakeItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex);
|
||||
};
|
||||
|
||||
/** CMSG_MAIL_DELETE packet builder */
|
||||
class MailDeletePacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId);
|
||||
};
|
||||
|
||||
/** CMSG_MAIL_MARK_AS_READ packet builder */
|
||||
class MailMarkAsReadPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ private:
|
|||
void renderGuildRoster(game::GameHandler& gameHandler);
|
||||
void renderGuildInvitePopup(game::GameHandler& gameHandler);
|
||||
void renderChatBubbles(game::GameHandler& gameHandler);
|
||||
void renderMailWindow(game::GameHandler& gameHandler);
|
||||
void renderMailComposeWindow(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
|
|
@ -242,6 +244,12 @@ private:
|
|||
std::vector<ChatBubble> chatBubbles_;
|
||||
bool chatBubbleCallbackSet_ = false;
|
||||
|
||||
// Mail compose state
|
||||
char mailRecipientBuffer_[256] = "";
|
||||
char mailSubjectBuffer_[256] = "";
|
||||
char mailBodyBuffer_[2048] = "";
|
||||
int mailComposeMoney_[3] = {0, 0, 0}; // gold, silver, copper
|
||||
|
||||
// Left-click targeting: distinguish click from camera drag
|
||||
glm::vec2 leftClickPressPos_ = glm::vec2(0.0f);
|
||||
bool leftClickWasPress_ = false;
|
||||
|
|
|
|||
|
|
@ -54,10 +54,9 @@ void Logger::log(LogLevel level, const std::string& message) {
|
|||
|
||||
line << "] " << message;
|
||||
|
||||
std::cout << line.str() << std::endl;
|
||||
std::cout << line.str() << '\n';
|
||||
if (fileStream.is_open()) {
|
||||
fileStream << line.str() << std::endl;
|
||||
fileStream.flush();
|
||||
fileStream << line.str() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1508,6 +1508,20 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
|
||||
// ---- Mail ----
|
||||
case Opcode::SMSG_SHOW_MAILBOX:
|
||||
handleShowMailbox(packet);
|
||||
break;
|
||||
case Opcode::SMSG_MAIL_LIST_RESULT:
|
||||
handleMailListResult(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SEND_MAIL_RESULT:
|
||||
handleSendMailResult(packet);
|
||||
break;
|
||||
case Opcode::SMSG_RECEIVED_MAIL:
|
||||
handleReceivedMail(packet);
|
||||
break;
|
||||
|
||||
default:
|
||||
// In pre-world states we need full visibility (char create/login handshakes).
|
||||
// In-world we keep de-duplication to avoid heavy log I/O in busy areas.
|
||||
|
|
@ -3978,7 +3992,7 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
|
||||
void GameHandler::handleDestroyObject(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_DESTROY_OBJECT");
|
||||
LOG_DEBUG("Handling SMSG_DESTROY_OBJECT");
|
||||
|
||||
DestroyObjectData data;
|
||||
if (!DestroyObjectParser::parse(packet, data)) {
|
||||
|
|
@ -9605,6 +9619,278 @@ void GameHandler::updateAttachedTransportChildren(float /*deltaTime*/) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mail System
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::closeMailbox() {
|
||||
mailboxOpen_ = false;
|
||||
mailboxGuid_ = 0;
|
||||
mailInbox_.clear();
|
||||
selectedMailIndex_ = -1;
|
||||
showMailCompose_ = false;
|
||||
}
|
||||
|
||||
void GameHandler::refreshMailList() {
|
||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||
auto packet = GetMailListPacket::build(mailboxGuid_);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::sendMail(const std::string& recipient, const std::string& subject,
|
||||
const std::string& body, uint32_t money, uint32_t cod) {
|
||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||
auto packet = SendMailPacket::build(mailboxGuid_, recipient, subject, body, money, cod);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::mailTakeMoney(uint32_t mailId) {
|
||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||
auto packet = MailTakeMoneyPacket::build(mailboxGuid_, mailId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::mailTakeItem(uint32_t mailId, uint32_t itemIndex) {
|
||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||
auto packet = MailTakeItemPacket::build(mailboxGuid_, mailId, itemIndex);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::mailDelete(uint32_t mailId) {
|
||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||
// Find mail template ID for this mail
|
||||
uint32_t templateId = 0;
|
||||
for (const auto& m : mailInbox_) {
|
||||
if (m.messageId == mailId) {
|
||||
templateId = m.mailTemplateId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto packet = MailDeletePacket::build(mailboxGuid_, mailId, templateId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::mailMarkAsRead(uint32_t mailId) {
|
||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||
auto packet = MailMarkAsReadPacket::build(mailboxGuid_, mailId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handleShowMailbox(network::Packet& packet) {
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
LOG_WARNING("SMSG_SHOW_MAILBOX too short");
|
||||
return;
|
||||
}
|
||||
uint64_t guid = packet.readUInt64();
|
||||
LOG_INFO("SMSG_SHOW_MAILBOX: guid=0x", std::hex, guid, std::dec);
|
||||
mailboxGuid_ = guid;
|
||||
mailboxOpen_ = true;
|
||||
selectedMailIndex_ = -1;
|
||||
showMailCompose_ = false;
|
||||
// Request inbox contents
|
||||
refreshMailList();
|
||||
}
|
||||
|
||||
void GameHandler::handleMailListResult(network::Packet& packet) {
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining < 5) {
|
||||
LOG_WARNING("SMSG_MAIL_LIST_RESULT too short (", remaining, " bytes)");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t totalCount = packet.readUInt32();
|
||||
uint8_t shownCount = packet.readUInt8();
|
||||
|
||||
LOG_INFO("SMSG_MAIL_LIST_RESULT: total=", totalCount, " shown=", (int)shownCount);
|
||||
|
||||
mailInbox_.clear();
|
||||
mailInbox_.reserve(shownCount);
|
||||
|
||||
for (uint8_t i = 0; i < shownCount; ++i) {
|
||||
remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining < 2) break;
|
||||
|
||||
// Read size of this mail entry (uint16)
|
||||
uint16_t msgSize = packet.readUInt16();
|
||||
size_t startPos = packet.getReadPos();
|
||||
|
||||
MailMessage msg;
|
||||
if (remaining < static_cast<size_t>(msgSize) + 2) {
|
||||
LOG_WARNING("Mail entry ", i, " truncated");
|
||||
break;
|
||||
}
|
||||
|
||||
msg.messageId = packet.readUInt32();
|
||||
msg.messageType = packet.readUInt8();
|
||||
|
||||
switch (msg.messageType) {
|
||||
case 0: // Normal player mail
|
||||
msg.senderGuid = packet.readUInt64();
|
||||
break;
|
||||
case 2: // Auction
|
||||
case 3: // Creature
|
||||
case 4: // GameObject
|
||||
case 5: // Calendar
|
||||
msg.senderEntry = packet.readUInt32();
|
||||
break;
|
||||
default:
|
||||
msg.senderEntry = packet.readUInt32();
|
||||
break;
|
||||
}
|
||||
|
||||
msg.cod = packet.readUInt32();
|
||||
packet.readUInt32(); // unknown / item text id
|
||||
packet.readUInt32(); // unknown
|
||||
msg.stationeryId = packet.readUInt32();
|
||||
msg.money = packet.readUInt32();
|
||||
msg.flags = packet.readUInt32();
|
||||
msg.expirationTime = packet.readFloat();
|
||||
msg.mailTemplateId = packet.readUInt32();
|
||||
msg.subject = packet.readString();
|
||||
|
||||
// Body - only present if not a mail template
|
||||
if (msg.mailTemplateId == 0) {
|
||||
msg.body = packet.readString();
|
||||
}
|
||||
|
||||
// Attachments
|
||||
uint8_t attachCount = packet.readUInt8();
|
||||
msg.attachments.reserve(attachCount);
|
||||
for (uint8_t j = 0; j < attachCount; ++j) {
|
||||
MailAttachment att;
|
||||
att.slot = packet.readUInt8();
|
||||
att.itemGuidLow = packet.readUInt32();
|
||||
att.itemId = packet.readUInt32();
|
||||
|
||||
// Enchantments (7 slots: id, duration, charges per slot = 21 uint32s)
|
||||
for (int e = 0; e < 7; ++e) {
|
||||
uint32_t enchId = packet.readUInt32();
|
||||
packet.readUInt32(); // duration
|
||||
packet.readUInt32(); // charges
|
||||
if (e == 0) att.enchantId = enchId;
|
||||
}
|
||||
|
||||
att.randomPropertyId = packet.readUInt32();
|
||||
att.randomSuffix = packet.readUInt32();
|
||||
att.stackCount = packet.readUInt32();
|
||||
att.chargesOrDurability = packet.readUInt32();
|
||||
att.maxDurability = packet.readUInt32();
|
||||
|
||||
msg.attachments.push_back(att);
|
||||
}
|
||||
|
||||
msg.read = (msg.flags & 0x01) != 0; // MAIL_CHECK_MASK_READ
|
||||
|
||||
// Resolve sender name for player mail
|
||||
if (msg.messageType == 0 && msg.senderGuid != 0) {
|
||||
msg.senderName = getCachedPlayerName(msg.senderGuid);
|
||||
if (msg.senderName.empty()) {
|
||||
queryPlayerName(msg.senderGuid);
|
||||
msg.senderName = "Unknown";
|
||||
}
|
||||
} else if (msg.messageType == 2) {
|
||||
msg.senderName = "Auction House";
|
||||
} else if (msg.messageType == 3) {
|
||||
msg.senderName = getCachedCreatureName(msg.senderEntry);
|
||||
if (msg.senderName.empty()) msg.senderName = "NPC";
|
||||
} else {
|
||||
msg.senderName = "System";
|
||||
}
|
||||
|
||||
mailInbox_.push_back(std::move(msg));
|
||||
|
||||
// Skip any unread bytes in this mail entry
|
||||
size_t consumed = packet.getReadPos() - startPos;
|
||||
if (consumed < msgSize) {
|
||||
size_t skip = msgSize - consumed;
|
||||
for (size_t s = 0; s < skip && packet.getReadPos() < packet.getSize(); ++s) {
|
||||
packet.readUInt8();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Parsed ", mailInbox_.size(), " mail messages");
|
||||
}
|
||||
|
||||
void GameHandler::handleSendMailResult(network::Packet& packet) {
|
||||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||||
LOG_WARNING("SMSG_SEND_MAIL_RESULT too short");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t mailId = packet.readUInt32();
|
||||
uint32_t command = packet.readUInt32();
|
||||
uint32_t error = packet.readUInt32();
|
||||
|
||||
// Commands: 0=send, 1=moneyTaken, 2=itemTaken, 3=returnedToSender, 4=deleted, 5=madePermanent
|
||||
// Errors: 0=OK, 1=equip, 2=cannotSend, 3=messageTooBig, 4=noMoney, ...
|
||||
static const char* cmdNames[] = {"Send", "TakeMoney", "TakeItem", "Return", "Delete", "MadePermanent"};
|
||||
const char* cmdName = (command < 6) ? cmdNames[command] : "Unknown";
|
||||
|
||||
LOG_INFO("SMSG_SEND_MAIL_RESULT: mailId=", mailId, " cmd=", cmdName, " error=", error);
|
||||
|
||||
if (error == 0) {
|
||||
// Success
|
||||
switch (command) {
|
||||
case 0: // Send
|
||||
addSystemChatMessage("Mail sent successfully.");
|
||||
showMailCompose_ = false;
|
||||
refreshMailList();
|
||||
break;
|
||||
case 1: // Money taken
|
||||
addSystemChatMessage("Money received from mail.");
|
||||
refreshMailList();
|
||||
break;
|
||||
case 2: // Item taken
|
||||
addSystemChatMessage("Item received from mail.");
|
||||
refreshMailList();
|
||||
break;
|
||||
case 4: // Deleted
|
||||
selectedMailIndex_ = -1;
|
||||
refreshMailList();
|
||||
break;
|
||||
default:
|
||||
refreshMailList();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Error
|
||||
std::string errMsg = "Mail error: ";
|
||||
switch (error) {
|
||||
case 1: errMsg += "Equipment error."; break;
|
||||
case 2: errMsg += "Cannot send mail."; break;
|
||||
case 3: errMsg += "Message too big."; break;
|
||||
case 4: errMsg += "Not enough money."; break;
|
||||
case 5: errMsg += "Not enough items."; break;
|
||||
case 6: errMsg += "Recipient not found."; break;
|
||||
case 7: errMsg += "Cannot send to that player."; break;
|
||||
case 8: errMsg += "Equip error."; break;
|
||||
case 9: errMsg += "Inventory full."; break;
|
||||
case 10: errMsg += "Not a GM."; break;
|
||||
case 11: errMsg += "Max attachments exceeded."; break;
|
||||
case 14: errMsg += "Cannot send wrapped COD."; break;
|
||||
case 15: errMsg += "Mail and chat suspended."; break;
|
||||
case 16: errMsg += "Too many attachments."; break;
|
||||
default: errMsg += "Unknown error (" + std::to_string(error) + ")."; break;
|
||||
}
|
||||
addSystemChatMessage(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleReceivedMail(network::Packet& packet) {
|
||||
// Server notifies us that new mail arrived
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
float nextMailTime = packet.readFloat();
|
||||
(void)nextMailTime;
|
||||
}
|
||||
LOG_INFO("SMSG_RECEIVED_MAIL: New mail arrived!");
|
||||
addSystemChatMessage("New mail has arrived.");
|
||||
// If mailbox is open, refresh
|
||||
if (mailboxOpen_) {
|
||||
refreshMailList();
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 GameHandler::getComposedWorldPosition() {
|
||||
if (playerTransportGuid_ != 0 && transportManager_) {
|
||||
return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
|
|
|
|||
|
|
@ -291,6 +291,18 @@ static const OpcodeNameEntry kOpcodeNames[] = {
|
|||
{"CMSG_CHANNEL_LIST", LogicalOpcode::CMSG_CHANNEL_LIST},
|
||||
{"SMSG_CHANNEL_LIST", LogicalOpcode::SMSG_CHANNEL_LIST},
|
||||
{"SMSG_INSPECT_TALENT", LogicalOpcode::SMSG_INSPECT_TALENT},
|
||||
// Mail
|
||||
{"SMSG_SHOW_MAILBOX", LogicalOpcode::SMSG_SHOW_MAILBOX},
|
||||
{"CMSG_GET_MAIL_LIST", LogicalOpcode::CMSG_GET_MAIL_LIST},
|
||||
{"SMSG_MAIL_LIST_RESULT", LogicalOpcode::SMSG_MAIL_LIST_RESULT},
|
||||
{"CMSG_SEND_MAIL", LogicalOpcode::CMSG_SEND_MAIL},
|
||||
{"SMSG_SEND_MAIL_RESULT", LogicalOpcode::SMSG_SEND_MAIL_RESULT},
|
||||
{"CMSG_MAIL_TAKE_MONEY", LogicalOpcode::CMSG_MAIL_TAKE_MONEY},
|
||||
{"CMSG_MAIL_TAKE_ITEM", LogicalOpcode::CMSG_MAIL_TAKE_ITEM},
|
||||
{"CMSG_MAIL_DELETE", LogicalOpcode::CMSG_MAIL_DELETE},
|
||||
{"CMSG_MAIL_MARK_AS_READ", LogicalOpcode::CMSG_MAIL_MARK_AS_READ},
|
||||
{"SMSG_RECEIVED_MAIL", LogicalOpcode::SMSG_RECEIVED_MAIL},
|
||||
{"MSG_QUERY_NEXT_MAIL_TIME", LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
|
@ -583,6 +595,18 @@ void OpcodeTable::loadWotlkDefaults() {
|
|||
{LogicalOpcode::CMSG_CHANNEL_LIST, 0x09A},
|
||||
{LogicalOpcode::SMSG_CHANNEL_LIST, 0x09B},
|
||||
{LogicalOpcode::SMSG_INSPECT_TALENT, 0x3F4},
|
||||
// Mail
|
||||
{LogicalOpcode::SMSG_SHOW_MAILBOX, 0x24B},
|
||||
{LogicalOpcode::CMSG_GET_MAIL_LIST, 0x23A},
|
||||
{LogicalOpcode::SMSG_MAIL_LIST_RESULT, 0x23B},
|
||||
{LogicalOpcode::CMSG_SEND_MAIL, 0x238},
|
||||
{LogicalOpcode::SMSG_SEND_MAIL_RESULT, 0x239},
|
||||
{LogicalOpcode::CMSG_MAIL_TAKE_MONEY, 0x245},
|
||||
{LogicalOpcode::CMSG_MAIL_TAKE_ITEM, 0x244},
|
||||
{LogicalOpcode::CMSG_MAIL_DELETE, 0x243},
|
||||
{LogicalOpcode::CMSG_MAIL_MARK_AS_READ, 0x242},
|
||||
{LogicalOpcode::SMSG_RECEIVED_MAIL, 0x285},
|
||||
{LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME, 0x284},
|
||||
};
|
||||
|
||||
logicalToWire_.clear();
|
||||
|
|
|
|||
|
|
@ -1116,9 +1116,9 @@ bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data
|
|||
data.isDeath = false;
|
||||
}
|
||||
|
||||
LOG_INFO("Parsed SMSG_DESTROY_OBJECT:");
|
||||
LOG_INFO(" GUID: 0x", std::hex, data.guid, std::dec);
|
||||
LOG_INFO(" Is death: ", data.isDeath ? "yes" : "no");
|
||||
LOG_DEBUG("Parsed SMSG_DESTROY_OBJECT:");
|
||||
LOG_DEBUG(" GUID: 0x", std::hex, data.guid, std::dec);
|
||||
LOG_DEBUG(" Is death: ", data.isDeath ? "yes" : "no");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2101,7 +2101,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
data.valid = !data.name.empty();
|
||||
LOG_INFO("Item query response: ", data.name, " (quality=", data.quality,
|
||||
LOG_DEBUG("Item query response: ", data.name, " (quality=", data.quality,
|
||||
" invType=", data.inventoryType, " stack=", data.maxStack, ")");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2463,7 +2463,7 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||
LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2485,7 +2485,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
|||
data.missCount = packet.readUInt8();
|
||||
// Skip miss details for now
|
||||
|
||||
LOG_INFO("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||
" misses=", (int)data.missCount);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -3303,5 +3303,61 @@ network::Packet GameObjectUsePacket::build(uint64_t guid) {
|
|||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mail System
|
||||
// ============================================================
|
||||
|
||||
network::Packet GetMailListPacket::build(uint64_t mailboxGuid) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GET_MAIL_LIST));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& recipient,
|
||||
const std::string& subject, const std::string& body,
|
||||
uint32_t money, uint32_t cod) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeString(recipient);
|
||||
packet.writeString(subject);
|
||||
packet.writeString(body);
|
||||
packet.writeUInt32(0); // stationery (default)
|
||||
packet.writeUInt32(0); // unknown
|
||||
packet.writeUInt8(0); // attachment count (no item attachments for now)
|
||||
packet.writeUInt32(money);
|
||||
packet.writeUInt32(cod);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_MONEY));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
packet.writeUInt32(itemIndex);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailDeletePacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
packet.writeUInt32(mailTemplateId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_MARK_AS_READ));
|
||||
packet.writeUInt64(mailboxGuid);
|
||||
packet.writeUInt32(mailId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ void WorldSocket::update() {
|
|||
}
|
||||
|
||||
if (receivedAny) {
|
||||
LOG_INFO("World socket read ", bytesReadThisTick, " bytes in ", readOps,
|
||||
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
|
||||
" recv call(s), buffered=", receiveBuffer.size());
|
||||
// Hex dump received bytes for auth debugging
|
||||
if (bytesReadThisTick <= 128) {
|
||||
|
|
@ -304,11 +304,11 @@ void WorldSocket::update() {
|
|||
for (size_t i = 0; i < receiveBuffer.size(); ++i) {
|
||||
char buf[4]; snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]); hex += buf;
|
||||
}
|
||||
LOG_INFO("World socket raw bytes: ", hex);
|
||||
LOG_DEBUG("World socket raw bytes: ", hex);
|
||||
}
|
||||
tryParsePackets();
|
||||
if (connected && !receiveBuffer.empty()) {
|
||||
LOG_INFO("World socket parse left ", receiveBuffer.size(),
|
||||
LOG_DEBUG("World socket parse left ", receiveBuffer.size(),
|
||||
" bytes buffered (awaiting complete packet)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@ void DBCLayout::loadWotlkDefaults() {
|
|||
{ "InventoryIcon", 5 }, { "GeosetGroup1", 7 }, { "GeosetGroup3", 9 }}};
|
||||
|
||||
// CharSections.dbc
|
||||
// Binary layout: ID(0) Race(1) Sex(2) Section(3) Tex1(4) Tex2(5) Tex3(6) Flags(7) Variation(8) Color(9)
|
||||
// Binary layout: ID(0) Race(1) Sex(2) Section(3) Variation(4) Color(5) Tex1(6) Tex2(7) Tex3(8) Flags(9)
|
||||
layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 },
|
||||
{ "Texture1", 4 }, { "Texture2", 5 }, { "Texture3", 6 },
|
||||
{ "Flags", 7 }, { "VariationIndex", 8 }, { "ColorIndex", 9 }}};
|
||||
{ "VariationIndex", 4 }, { "ColorIndex", 5 },
|
||||
{ "Texture1", 6 }, { "Texture2", 7 }, { "Texture3", 8 },
|
||||
{ "Flags", 9 }}};
|
||||
|
||||
// SpellIcon.dbc (Icon.dbc in code but actually SpellIcon)
|
||||
layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}};
|
||||
|
|
|
|||
|
|
@ -744,7 +744,7 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
header.nParticleEmitters = r32();
|
||||
header.ofsParticleEmitters = r32();
|
||||
|
||||
core::Logger::getInstance().info("Vanilla M2 (version ", header.version,
|
||||
core::Logger::getInstance().debug("Vanilla M2 (version ", header.version,
|
||||
"): nVerts=", header.nVertices, " nViews=", header.nViews,
|
||||
" ofsViews=", ofsViews, " nTex=", header.nTextures);
|
||||
} else {
|
||||
|
|
@ -1315,7 +1315,7 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
}
|
||||
}
|
||||
|
||||
core::Logger::getInstance().info("Vanilla M2: embedded skin loaded — ",
|
||||
core::Logger::getInstance().debug("Vanilla M2: embedded skin loaded — ",
|
||||
model.indices.size(), " indices, ", model.batches.size(), " batches");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -657,7 +657,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
|||
composite[dstIdx + 3] = base.data[srcIdx + 3];
|
||||
}
|
||||
}
|
||||
core::Logger::getInstance().info("compositeWithRegions: upscaled 256x256 to 512x512");
|
||||
core::Logger::getInstance().debug("compositeWithRegions: upscaled 256x256 to 512x512");
|
||||
} else {
|
||||
composite = base.data;
|
||||
}
|
||||
|
|
@ -748,7 +748,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
|||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||
}
|
||||
|
||||
core::Logger::getInstance().info("compositeWithRegions: region ", regionIdx,
|
||||
core::Logger::getInstance().debug("compositeWithRegions: region ", regionIdx,
|
||||
" at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second);
|
||||
}
|
||||
|
||||
|
|
@ -765,7 +765,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
|||
applyAnisotropicFiltering();
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
core::Logger::getInstance().info("compositeWithRegions: created ", width, "x", height,
|
||||
core::Logger::getInstance().debug("compositeWithRegions: created ", width, "x", height,
|
||||
" texture with ", regionLayers.size(), " equipment regions");
|
||||
compositeCache_[cacheKey] = texId;
|
||||
return texId;
|
||||
|
|
|
|||
|
|
@ -252,6 +252,8 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderVendorWindow(gameHandler);
|
||||
renderTrainerWindow(gameHandler);
|
||||
renderTaxiWindow(gameHandler);
|
||||
renderMailWindow(gameHandler);
|
||||
renderMailComposeWindow(gameHandler);
|
||||
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||
renderMinimapMarkers(gameHandler);
|
||||
renderDeathScreen(gameHandler);
|
||||
|
|
@ -6028,4 +6030,262 @@ void GameScreen::loadSettings() {
|
|||
LOG_INFO("Settings loaded from ", path);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mail Window
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderMailWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isMailboxOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 250, 80), ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Appearing);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Mailbox", &open)) {
|
||||
const auto& inbox = gameHandler.getMailInbox();
|
||||
|
||||
// Top bar: money + compose button
|
||||
uint64_t money = gameHandler.getMoneyCopper();
|
||||
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
||||
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
||||
uint32_t mc = static_cast<uint32_t>(money % 100);
|
||||
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - 100);
|
||||
if (ImGui::Button("Compose")) {
|
||||
mailRecipientBuffer_[0] = '\0';
|
||||
mailSubjectBuffer_[0] = '\0';
|
||||
mailBodyBuffer_[0] = '\0';
|
||||
mailComposeMoney_[0] = 0;
|
||||
mailComposeMoney_[1] = 0;
|
||||
mailComposeMoney_[2] = 0;
|
||||
gameHandler.openMailCompose();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
if (inbox.empty()) {
|
||||
ImGui::TextDisabled("No mail.");
|
||||
} else {
|
||||
// Two-panel layout: left = mail list, right = selected mail detail
|
||||
float listWidth = 220.0f;
|
||||
|
||||
// Left panel - mail list
|
||||
ImGui::BeginChild("MailList", ImVec2(listWidth, 0), true);
|
||||
for (size_t i = 0; i < inbox.size(); ++i) {
|
||||
const auto& mail = inbox[i];
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
bool selected = (gameHandler.getSelectedMailIndex() == static_cast<int>(i));
|
||||
std::string label = mail.subject.empty() ? "(No Subject)" : mail.subject;
|
||||
|
||||
// Unread indicator
|
||||
if (!mail.read) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.5f, 1.0f));
|
||||
}
|
||||
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
gameHandler.setSelectedMailIndex(static_cast<int>(i));
|
||||
// Mark as read
|
||||
if (!mail.read) {
|
||||
gameHandler.mailMarkAsRead(mail.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mail.read) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Sub-info line
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), " From: %s", mail.senderName.c_str());
|
||||
if (mail.money > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), " [G]");
|
||||
}
|
||||
if (!mail.attachments.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]");
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Right panel - selected mail detail
|
||||
ImGui::BeginChild("MailDetail", ImVec2(0, 0), true);
|
||||
int sel = gameHandler.getSelectedMailIndex();
|
||||
if (sel >= 0 && sel < static_cast<int>(inbox.size())) {
|
||||
const auto& mail = inbox[sel];
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s",
|
||||
mail.subject.empty() ? "(No Subject)" : mail.subject.c_str());
|
||||
ImGui::Text("From: %s", mail.senderName.c_str());
|
||||
|
||||
if (mail.messageType == 2) {
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]");
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// Body text
|
||||
if (!mail.body.empty()) {
|
||||
ImGui::TextWrapped("%s", mail.body.c_str());
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// Money
|
||||
if (mail.money > 0) {
|
||||
uint32_t g = mail.money / 10000;
|
||||
uint32_t s = (mail.money / 100) % 100;
|
||||
uint32_t c = mail.money % 100;
|
||||
ImGui::Text("Money: %ug %us %uc", g, s, c);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Take Money")) {
|
||||
gameHandler.mailTakeMoney(mail.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
// COD warning
|
||||
if (mail.cod > 0) {
|
||||
uint32_t g = mail.cod / 10000;
|
||||
uint32_t s = (mail.cod / 100) % 100;
|
||||
uint32_t c = mail.cod % 100;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"COD: %ug %us %uc (you pay this to take items)", g, s, c);
|
||||
}
|
||||
|
||||
// Attachments
|
||||
if (!mail.attachments.empty()) {
|
||||
ImGui::Text("Attachments: %zu", mail.attachments.size());
|
||||
for (size_t j = 0; j < mail.attachments.size(); ++j) {
|
||||
const auto& att = mail.attachments[j];
|
||||
ImGui::PushID(static_cast<int>(j));
|
||||
|
||||
auto* info = gameHandler.getItemInfo(att.itemId);
|
||||
if (info && info->valid) {
|
||||
ImGui::BulletText("%s x%u", info->name.c_str(), att.stackCount);
|
||||
} else {
|
||||
ImGui::BulletText("Item %u x%u", att.itemId, att.stackCount);
|
||||
gameHandler.ensureItemInfo(att.itemId);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Take")) {
|
||||
gameHandler.mailTakeItem(mail.messageId, att.slot);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
||||
// Action buttons
|
||||
if (ImGui::Button("Delete")) {
|
||||
gameHandler.mailDelete(mail.messageId);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (mail.messageType == 0 && ImGui::Button("Reply")) {
|
||||
// Pre-fill compose with sender as recipient
|
||||
strncpy(mailRecipientBuffer_, mail.senderName.c_str(), sizeof(mailRecipientBuffer_) - 1);
|
||||
mailRecipientBuffer_[sizeof(mailRecipientBuffer_) - 1] = '\0';
|
||||
std::string reSubject = "Re: " + mail.subject;
|
||||
strncpy(mailSubjectBuffer_, reSubject.c_str(), sizeof(mailSubjectBuffer_) - 1);
|
||||
mailSubjectBuffer_[sizeof(mailSubjectBuffer_) - 1] = '\0';
|
||||
mailBodyBuffer_[0] = '\0';
|
||||
mailComposeMoney_[0] = 0;
|
||||
mailComposeMoney_[1] = 0;
|
||||
mailComposeMoney_[2] = 0;
|
||||
gameHandler.openMailCompose();
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Select a mail to read.");
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.closeMailbox();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isMailComposeOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, screenH / 2 - 200), ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(380, 400), ImGuiCond_Appearing);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Send Mail", &open)) {
|
||||
ImGui::Text("To:");
|
||||
ImGui::SameLine(60);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##MailTo", mailRecipientBuffer_, sizeof(mailRecipientBuffer_));
|
||||
|
||||
ImGui::Text("Subject:");
|
||||
ImGui::SameLine(60);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##MailSubject", mailSubjectBuffer_, sizeof(mailSubjectBuffer_));
|
||||
|
||||
ImGui::Text("Body:");
|
||||
ImGui::InputTextMultiline("##MailBody", mailBodyBuffer_, sizeof(mailBodyBuffer_),
|
||||
ImVec2(-1, 150));
|
||||
|
||||
ImGui::Text("Money:");
|
||||
ImGui::SameLine(60);
|
||||
ImGui::SetNextItemWidth(60);
|
||||
ImGui::InputInt("##MailGold", &mailComposeMoney_[0], 0, 0);
|
||||
if (mailComposeMoney_[0] < 0) mailComposeMoney_[0] = 0;
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("g");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(40);
|
||||
ImGui::InputInt("##MailSilver", &mailComposeMoney_[1], 0, 0);
|
||||
if (mailComposeMoney_[1] < 0) mailComposeMoney_[1] = 0;
|
||||
if (mailComposeMoney_[1] > 99) mailComposeMoney_[1] = 99;
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("s");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(40);
|
||||
ImGui::InputInt("##MailCopper", &mailComposeMoney_[2], 0, 0);
|
||||
if (mailComposeMoney_[2] < 0) mailComposeMoney_[2] = 0;
|
||||
if (mailComposeMoney_[2] > 99) mailComposeMoney_[2] = 99;
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("c");
|
||||
|
||||
uint32_t totalMoney = static_cast<uint32_t>(mailComposeMoney_[0]) * 10000 +
|
||||
static_cast<uint32_t>(mailComposeMoney_[1]) * 100 +
|
||||
static_cast<uint32_t>(mailComposeMoney_[2]);
|
||||
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: 30c");
|
||||
|
||||
ImGui::Spacing();
|
||||
bool canSend = (strlen(mailRecipientBuffer_) > 0);
|
||||
if (!canSend) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Send", ImVec2(80, 0))) {
|
||||
gameHandler.sendMail(mailRecipientBuffer_, mailSubjectBuffer_,
|
||||
mailBodyBuffer_, totalMoney);
|
||||
}
|
||||
if (!canSend) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
|
||||
gameHandler.closeMailCompose();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.closeMailCompose();
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue