Add guild features, fix channel joining, and improve whisper reply

Guild: add disband, leader transfer, public/officer note commands with
roster context menu showing rank names and officer notes column. Auto-refresh
roster after guild events.

Channels: fix city/region channels not working by accepting SMSG_CHANNEL_NOTIFY
during ENTERING_WORLD state (server auto-joins before VERIFY_WORLD) and handling
PLAYER_ALREADY_MEMBER notification.

Whisper: /r now switches to whisper tab and sets target to last sender,
matching WoW behavior.

Camera: extend WMO collision raycasting to work outside WMOs too.
This commit is contained in:
Kelsi 2026-02-16 20:16:14 -08:00
parent ee5ab30826
commit 2ece1bf1cf
9 changed files with 271 additions and 9 deletions

View file

@ -772,7 +772,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
case Opcode::SMSG_CHANNEL_NOTIFY:
if (state == WorldState::IN_WORLD) {
// Accept during ENTERING_WORLD too — server auto-joins channels before VERIFY_WORLD
if (state == WorldState::IN_WORLD || state == WorldState::ENTERING_WORLD) {
handleChannelNotify(packet);
}
break;
@ -4331,6 +4332,23 @@ void GameHandler::handleChannelNotify(network::Packet& packet) {
LOG_INFO("Left channel: ", data.channelName);
break;
}
case ChannelNotifyType::PLAYER_ALREADY_MEMBER: {
// Server says we're already in this channel (e.g. server auto-joined us)
// Still track it in our channel list
bool found = false;
for (const auto& ch : joinedChannels_) {
if (ch == data.channelName) { found = true; break; }
}
if (!found) {
joinedChannels_.push_back(data.channelName);
LOG_INFO("Already in channel: ", data.channelName);
}
break;
}
case ChannelNotifyType::NOT_IN_AREA: {
LOG_DEBUG("Cannot join channel ", data.channelName, " (not in area)");
break;
}
default:
LOG_DEBUG("Channel notify type ", static_cast<int>(data.notifyType),
" for channel ", data.channelName);
@ -7172,6 +7190,34 @@ void GameHandler::kickGuildMember(const std::string& playerName) {
LOG_INFO("Kicking guild member: ", playerName);
}
void GameHandler::disbandGuild() {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildDisbandPacket::build();
socket->send(packet);
LOG_INFO("Disbanding guild");
}
void GameHandler::setGuildLeader(const std::string& name) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildLeaderPacket::build(name);
socket->send(packet);
LOG_INFO("Setting guild leader: ", name);
}
void GameHandler::setGuildPublicNote(const std::string& name, const std::string& note) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildSetPublicNotePacket::build(name, note);
socket->send(packet);
LOG_INFO("Setting public note for ", name, ": ", note);
}
void GameHandler::setGuildOfficerNote(const std::string& name, const std::string& note) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GuildSetOfficerNotePacket::build(name, note);
socket->send(packet);
LOG_INFO("Setting officer note for ", name, ": ", note);
}
void GameHandler::acceptGuildInvite() {
if (state != WorldState::IN_WORLD || !socket) return;
pendingGuildInvite_ = false;
@ -7291,6 +7337,20 @@ void GameHandler::handleGuildEvent(network::Packet& packet) {
chatMsg.message = msg;
addLocalChatMessage(chatMsg);
}
// Auto-refresh roster after membership/rank changes
switch (data.eventType) {
case GuildEvent::PROMOTION:
case GuildEvent::DEMOTION:
case GuildEvent::JOINED:
case GuildEvent::LEFT:
case GuildEvent::REMOVED:
case GuildEvent::LEADER_CHANGED:
if (hasGuildRoster_) requestGuildRoster();
break;
default:
break;
}
}
void GameHandler::handleGuildInvite(network::Packet& packet) {

View file

@ -100,6 +100,10 @@ static const OpcodeNameEntry kOpcodeNames[] = {
{"SMSG_GUILD_QUERY_RESPONSE", LogicalOpcode::SMSG_GUILD_QUERY_RESPONSE},
{"SMSG_GUILD_INVITE", LogicalOpcode::SMSG_GUILD_INVITE},
{"CMSG_GUILD_REMOVE", LogicalOpcode::CMSG_GUILD_REMOVE},
{"CMSG_GUILD_DISBAND", LogicalOpcode::CMSG_GUILD_DISBAND},
{"CMSG_GUILD_LEADER", LogicalOpcode::CMSG_GUILD_LEADER},
{"CMSG_GUILD_SET_PUBLIC_NOTE", LogicalOpcode::CMSG_GUILD_SET_PUBLIC_NOTE},
{"CMSG_GUILD_SET_OFFICER_NOTE", LogicalOpcode::CMSG_GUILD_SET_OFFICER_NOTE},
{"SMSG_GUILD_EVENT", LogicalOpcode::SMSG_GUILD_EVENT},
{"SMSG_GUILD_COMMAND_RESULT", LogicalOpcode::SMSG_GUILD_COMMAND_RESULT},
{"MSG_RAID_READY_CHECK", LogicalOpcode::MSG_RAID_READY_CHECK},
@ -404,6 +408,10 @@ void OpcodeTable::loadWotlkDefaults() {
{LogicalOpcode::SMSG_GUILD_QUERY_RESPONSE, 0x052},
{LogicalOpcode::SMSG_GUILD_INVITE, 0x083},
{LogicalOpcode::CMSG_GUILD_REMOVE, 0x08E},
{LogicalOpcode::CMSG_GUILD_DISBAND, 0x08F},
{LogicalOpcode::CMSG_GUILD_LEADER, 0x090},
{LogicalOpcode::CMSG_GUILD_SET_PUBLIC_NOTE, 0x234},
{LogicalOpcode::CMSG_GUILD_SET_OFFICER_NOTE, 0x235},
{LogicalOpcode::SMSG_GUILD_EVENT, 0x092},
{LogicalOpcode::SMSG_GUILD_COMMAND_RESULT, 0x093},
{LogicalOpcode::MSG_RAID_READY_CHECK, 0x322},

View file

@ -1622,6 +1622,35 @@ network::Packet GuildRemovePacket::build(const std::string& playerName) {
return packet;
}
network::Packet GuildDisbandPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DISBAND));
LOG_DEBUG("Built CMSG_GUILD_DISBAND");
return packet;
}
network::Packet GuildLeaderPacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_LEADER));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_LEADER: ", playerName);
return packet;
}
network::Packet GuildSetPublicNotePacket::build(const std::string& playerName, const std::string& note) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_SET_PUBLIC_NOTE));
packet.writeString(playerName);
packet.writeString(note);
LOG_DEBUG("Built CMSG_GUILD_SET_PUBLIC_NOTE: ", playerName, " -> ", note);
return packet;
}
network::Packet GuildSetOfficerNotePacket::build(const std::string& playerName, const std::string& note) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_SET_OFFICER_NOTE));
packet.writeString(playerName);
packet.writeString(note);
LOG_DEBUG("Built CMSG_GUILD_SET_OFFICER_NOTE: ", playerName, " -> ", note);
return packet;
}
network::Packet GuildAcceptPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ACCEPT));
LOG_DEBUG("Built CMSG_GUILD_ACCEPT");