mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix vanilla M2 animations, movement packets, and DBC locale
- Parse vanilla M2 animation tracks (flat arrays with M2Range indices) instead of skipping them, fixing T-pose on all vanilla models - Use C4Quaternion (float[4]) for vanilla bone rotations instead of CompressedQuat (int16[4]) which produced garbage transforms - Fix vanilla M2 attachment struct size (48 bytes, not 40) so weapons attach to correct bones instead of model origin - Route movement packets through expansion-specific packet parsers instead of hardcoded WotLK format, fixing server-side position sync - Fix Spell.dbc field indices for classic/turtle (Name=120, Rank=129, IconID=117) - were pointing to Portuguese locale column (+7 offset) - Change guild roster keybind from J to O (WoW default) - Add guild opcodes for all expansions
This commit is contained in:
parent
60c93fa1e3
commit
22728b461f
16 changed files with 951 additions and 26 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||
"Name": 127, "Tooltip": 154, "Rank": 136
|
||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||
"Name": 120, "Tooltip": 147, "Rank": 129
|
||||
},
|
||||
"ItemDisplayInfo": {
|
||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@
|
|||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"CMSG_GUILD_QUERY": "0x051",
|
||||
"SMSG_GUILD_QUERY_RESPONSE": "0x052",
|
||||
"SMSG_GUILD_INVITE": "0x083",
|
||||
"CMSG_GUILD_REMOVE": "0x08E",
|
||||
"SMSG_GUILD_EVENT": "0x092",
|
||||
"SMSG_GUILD_COMMAND_RESULT": "0x093",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
"CMSG_DUEL_ACCEPTED": "0x16C",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@
|
|||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"CMSG_GUILD_QUERY": "0x051",
|
||||
"SMSG_GUILD_QUERY_RESPONSE": "0x052",
|
||||
"SMSG_GUILD_INVITE": "0x083",
|
||||
"CMSG_GUILD_REMOVE": "0x08E",
|
||||
"SMSG_GUILD_EVENT": "0x092",
|
||||
"SMSG_GUILD_COMMAND_RESULT": "0x093",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"MSG_RAID_READY_CHECK_CONFIRM": "0x3AE",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||
"Name": 127, "Tooltip": 154, "Rank": 136
|
||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||
"Name": 120, "Tooltip": 147, "Rank": 129
|
||||
},
|
||||
"ItemDisplayInfo": {
|
||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@
|
|||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"CMSG_GUILD_QUERY": "0x051",
|
||||
"SMSG_GUILD_QUERY_RESPONSE": "0x052",
|
||||
"SMSG_GUILD_INVITE": "0x083",
|
||||
"CMSG_GUILD_REMOVE": "0x08E",
|
||||
"SMSG_GUILD_EVENT": "0x092",
|
||||
"SMSG_GUILD_COMMAND_RESULT": "0x093",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
"CMSG_DUEL_ACCEPTED": "0x16C",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@
|
|||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"CMSG_GUILD_QUERY": "0x051",
|
||||
"SMSG_GUILD_QUERY_RESPONSE": "0x052",
|
||||
"SMSG_GUILD_INVITE": "0x083",
|
||||
"CMSG_GUILD_REMOVE": "0x08E",
|
||||
"SMSG_GUILD_EVENT": "0x092",
|
||||
"SMSG_GUILD_COMMAND_RESULT": "0x093",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"MSG_RAID_READY_CHECK_CONFIRM": "0x3AE",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
|
|
|
|||
|
|
@ -314,6 +314,19 @@ public:
|
|||
void demoteGuildMember(const std::string& playerName);
|
||||
void leaveGuild();
|
||||
void inviteToGuild(const std::string& playerName);
|
||||
void kickGuildMember(const std::string& playerName);
|
||||
void acceptGuildInvite();
|
||||
void declineGuildInvite();
|
||||
void queryGuildInfo(uint32_t guildId);
|
||||
|
||||
// Guild state accessors
|
||||
bool isInGuild() const { return !guildName_.empty(); }
|
||||
const std::string& getGuildName() const { return guildName_; }
|
||||
const GuildRosterData& getGuildRoster() const { return guildRoster_; }
|
||||
bool hasGuildRoster() const { return hasGuildRoster_; }
|
||||
bool hasPendingGuildInvite() const { return pendingGuildInvite_; }
|
||||
const std::string& getPendingGuildInviterName() const { return pendingGuildInviterName_; }
|
||||
const std::string& getPendingGuildInviteGuildName() const { return pendingGuildInviteGuildName_; }
|
||||
|
||||
// Ready check
|
||||
void initiateReadyCheck();
|
||||
|
|
@ -878,6 +891,14 @@ private:
|
|||
void handleGroupUninvite(network::Packet& packet);
|
||||
void handlePartyCommandResult(network::Packet& packet);
|
||||
|
||||
// ---- Guild handlers ----
|
||||
void handleGuildInfo(network::Packet& packet);
|
||||
void handleGuildRoster(network::Packet& packet);
|
||||
void handleGuildQueryResponse(network::Packet& packet);
|
||||
void handleGuildEvent(network::Packet& packet);
|
||||
void handleGuildInvite(network::Packet& packet);
|
||||
void handleGuildCommandResult(network::Packet& packet);
|
||||
|
||||
// ---- Character creation handler ----
|
||||
void handleCharCreateResponse(network::Packet& packet);
|
||||
|
||||
|
|
@ -1155,6 +1176,15 @@ private:
|
|||
bool pendingGroupInvite = false;
|
||||
std::string pendingInviterName;
|
||||
|
||||
// ---- Guild state ----
|
||||
std::string guildName_;
|
||||
std::vector<std::string> guildRankNames_;
|
||||
GuildRosterData guildRoster_;
|
||||
bool hasGuildRoster_ = false;
|
||||
bool pendingGuildInvite_ = false;
|
||||
std::string pendingGuildInviterName_;
|
||||
std::string pendingGuildInviteGuildName_;
|
||||
|
||||
uint64_t activeCharacterGuid_ = 0;
|
||||
Race playerRace_ = Race::HUMAN;
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,18 @@ public:
|
|||
return DestroyObjectParser::parse(packet, data);
|
||||
}
|
||||
|
||||
// --- Guild ---
|
||||
|
||||
/** Parse SMSG_GUILD_ROSTER */
|
||||
virtual bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) {
|
||||
return GuildRosterParser::parse(packet, data);
|
||||
}
|
||||
|
||||
/** Parse SMSG_GUILD_QUERY_RESPONSE */
|
||||
virtual bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) {
|
||||
return GuildQueryResponseParser::parse(packet, data);
|
||||
}
|
||||
|
||||
// --- Utility ---
|
||||
|
||||
/** Read a packed GUID from the packet */
|
||||
|
|
@ -190,6 +202,8 @@ public:
|
|||
const MovementInfo& info,
|
||||
uint64_t playerGuid = 0) override;
|
||||
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
|
||||
bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override;
|
||||
bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -880,6 +880,166 @@ public:
|
|||
static network::Packet build(const std::string& playerName);
|
||||
};
|
||||
|
||||
/** CMSG_GUILD_QUERY packet builder */
|
||||
class GuildQueryPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t guildId);
|
||||
};
|
||||
|
||||
/** CMSG_GUILD_REMOVE packet builder */
|
||||
class GuildRemovePacket {
|
||||
public:
|
||||
static network::Packet build(const std::string& playerName);
|
||||
};
|
||||
|
||||
/** CMSG_GUILD_ACCEPT packet builder (empty body) */
|
||||
class GuildAcceptPacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** CMSG_GUILD_DECLINE_INVITATION packet builder (empty body) */
|
||||
class GuildDeclineInvitationPacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
// Guild event type constants
|
||||
namespace GuildEvent {
|
||||
constexpr uint8_t PROMOTION = 0;
|
||||
constexpr uint8_t DEMOTION = 1;
|
||||
constexpr uint8_t MOTD = 2;
|
||||
constexpr uint8_t JOINED = 3;
|
||||
constexpr uint8_t LEFT = 4;
|
||||
constexpr uint8_t REMOVED = 5;
|
||||
constexpr uint8_t LEADER_IS = 6;
|
||||
constexpr uint8_t LEADER_CHANGED = 7;
|
||||
constexpr uint8_t DISBANDED = 8;
|
||||
constexpr uint8_t SIGNED_ON = 14;
|
||||
constexpr uint8_t SIGNED_OFF = 15;
|
||||
}
|
||||
|
||||
/** SMSG_GUILD_QUERY_RESPONSE data */
|
||||
struct GuildQueryResponseData {
|
||||
uint32_t guildId = 0;
|
||||
std::string guildName;
|
||||
std::string rankNames[10];
|
||||
uint32_t emblemStyle = 0;
|
||||
uint32_t emblemColor = 0;
|
||||
uint32_t borderStyle = 0;
|
||||
uint32_t borderColor = 0;
|
||||
uint32_t backgroundColor = 0;
|
||||
uint32_t rankCount = 0;
|
||||
|
||||
bool isValid() const { return guildId != 0 && !guildName.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_QUERY_RESPONSE parser */
|
||||
class GuildQueryResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GuildQueryResponseData& data);
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_INFO data */
|
||||
struct GuildInfoData {
|
||||
std::string guildName;
|
||||
uint32_t creationDay = 0;
|
||||
uint32_t creationMonth = 0;
|
||||
uint32_t creationYear = 0;
|
||||
uint32_t numMembers = 0;
|
||||
uint32_t numAccounts = 0;
|
||||
|
||||
bool isValid() const { return !guildName.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_INFO parser */
|
||||
class GuildInfoParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GuildInfoData& data);
|
||||
};
|
||||
|
||||
/** Guild roster member entry */
|
||||
struct GuildRosterMember {
|
||||
uint64_t guid = 0;
|
||||
bool online = false;
|
||||
std::string name;
|
||||
uint32_t rankIndex = 0;
|
||||
uint8_t level = 0;
|
||||
uint8_t classId = 0;
|
||||
uint8_t gender = 0;
|
||||
uint32_t zoneId = 0;
|
||||
float lastOnline = 0.0f;
|
||||
std::string publicNote;
|
||||
std::string officerNote;
|
||||
};
|
||||
|
||||
/** Guild rank info */
|
||||
struct GuildRankInfo {
|
||||
uint32_t rights = 0;
|
||||
uint32_t goldLimit = 0;
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_ROSTER data */
|
||||
struct GuildRosterData {
|
||||
std::string motd;
|
||||
std::string guildInfo;
|
||||
std::vector<GuildRankInfo> ranks;
|
||||
std::vector<GuildRosterMember> members;
|
||||
|
||||
bool isEmpty() const { return members.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_ROSTER parser */
|
||||
class GuildRosterParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GuildRosterData& data);
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_EVENT data */
|
||||
struct GuildEventData {
|
||||
uint8_t eventType = 0;
|
||||
uint8_t numStrings = 0;
|
||||
std::string strings[3];
|
||||
uint64_t guid = 0;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_EVENT parser */
|
||||
class GuildEventParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GuildEventData& data);
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_INVITE data */
|
||||
struct GuildInviteResponseData {
|
||||
std::string inviterName;
|
||||
std::string guildName;
|
||||
|
||||
bool isValid() const { return !inviterName.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_INVITE parser */
|
||||
class GuildInviteResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GuildInviteResponseData& data);
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_COMMAND_RESULT data */
|
||||
struct GuildCommandResultData {
|
||||
uint32_t command = 0;
|
||||
std::string name;
|
||||
uint32_t errorCode = 0;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
/** SMSG_GUILD_COMMAND_RESULT parser */
|
||||
class GuildCommandResultParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GuildCommandResultData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Ready Check
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ private:
|
|||
bool showEntityWindow = false;
|
||||
bool showChatWindow = true;
|
||||
bool showPlayerInfo = false;
|
||||
bool showGuildRoster_ = false;
|
||||
bool refocusChatInput = false;
|
||||
bool chatWindowLocked = true;
|
||||
ImVec2 chatWindowPos_ = ImVec2(0.0f, 0.0f);
|
||||
|
|
@ -166,6 +167,8 @@ private:
|
|||
void renderSettingsWindow();
|
||||
void renderQuestMarkers(game::GameHandler& gameHandler);
|
||||
void renderMinimapMarkers(game::GameHandler& gameHandler);
|
||||
void renderGuildRoster(game::GameHandler& gameHandler);
|
||||
void renderGuildInvitePopup(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
|
|
|
|||
|
|
@ -879,6 +879,26 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handlePartyCommandResult(packet);
|
||||
break;
|
||||
|
||||
// ---- Guild ----
|
||||
case Opcode::SMSG_GUILD_INFO:
|
||||
handleGuildInfo(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GUILD_ROSTER:
|
||||
handleGuildRoster(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GUILD_QUERY_RESPONSE:
|
||||
handleGuildQueryResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GUILD_EVENT:
|
||||
handleGuildEvent(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GUILD_INVITE:
|
||||
handleGuildInvite(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GUILD_COMMAND_RESULT:
|
||||
handleGuildCommandResult(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 5: Loot/Gossip/Vendor ----
|
||||
case Opcode::SMSG_LOOT_RESPONSE:
|
||||
handleLootResponse(packet);
|
||||
|
|
@ -1914,6 +1934,16 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
worldEntryCallback_(data.mapId, data.x, data.y, data.z);
|
||||
}
|
||||
|
||||
// Auto-query guild info on login
|
||||
const Character* activeChar = getActiveCharacter();
|
||||
if (activeChar && activeChar->hasGuild() && socket) {
|
||||
auto gqPacket = GuildQueryPacket::build(activeChar->guildId);
|
||||
socket->send(gqPacket);
|
||||
auto grPacket = GuildRosterPacket::build();
|
||||
socket->send(grPacket);
|
||||
LOG_INFO("Auto-queried guild info (guildId=", activeChar->guildId, ")");
|
||||
}
|
||||
|
||||
// If we disconnected mid-taxi, attempt to recover to destination after login.
|
||||
if (taxiRecoverPending_ && taxiRecoverMapId_ == data.mapId) {
|
||||
float dx = movementInfo.x - taxiRecoverPos_.x;
|
||||
|
|
@ -2629,8 +2659,10 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
wireInfo.transportO = core::coords::normalizeAngleRad(-wireInfo.transportO);
|
||||
}
|
||||
|
||||
// Build and send movement packet
|
||||
auto packet = MovementPacket::build(opcode, wireInfo, playerGuid);
|
||||
// Build and send movement packet (expansion-specific format)
|
||||
auto packet = packetParsers_
|
||||
? packetParsers_->buildMovementPacket(opcode, wireInfo, playerGuid)
|
||||
: MovementPacket::build(opcode, wireInfo, playerGuid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
|
|
@ -6481,6 +6513,161 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Guild Handlers
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::kickGuildMember(const std::string& playerName) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GuildRemovePacket::build(playerName);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Kicking guild member: ", playerName);
|
||||
}
|
||||
|
||||
void GameHandler::acceptGuildInvite() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingGuildInvite_ = false;
|
||||
auto packet = GuildAcceptPacket::build();
|
||||
socket->send(packet);
|
||||
LOG_INFO("Accepted guild invite");
|
||||
}
|
||||
|
||||
void GameHandler::declineGuildInvite() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingGuildInvite_ = false;
|
||||
auto packet = GuildDeclineInvitationPacket::build();
|
||||
socket->send(packet);
|
||||
LOG_INFO("Declined guild invite");
|
||||
}
|
||||
|
||||
void GameHandler::queryGuildInfo(uint32_t guildId) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GuildQueryPacket::build(guildId);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Querying guild info: guildId=", guildId);
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildInfo(network::Packet& packet) {
|
||||
GuildInfoData data;
|
||||
if (!GuildInfoParser::parse(packet, data)) return;
|
||||
|
||||
addSystemChatMessage("Guild: " + data.guildName + " (" +
|
||||
std::to_string(data.numMembers) + " members, " +
|
||||
std::to_string(data.numAccounts) + " accounts)");
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildRoster(network::Packet& packet) {
|
||||
GuildRosterData data;
|
||||
if (!packetParsers_->parseGuildRoster(packet, data)) return;
|
||||
|
||||
guildRoster_ = std::move(data);
|
||||
hasGuildRoster_ = true;
|
||||
LOG_INFO("Guild roster received: ", guildRoster_.members.size(), " members");
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildQueryResponse(network::Packet& packet) {
|
||||
GuildQueryResponseData data;
|
||||
if (!packetParsers_->parseGuildQueryResponse(packet, data)) return;
|
||||
|
||||
guildName_ = data.guildName;
|
||||
guildRankNames_.clear();
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
guildRankNames_.push_back(data.rankNames[i]);
|
||||
}
|
||||
LOG_INFO("Guild name set to: ", guildName_);
|
||||
addSystemChatMessage("Guild: <" + guildName_ + ">");
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildEvent(network::Packet& packet) {
|
||||
GuildEventData data;
|
||||
if (!GuildEventParser::parse(packet, data)) return;
|
||||
|
||||
std::string msg;
|
||||
switch (data.eventType) {
|
||||
case GuildEvent::PROMOTION:
|
||||
if (data.numStrings >= 3)
|
||||
msg = data.strings[0] + " has promoted " + data.strings[1] + " to " + data.strings[2] + ".";
|
||||
break;
|
||||
case GuildEvent::DEMOTION:
|
||||
if (data.numStrings >= 3)
|
||||
msg = data.strings[0] + " has demoted " + data.strings[1] + " to " + data.strings[2] + ".";
|
||||
break;
|
||||
case GuildEvent::MOTD:
|
||||
if (data.numStrings >= 1)
|
||||
msg = "Guild MOTD: " + data.strings[0];
|
||||
break;
|
||||
case GuildEvent::JOINED:
|
||||
if (data.numStrings >= 1)
|
||||
msg = data.strings[0] + " has joined the guild.";
|
||||
break;
|
||||
case GuildEvent::LEFT:
|
||||
if (data.numStrings >= 1)
|
||||
msg = data.strings[0] + " has left the guild.";
|
||||
break;
|
||||
case GuildEvent::REMOVED:
|
||||
if (data.numStrings >= 2)
|
||||
msg = data.strings[1] + " has been kicked from the guild by " + data.strings[0] + ".";
|
||||
break;
|
||||
case GuildEvent::LEADER_IS:
|
||||
if (data.numStrings >= 1)
|
||||
msg = data.strings[0] + " is the guild leader.";
|
||||
break;
|
||||
case GuildEvent::LEADER_CHANGED:
|
||||
if (data.numStrings >= 2)
|
||||
msg = data.strings[0] + " has made " + data.strings[1] + " the new guild leader.";
|
||||
break;
|
||||
case GuildEvent::DISBANDED:
|
||||
msg = "Guild has been disbanded.";
|
||||
guildName_.clear();
|
||||
guildRankNames_.clear();
|
||||
guildRoster_ = GuildRosterData{};
|
||||
hasGuildRoster_ = false;
|
||||
break;
|
||||
case GuildEvent::SIGNED_ON:
|
||||
if (data.numStrings >= 1)
|
||||
msg = "[Guild] " + data.strings[0] + " has come online.";
|
||||
break;
|
||||
case GuildEvent::SIGNED_OFF:
|
||||
if (data.numStrings >= 1)
|
||||
msg = "[Guild] " + data.strings[0] + " has gone offline.";
|
||||
break;
|
||||
default:
|
||||
msg = "Guild event " + std::to_string(data.eventType);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!msg.empty()) {
|
||||
MessageChatData chatMsg;
|
||||
chatMsg.type = ChatType::GUILD;
|
||||
chatMsg.language = ChatLanguage::UNIVERSAL;
|
||||
chatMsg.message = msg;
|
||||
addLocalChatMessage(chatMsg);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildInvite(network::Packet& packet) {
|
||||
GuildInviteResponseData data;
|
||||
if (!GuildInviteResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingGuildInvite_ = true;
|
||||
pendingGuildInviterName_ = data.inviterName;
|
||||
pendingGuildInviteGuildName_ = data.guildName;
|
||||
LOG_INFO("Guild invite from: ", data.inviterName, " to guild: ", data.guildName);
|
||||
addSystemChatMessage(data.inviterName + " has invited you to join " + data.guildName + ".");
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildCommandResult(network::Packet& packet) {
|
||||
GuildCommandResultData data;
|
||||
if (!GuildCommandResultParser::parse(packet, data)) return;
|
||||
|
||||
if (data.errorCode != 0) {
|
||||
std::string msg = "Guild command failed";
|
||||
if (!data.name.empty()) msg += " for " + data.name;
|
||||
msg += " (error " + std::to_string(data.errorCode) + ")";
|
||||
addSystemChatMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot, Gossip, Vendor
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -378,6 +378,12 @@ void OpcodeTable::loadWotlkDefaults() {
|
|||
{LogicalOpcode::CMSG_GUILD_MOTD, 0x091},
|
||||
{LogicalOpcode::SMSG_GUILD_INFO, 0x088},
|
||||
{LogicalOpcode::SMSG_GUILD_ROSTER, 0x08A},
|
||||
{LogicalOpcode::CMSG_GUILD_QUERY, 0x051},
|
||||
{LogicalOpcode::SMSG_GUILD_QUERY_RESPONSE, 0x052},
|
||||
{LogicalOpcode::SMSG_GUILD_INVITE, 0x083},
|
||||
{LogicalOpcode::CMSG_GUILD_REMOVE, 0x08E},
|
||||
{LogicalOpcode::SMSG_GUILD_EVENT, 0x092},
|
||||
{LogicalOpcode::SMSG_GUILD_COMMAND_RESULT, 0x093},
|
||||
{LogicalOpcode::MSG_RAID_READY_CHECK, 0x322},
|
||||
{LogicalOpcode::MSG_RAID_READY_CHECK_CONFIRM, 0x3AE},
|
||||
{LogicalOpcode::CMSG_DUEL_PROPOSED, 0x166},
|
||||
|
|
|
|||
|
|
@ -444,5 +444,78 @@ bool ClassicPacketParsers::parseMessageChat(network::Packet& packet, MessageChat
|
|||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic guild roster parser
|
||||
// Differences from WotLK:
|
||||
// - No rankCount field (fixed 10 ranks, read rights only)
|
||||
// - No per-rank bank tab data
|
||||
// - No gender byte per member
|
||||
// ============================================================================
|
||||
|
||||
bool ClassicPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData& data) {
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("Classic SMSG_GUILD_ROSTER too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
uint32_t numMembers = packet.readUInt32();
|
||||
data.motd = packet.readString();
|
||||
data.guildInfo = packet.readString();
|
||||
|
||||
// Classic: fixed 10 ranks, just uint32 rights each (no goldLimit, no bank tabs)
|
||||
data.ranks.resize(10);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
data.ranks[i].rights = packet.readUInt32();
|
||||
data.ranks[i].goldLimit = 0;
|
||||
}
|
||||
|
||||
data.members.resize(numMembers);
|
||||
for (uint32_t i = 0; i < numMembers; ++i) {
|
||||
auto& m = data.members[i];
|
||||
m.guid = packet.readUInt64();
|
||||
m.online = (packet.readUInt8() != 0);
|
||||
m.name = packet.readString();
|
||||
m.rankIndex = packet.readUInt32();
|
||||
m.level = packet.readUInt8();
|
||||
m.classId = packet.readUInt8();
|
||||
// Classic: NO gender byte
|
||||
m.gender = 0;
|
||||
m.zoneId = packet.readUInt32();
|
||||
if (!m.online) {
|
||||
m.lastOnline = packet.readFloat();
|
||||
}
|
||||
m.publicNote = packet.readString();
|
||||
m.officerNote = packet.readString();
|
||||
}
|
||||
LOG_INFO("Parsed Classic SMSG_GUILD_ROSTER: ", numMembers, " members");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic guild query response parser
|
||||
// Differences from WotLK:
|
||||
// - No trailing rankCount uint32
|
||||
// ============================================================================
|
||||
|
||||
bool ClassicPacketParsers::parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) {
|
||||
if (packet.getSize() < 8) {
|
||||
LOG_ERROR("Classic SMSG_GUILD_QUERY_RESPONSE too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
data.guildId = packet.readUInt32();
|
||||
data.guildName = packet.readString();
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
data.rankNames[i] = packet.readString();
|
||||
}
|
||||
data.emblemStyle = packet.readUInt32();
|
||||
data.emblemColor = packet.readUInt32();
|
||||
data.borderStyle = packet.readUInt32();
|
||||
data.borderColor = packet.readUInt32();
|
||||
data.backgroundColor = packet.readUInt32();
|
||||
// Classic: NO trailing rankCount
|
||||
data.rankCount = 10;
|
||||
LOG_INFO("Parsed Classic SMSG_GUILD_QUERY_RESPONSE: guild=", data.guildName);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -1531,6 +1531,151 @@ network::Packet GuildInvitePacket::build(const std::string& playerName) {
|
|||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildQueryPacket::build(uint32_t guildId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_QUERY));
|
||||
packet.writeUInt32(guildId);
|
||||
LOG_DEBUG("Built CMSG_GUILD_QUERY: guildId=", guildId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildRemovePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_REMOVE));
|
||||
packet.writeString(playerName);
|
||||
LOG_DEBUG("Built CMSG_GUILD_REMOVE: ", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildAcceptPacket::build() {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ACCEPT));
|
||||
LOG_DEBUG("Built CMSG_GUILD_ACCEPT");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildDeclineInvitationPacket::build() {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DECLINE_INVITATION));
|
||||
LOG_DEBUG("Built CMSG_GUILD_DECLINE_INVITATION");
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponseData& data) {
|
||||
if (packet.getSize() < 8) {
|
||||
LOG_ERROR("SMSG_GUILD_QUERY_RESPONSE too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
data.guildId = packet.readUInt32();
|
||||
data.guildName = packet.readString();
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
data.rankNames[i] = packet.readString();
|
||||
}
|
||||
data.emblemStyle = packet.readUInt32();
|
||||
data.emblemColor = packet.readUInt32();
|
||||
data.borderStyle = packet.readUInt32();
|
||||
data.borderColor = packet.readUInt32();
|
||||
data.backgroundColor = packet.readUInt32();
|
||||
if ((packet.getSize() - packet.getReadPos()) >= 4) {
|
||||
data.rankCount = packet.readUInt32();
|
||||
}
|
||||
LOG_INFO("Parsed SMSG_GUILD_QUERY_RESPONSE: guild=", data.guildName, " id=", data.guildId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GuildInfoParser::parse(network::Packet& packet, GuildInfoData& data) {
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("SMSG_GUILD_INFO too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
data.guildName = packet.readString();
|
||||
data.creationDay = packet.readUInt32();
|
||||
data.creationMonth = packet.readUInt32();
|
||||
data.creationYear = packet.readUInt32();
|
||||
data.numMembers = packet.readUInt32();
|
||||
data.numAccounts = packet.readUInt32();
|
||||
LOG_INFO("Parsed SMSG_GUILD_INFO: ", data.guildName, " members=", data.numMembers);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) {
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("SMSG_GUILD_ROSTER too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
uint32_t numMembers = packet.readUInt32();
|
||||
data.motd = packet.readString();
|
||||
data.guildInfo = packet.readString();
|
||||
|
||||
uint32_t rankCount = packet.readUInt32();
|
||||
data.ranks.resize(rankCount);
|
||||
for (uint32_t i = 0; i < rankCount; ++i) {
|
||||
data.ranks[i].rights = packet.readUInt32();
|
||||
data.ranks[i].goldLimit = packet.readUInt32();
|
||||
// 6 bank tab flags + 6 bank tab items per day
|
||||
for (int t = 0; t < 6; ++t) {
|
||||
packet.readUInt32(); // tabFlags
|
||||
packet.readUInt32(); // tabItemsPerDay
|
||||
}
|
||||
}
|
||||
|
||||
data.members.resize(numMembers);
|
||||
for (uint32_t i = 0; i < numMembers; ++i) {
|
||||
auto& m = data.members[i];
|
||||
m.guid = packet.readUInt64();
|
||||
m.online = (packet.readUInt8() != 0);
|
||||
m.name = packet.readString();
|
||||
m.rankIndex = packet.readUInt32();
|
||||
m.level = packet.readUInt8();
|
||||
m.classId = packet.readUInt8();
|
||||
m.gender = packet.readUInt8();
|
||||
m.zoneId = packet.readUInt32();
|
||||
if (!m.online) {
|
||||
m.lastOnline = packet.readFloat();
|
||||
}
|
||||
m.publicNote = packet.readString();
|
||||
m.officerNote = packet.readString();
|
||||
}
|
||||
LOG_INFO("Parsed SMSG_GUILD_ROSTER: ", numMembers, " members, motd=", data.motd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GuildEventParser::parse(network::Packet& packet, GuildEventData& data) {
|
||||
if (packet.getSize() < 2) {
|
||||
LOG_ERROR("SMSG_GUILD_EVENT too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
data.eventType = packet.readUInt8();
|
||||
data.numStrings = packet.readUInt8();
|
||||
for (uint8_t i = 0; i < data.numStrings && i < 3; ++i) {
|
||||
data.strings[i] = packet.readString();
|
||||
}
|
||||
if ((packet.getSize() - packet.getReadPos()) >= 8) {
|
||||
data.guid = packet.readUInt64();
|
||||
}
|
||||
LOG_INFO("Parsed SMSG_GUILD_EVENT: type=", (int)data.eventType, " strings=", (int)data.numStrings);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GuildInviteResponseParser::parse(network::Packet& packet, GuildInviteResponseData& data) {
|
||||
if (packet.getSize() < 2) {
|
||||
LOG_ERROR("SMSG_GUILD_INVITE too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
data.inviterName = packet.readString();
|
||||
data.guildName = packet.readString();
|
||||
LOG_INFO("Parsed SMSG_GUILD_INVITE: from=", data.inviterName, " guild=", data.guildName);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GuildCommandResultParser::parse(network::Packet& packet, GuildCommandResultData& data) {
|
||||
if (packet.getSize() < 8) {
|
||||
LOG_ERROR("SMSG_GUILD_COMMAND_RESULT too small: ", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
data.command = packet.readUInt32();
|
||||
data.name = packet.readString();
|
||||
data.errorCode = packet.readUInt32();
|
||||
LOG_INFO("Parsed SMSG_GUILD_COMMAND_RESULT: cmd=", data.command, " error=", data.errorCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Ready Check
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -312,13 +312,29 @@ struct M2TextureTransformDisk {
|
|||
M2TrackDisk scaling; // 20
|
||||
};
|
||||
|
||||
// M2 attachment point (on-disk)
|
||||
// Vanilla M2 texture transform (3 × 28-byte tracks = 84 bytes)
|
||||
struct M2TextureTransformDiskVanilla {
|
||||
M2TrackDiskVanilla translation; // 28
|
||||
M2TrackDiskVanilla rotation; // 28
|
||||
M2TrackDiskVanilla scaling; // 28
|
||||
};
|
||||
|
||||
// M2 attachment point (on-disk, WotLK — 40 bytes)
|
||||
struct M2AttachmentDisk {
|
||||
uint32_t id;
|
||||
uint16_t bone;
|
||||
uint16_t unknown;
|
||||
float position[3];
|
||||
uint8_t trackData[20]; // M2Track<uint8_t> — skip
|
||||
uint8_t trackData[20]; // M2TrackDisk (20 bytes)
|
||||
};
|
||||
|
||||
// M2 attachment point (on-disk, vanilla — 48 bytes, track is 28 bytes)
|
||||
struct M2AttachmentDiskVanilla {
|
||||
uint32_t id;
|
||||
uint16_t bone;
|
||||
uint16_t unknown;
|
||||
float position[3];
|
||||
uint8_t trackData[28]; // M2TrackDiskVanilla (28 bytes)
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
|
|
@ -454,6 +470,96 @@ void parseAnimTrack(const std::vector<uint8_t>& data,
|
|||
}
|
||||
}
|
||||
|
||||
// Vanilla M2 range: indices into flat timestamp/key arrays for a given sequence
|
||||
struct M2Range { uint32_t start; uint32_t end; };
|
||||
|
||||
// Parse a vanilla M2 animation track (version < 264).
|
||||
// Vanilla uses flat arrays with per-sequence M2Range indices, unlike WotLK's array-of-arrays.
|
||||
// Vanilla also uses Quaternion16 (simple x/32767) instead of WotLK's CompressedQuaternion.
|
||||
void parseAnimTrackVanilla(const std::vector<uint8_t>& data,
|
||||
const M2TrackDiskVanilla& disk,
|
||||
M2AnimationTrack& track,
|
||||
TrackType type) {
|
||||
track.interpolationType = disk.interpolationType;
|
||||
track.globalSequence = disk.globalSequence;
|
||||
|
||||
if (disk.nTimestamps == 0 || disk.nKeys == 0) return;
|
||||
// Sanity caps
|
||||
if (disk.nTimestamps > 100000 || disk.nKeys > 100000) return;
|
||||
|
||||
// Validate flat timestamp array
|
||||
if (disk.ofsTimestamps + disk.nTimestamps * sizeof(uint32_t) > data.size()) return;
|
||||
auto allTimestamps = readArray<uint32_t>(data, disk.ofsTimestamps, disk.nTimestamps);
|
||||
|
||||
// Validate flat key array
|
||||
// Vanilla stores rotations as full float quaternions (16 bytes), NOT compressed int16 (8 bytes)
|
||||
size_t keySize;
|
||||
if (type == TrackType::FLOAT) keySize = sizeof(float);
|
||||
else if (type == TrackType::VEC3) keySize = sizeof(float) * 3;
|
||||
else keySize = sizeof(float) * 4; // C4Quaternion (float[4]) in vanilla
|
||||
if (disk.ofsKeys + disk.nKeys * keySize > data.size()) return;
|
||||
|
||||
// Read per-sequence ranges
|
||||
std::vector<M2Range> ranges;
|
||||
if (disk.nRanges > 0 && disk.ofsRanges > 0 &&
|
||||
disk.nRanges < 4096 &&
|
||||
disk.ofsRanges + disk.nRanges * sizeof(M2Range) <= data.size()) {
|
||||
ranges = readArray<M2Range>(data, disk.ofsRanges, disk.nRanges);
|
||||
}
|
||||
|
||||
// If no ranges, treat entire array as one sequence
|
||||
if (ranges.empty()) {
|
||||
ranges.push_back({0, disk.nTimestamps});
|
||||
}
|
||||
|
||||
track.sequences.resize(ranges.size());
|
||||
|
||||
for (size_t i = 0; i < ranges.size(); i++) {
|
||||
uint32_t start = ranges[i].start;
|
||||
uint32_t end = ranges[i].end;
|
||||
if (start >= end || start >= disk.nTimestamps) continue;
|
||||
end = std::min(end, disk.nTimestamps);
|
||||
|
||||
// Copy timestamps for this sequence
|
||||
track.sequences[i].timestamps.assign(
|
||||
allTimestamps.begin() + start, allTimestamps.begin() + end);
|
||||
|
||||
// Copy key values for this sequence
|
||||
if (start >= disk.nKeys) continue;
|
||||
uint32_t keyEnd = std::min(end, disk.nKeys);
|
||||
uint32_t keyCount = keyEnd - start;
|
||||
|
||||
if (type == TrackType::FLOAT) {
|
||||
auto allValues = readArray<float>(data, disk.ofsKeys, disk.nKeys);
|
||||
track.sequences[i].floatValues.assign(
|
||||
allValues.begin() + start, allValues.begin() + start + keyCount);
|
||||
} else if (type == TrackType::VEC3) {
|
||||
struct Vec3Disk { float x, y, z; };
|
||||
auto allValues = readArray<Vec3Disk>(data, disk.ofsKeys, disk.nKeys);
|
||||
track.sequences[i].vec3Values.reserve(keyCount);
|
||||
for (uint32_t k = start; k < start + keyCount; k++) {
|
||||
track.sequences[i].vec3Values.emplace_back(
|
||||
allValues[k].x, allValues[k].y, allValues[k].z);
|
||||
}
|
||||
} else {
|
||||
// Vanilla: C4Quaternion — full float[4] per key (XYZW on disk)
|
||||
// NOT compressed int16 like WotLK
|
||||
struct C4Quaternion { float x, y, z, w; };
|
||||
auto allQ = readArray<C4Quaternion>(data, disk.ofsKeys, disk.nKeys);
|
||||
track.sequences[i].quatValues.reserve(keyCount);
|
||||
for (uint32_t k = start; k < start + keyCount; k++) {
|
||||
const auto& fq = allQ[k];
|
||||
// Disk order: XYZW, glm::quat constructor: (w, x, y, z)
|
||||
glm::quat q(fq.w, fq.x, fq.y, fq.z);
|
||||
float len = glm::length(q);
|
||||
if (len > 0.001f) q = q / len;
|
||||
else q = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
track.sequences[i].quatValues.push_back(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse an FBlock (particle lifetime curve) from a 16-byte on-disk header.
|
||||
// FBlocks are like M2Track but WITHOUT the interpolationType/globalSequence prefix.
|
||||
void parseFBlock(const std::vector<uint8_t>& data, uint32_t offset,
|
||||
|
|
@ -803,11 +909,17 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
scale = db.scale;
|
||||
}
|
||||
|
||||
// Parse animation tracks (skip for vanilla — flat array format differs from WotLK)
|
||||
// Parse animation tracks
|
||||
if (header.version >= 264) {
|
||||
parseAnimTrack(m2Data, translation, bone.translation, TrackType::VEC3, seqFlags);
|
||||
parseAnimTrack(m2Data, rotation, bone.rotation, TrackType::QUAT_COMPRESSED, seqFlags);
|
||||
parseAnimTrack(m2Data, scale, bone.scale, TrackType::VEC3, seqFlags);
|
||||
} else {
|
||||
// Vanilla: flat array format with per-sequence ranges + Quaternion16
|
||||
M2BoneDiskVanilla dbv = readValue<M2BoneDiskVanilla>(m2Data, boneOffset);
|
||||
parseAnimTrackVanilla(m2Data, dbv.translation, bone.translation, TrackType::VEC3);
|
||||
parseAnimTrackVanilla(m2Data, dbv.rotation, bone.rotation, TrackType::QUAT_COMPRESSED);
|
||||
parseAnimTrackVanilla(m2Data, dbv.scale, bone.scale, TrackType::VEC3);
|
||||
}
|
||||
|
||||
if (bone.translation.hasData() || bone.rotation.hasData() || bone.scale.hasData()) {
|
||||
|
|
@ -860,8 +972,8 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
core::Logger::getInstance().debug(" Materials: ", model.materials.size());
|
||||
}
|
||||
|
||||
// Read texture transforms (UV animation data) — skip for vanilla (different track format)
|
||||
if (header.nUVAnimation > 0 && header.ofsUVAnimation > 0 && header.version >= 264) {
|
||||
// Read texture transforms (UV animation data)
|
||||
if (header.nUVAnimation > 0 && header.ofsUVAnimation > 0) {
|
||||
// Build per-sequence flags for skipping external .anim data
|
||||
std::vector<uint32_t> seqFlags;
|
||||
seqFlags.reserve(model.sequences.size());
|
||||
|
|
@ -869,16 +981,27 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
seqFlags.push_back(seq.flags);
|
||||
}
|
||||
|
||||
size_t uvStructSize = (header.version >= 264)
|
||||
? sizeof(M2TextureTransformDisk)
|
||||
: sizeof(M2TextureTransformDiskVanilla);
|
||||
|
||||
model.textureTransforms.reserve(header.nUVAnimation);
|
||||
for (uint32_t i = 0; i < header.nUVAnimation; i++) {
|
||||
uint32_t ofs = header.ofsUVAnimation + i * sizeof(M2TextureTransformDisk);
|
||||
if (ofs + sizeof(M2TextureTransformDisk) > m2Data.size()) break;
|
||||
uint32_t ofs = header.ofsUVAnimation + i * uvStructSize;
|
||||
if (ofs + uvStructSize > m2Data.size()) break;
|
||||
|
||||
M2TextureTransformDisk dt = readValue<M2TextureTransformDisk>(m2Data, ofs);
|
||||
M2TextureTransform tt;
|
||||
parseAnimTrack(m2Data, dt.translation, tt.translation, TrackType::VEC3, seqFlags);
|
||||
parseAnimTrack(m2Data, dt.rotation, tt.rotation, TrackType::QUAT_COMPRESSED, seqFlags);
|
||||
parseAnimTrack(m2Data, dt.scaling, tt.scale, TrackType::VEC3, seqFlags);
|
||||
if (header.version >= 264) {
|
||||
M2TextureTransformDisk dt = readValue<M2TextureTransformDisk>(m2Data, ofs);
|
||||
parseAnimTrack(m2Data, dt.translation, tt.translation, TrackType::VEC3, seqFlags);
|
||||
parseAnimTrack(m2Data, dt.rotation, tt.rotation, TrackType::QUAT_COMPRESSED, seqFlags);
|
||||
parseAnimTrack(m2Data, dt.scaling, tt.scale, TrackType::VEC3, seqFlags);
|
||||
} else {
|
||||
M2TextureTransformDiskVanilla dt = readValue<M2TextureTransformDiskVanilla>(m2Data, ofs);
|
||||
parseAnimTrackVanilla(m2Data, dt.translation, tt.translation, TrackType::VEC3);
|
||||
parseAnimTrackVanilla(m2Data, dt.rotation, tt.rotation, TrackType::QUAT_COMPRESSED);
|
||||
parseAnimTrackVanilla(m2Data, dt.scaling, tt.scale, TrackType::VEC3);
|
||||
}
|
||||
model.textureTransforms.push_back(std::move(tt));
|
||||
}
|
||||
core::Logger::getInstance().debug(" Texture transforms: ", model.textureTransforms.size());
|
||||
|
|
@ -889,16 +1012,27 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
model.textureTransformLookup = readArray<uint16_t>(m2Data, header.ofsTransLookup, header.nTransLookup);
|
||||
}
|
||||
|
||||
// Read attachment points
|
||||
// Read attachment points (vanilla uses 48-byte struct, WotLK uses 40-byte)
|
||||
if (header.nAttachments > 0 && header.ofsAttachments > 0) {
|
||||
auto diskAttachments = readArray<M2AttachmentDisk>(m2Data, header.ofsAttachments, header.nAttachments);
|
||||
model.attachments.reserve(diskAttachments.size());
|
||||
for (const auto& da : diskAttachments) {
|
||||
M2Attachment att;
|
||||
att.id = da.id;
|
||||
att.bone = da.bone;
|
||||
att.position = glm::vec3(da.position[0], da.position[1], da.position[2]);
|
||||
model.attachments.push_back(att);
|
||||
model.attachments.reserve(header.nAttachments);
|
||||
if (header.version < 264) {
|
||||
auto diskAttachments = readArray<M2AttachmentDiskVanilla>(m2Data, header.ofsAttachments, header.nAttachments);
|
||||
for (const auto& da : diskAttachments) {
|
||||
M2Attachment att;
|
||||
att.id = da.id;
|
||||
att.bone = da.bone;
|
||||
att.position = glm::vec3(da.position[0], da.position[1], da.position[2]);
|
||||
model.attachments.push_back(att);
|
||||
}
|
||||
} else {
|
||||
auto diskAttachments = readArray<M2AttachmentDisk>(m2Data, header.ofsAttachments, header.nAttachments);
|
||||
for (const auto& da : diskAttachments) {
|
||||
M2Attachment att;
|
||||
att.id = da.id;
|
||||
att.bone = da.bone;
|
||||
att.position = glm::vec3(da.position[0], da.position[1], da.position[2]);
|
||||
model.attachments.push_back(att);
|
||||
}
|
||||
}
|
||||
core::Logger::getInstance().debug(" Attachments: ", model.attachments.size());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,6 +170,8 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderCombatText(gameHandler);
|
||||
renderPartyFrames(gameHandler);
|
||||
renderGroupInvitePopup(gameHandler);
|
||||
renderGuildInvitePopup(gameHandler);
|
||||
renderGuildRoster(gameHandler);
|
||||
renderBuffBar(gameHandler);
|
||||
renderLootWindow(gameHandler);
|
||||
renderGossipWindow(gameHandler);
|
||||
|
|
@ -1589,6 +1591,24 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// /gkick command
|
||||
if (cmdLower == "gkick" || cmdLower == "guildkick") {
|
||||
if (spacePos != std::string::npos) {
|
||||
std::string playerName = command.substr(spacePos + 1);
|
||||
gameHandler.kickGuildMember(playerName);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gkick <player>";
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// /readycheck command
|
||||
if (cmdLower == "readycheck" || cmdLower == "rc") {
|
||||
gameHandler.initiateReadyCheck();
|
||||
|
|
@ -3181,6 +3201,135 @@ void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.hasPendingGuildInvite()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 250), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
||||
|
||||
if (ImGui::Begin("Guild Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
||||
ImGui::TextWrapped("%s has invited you to join %s.",
|
||||
gameHandler.getPendingGuildInviterName().c_str(),
|
||||
gameHandler.getPendingGuildInviteGuildName().c_str());
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Accept", ImVec2(155, 30))) {
|
||||
gameHandler.acceptGuildInvite();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Decline", ImVec2(155, 30))) {
|
||||
gameHandler.declineGuildInvite();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
||||
// O key toggle (WoW default Social/Guild keybind)
|
||||
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
||||
showGuildRoster_ = !showGuildRoster_;
|
||||
if (showGuildRoster_) {
|
||||
if (!gameHandler.isInGuild()) {
|
||||
gameHandler.addLocalChatMessage(game::MessageChatData{
|
||||
game::ChatType::SYSTEM, game::ChatLanguage::UNIVERSAL, 0, "", 0, "", "You are not in a guild.", "", 0});
|
||||
showGuildRoster_ = false;
|
||||
return;
|
||||
}
|
||||
gameHandler.requestGuildRoster();
|
||||
}
|
||||
}
|
||||
|
||||
if (!showGuildRoster_) 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 - 300, screenH / 2 - 250), ImGuiCond_Once);
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Once);
|
||||
|
||||
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Roster") : "Guild Roster";
|
||||
bool open = showGuildRoster_;
|
||||
if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!gameHandler.hasGuildRoster()) {
|
||||
ImGui::Text("Loading roster...");
|
||||
} else {
|
||||
const auto& roster = gameHandler.getGuildRoster();
|
||||
|
||||
// MOTD
|
||||
if (!roster.motd.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str());
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// Count online
|
||||
int onlineCount = 0;
|
||||
for (const auto& m : roster.members) {
|
||||
if (m.online) ++onlineCount;
|
||||
}
|
||||
ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount);
|
||||
ImGui::Separator();
|
||||
|
||||
// Table
|
||||
if (ImGui::BeginTable("GuildRoster", 6,
|
||||
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
|
||||
ImGuiTableFlags_Sortable)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
|
||||
ImGui::TableSetupColumn("Rank");
|
||||
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
||||
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f);
|
||||
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableSetupColumn("Note");
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// Online members first, then offline
|
||||
auto sortedMembers = roster.members;
|
||||
std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) {
|
||||
if (a.online != b.online) return a.online > b.online;
|
||||
return a.name < b.name;
|
||||
});
|
||||
|
||||
static const char* classNames[] = {
|
||||
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue",
|
||||
"Priest", "Death Knight", "Shaman", "Mage", "Warlock",
|
||||
"", "Druid"
|
||||
};
|
||||
|
||||
for (const auto& m : sortedMembers) {
|
||||
ImGui::TableNextRow();
|
||||
ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f)
|
||||
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(textColor, "%s", m.name.c_str());
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(textColor, "%u", m.rankIndex);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(textColor, "%u", m.level);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown";
|
||||
ImGui::TextColored(textColor, "%s", className);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(textColor, "%u", m.zoneId);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
showGuildRoster_ = open;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Buff/Debuff Bar (Phase 3)
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue