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:
Kelsi 2026-02-13 21:39:48 -08:00
parent 60c93fa1e3
commit 22728b461f
16 changed files with 951 additions and 26 deletions

View file

@ -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;

View file

@ -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;
};
/**

View file

@ -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
// ============================================================

View file

@ -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