mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 01:23:51 +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
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue