Add Tier 1 utility commands: ignore, sit/stand, and logout

Ignore Commands:
- Add /ignore <name> to block messages from players
- Add /unignore <name> to unblock players
- Maintain ignoreCache for name-to-GUID lookups
- Show confirmation and error messages for ignore actions
- Use CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D)

Sit/Stand/Kneel Commands:
- Add /sit to sit down (stand state 1)
- Add /stand to stand up (stand state 0)
- Add /kneel to kneel (stand state 8)
- Instant visual feedback with CMSG_STAND_STATE_CHANGE (0x101)
- Support for additional stand states (chair, sleep, etc.)

Logout Commands:
- Add /logout and /camp to initiate logout with countdown
- Add /cancellogout to cancel pending logout
- Show "Logging out in 20 seconds..." or "Logout complete" messages
- Track logout state with loggingOut_ flag to prevent duplicate requests
- Handle instant logout (in inn/city) vs countdown logout
- Use opcodes:
  - CMSG_LOGOUT_REQUEST (0x4B)
  - CMSG_LOGOUT_CANCEL (0x4E)
  - SMSG_LOGOUT_RESPONSE (0x4C)
  - SMSG_LOGOUT_COMPLETE (0x4D)

Implementation:
- Add LogoutRequestPacket, LogoutCancelPacket builders
- Add LogoutResponseParser to parse server logout responses
- Add StandStateChangePacket builder for stance changes
- Add AddIgnorePacket and DelIgnorePacket for ignore list management
- Add handleLogoutResponse() and handleLogoutComplete() handlers
- Add ignoreCache map and loggingOut_ state tracking
- All commands display feedback in chat window
This commit is contained in:
kelsi davis 2026-02-07 12:58:11 -08:00
parent 6f45c6ab69
commit ec32286b0d
6 changed files with 325 additions and 0 deletions

View file

@ -317,6 +317,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
break;
case Opcode::SMSG_LOGOUT_RESPONSE:
handleLogoutResponse(packet);
break;
case Opcode::SMSG_LOGOUT_COMPLETE:
handleLogoutComplete(packet);
break;
// ---- Phase 1: Foundation ----
case Opcode::SMSG_NAME_QUERY_RESPONSE:
handleNameQueryResponse(packet);
@ -1679,6 +1687,95 @@ void GameHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) {
LOG_INFO("Rolled ", minRoll, "-", maxRoll);
}
void GameHandler::addIgnore(const std::string& playerName) {
if (state != WorldState::IN_WORLD || !socket) {
LOG_WARNING("Cannot add ignore: not in world or not connected");
return;
}
if (playerName.empty()) {
addSystemChatMessage("You must specify a player name.");
return;
}
auto packet = AddIgnorePacket::build(playerName);
socket->send(packet);
addSystemChatMessage("Adding " + playerName + " to ignore list...");
LOG_INFO("Sent ignore request for: ", playerName);
}
void GameHandler::removeIgnore(const std::string& playerName) {
if (state != WorldState::IN_WORLD || !socket) {
LOG_WARNING("Cannot remove ignore: not in world or not connected");
return;
}
if (playerName.empty()) {
addSystemChatMessage("You must specify a player name.");
return;
}
// Look up GUID from cache
auto it = ignoreCache.find(playerName);
if (it == ignoreCache.end()) {
addSystemChatMessage(playerName + " is not in your ignore list.");
LOG_WARNING("Ignored player not found in cache: ", playerName);
return;
}
auto packet = DelIgnorePacket::build(it->second);
socket->send(packet);
addSystemChatMessage("Removing " + playerName + " from ignore list...");
ignoreCache.erase(it);
LOG_INFO("Sent remove ignore request for: ", playerName, " (GUID: 0x", std::hex, it->second, std::dec, ")");
}
void GameHandler::requestLogout() {
if (!socket) {
LOG_WARNING("Cannot logout: not connected");
return;
}
if (loggingOut_) {
addSystemChatMessage("Already logging out.");
return;
}
auto packet = LogoutRequestPacket::build();
socket->send(packet);
loggingOut_ = true;
LOG_INFO("Sent logout request");
}
void GameHandler::cancelLogout() {
if (!socket) {
LOG_WARNING("Cannot cancel logout: not connected");
return;
}
if (!loggingOut_) {
addSystemChatMessage("Not currently logging out.");
return;
}
auto packet = LogoutCancelPacket::build();
socket->send(packet);
loggingOut_ = false;
addSystemChatMessage("Logout cancelled.");
LOG_INFO("Cancelled logout");
}
void GameHandler::setStandState(uint8_t standState) {
if (state != WorldState::IN_WORLD || !socket) {
LOG_WARNING("Cannot change stand state: not in world or not connected");
return;
}
auto packet = StandStateChangePacket::build(standState);
socket->send(packet);
LOG_INFO("Changed stand state to: ", (int)standState);
}
void GameHandler::releaseSpirit() {
if (!playerDead_) return;
if (socket && state == WorldState::IN_WORLD) {
@ -3123,6 +3220,36 @@ void GameHandler::handleRandomRoll(network::Packet& packet) {
LOG_INFO("Random roll: ", rollerName, " rolled ", data.result, " (", data.minRoll, "-", data.maxRoll, ")");
}
void GameHandler::handleLogoutResponse(network::Packet& packet) {
LogoutResponseData data;
if (!LogoutResponseParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_LOGOUT_RESPONSE");
return;
}
if (data.result == 0) {
// Success - logout initiated
if (data.instant) {
addSystemChatMessage("Logging out...");
} else {
addSystemChatMessage("Logging out in 20 seconds...");
}
LOG_INFO("Logout response: success, instant=", (int)data.instant);
} else {
// Failure
addSystemChatMessage("Cannot logout right now.");
loggingOut_ = false;
LOG_WARNING("Logout failed, result=", data.result);
}
}
void GameHandler::handleLogoutComplete(network::Packet& /*packet*/) {
addSystemChatMessage("Logout complete.");
loggingOut_ = false;
LOG_INFO("Logout complete");
// Server will disconnect us
}
uint32_t GameHandler::generateClientSeed() {
// Generate cryptographically random seed
std::random_device rd;

View file

@ -1268,6 +1268,54 @@ bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data)
return true;
}
network::Packet AddIgnorePacket::build(const std::string& playerName) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ADD_IGNORE));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_ADD_IGNORE: player=", playerName);
return packet;
}
network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DEL_IGNORE));
packet.writeUInt64(ignoreGuid);
LOG_DEBUG("Built CMSG_DEL_IGNORE: guid=0x", std::hex, ignoreGuid, std::dec);
return packet;
}
// ============================================================
// Logout Commands
// ============================================================
network::Packet LogoutRequestPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOGOUT_REQUEST));
LOG_DEBUG("Built CMSG_LOGOUT_REQUEST");
return packet;
}
network::Packet LogoutCancelPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOGOUT_CANCEL));
LOG_DEBUG("Built CMSG_LOGOUT_CANCEL");
return packet;
}
bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& data) {
data.result = packet.readUInt32();
data.instant = packet.readUInt8();
LOG_DEBUG("Parsed SMSG_LOGOUT_RESPONSE: result=", data.result, " instant=", (int)data.instant);
return true;
}
// ============================================================
// Stand State
// ============================================================
network::Packet StandStateChangePacket::build(uint8_t state) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_STAND_STATE_CHANGE));
packet.writeUInt32(state);
LOG_DEBUG("Built CMSG_STAND_STATE_CHANGE: state=", (int)state);
return packet;
}
// ============================================================
// Random Roll
// ============================================================

View file

@ -1079,6 +1079,77 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
return;
}
// /ignore command
if (cmdLower == "ignore") {
if (spacePos != std::string::npos) {
std::string playerName = command.substr(spacePos + 1);
gameHandler.addIgnore(playerName);
chatInputBuffer[0] = '\0';
return;
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /ignore <name>";
gameHandler.addLocalChatMessage(msg);
chatInputBuffer[0] = '\0';
return;
}
// /unignore command
if (cmdLower == "unignore") {
if (spacePos != std::string::npos) {
std::string playerName = command.substr(spacePos + 1);
gameHandler.removeIgnore(playerName);
chatInputBuffer[0] = '\0';
return;
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /unignore <name>";
gameHandler.addLocalChatMessage(msg);
chatInputBuffer[0] = '\0';
return;
}
// /sit command
if (cmdLower == "sit") {
gameHandler.setStandState(1); // 1 = sit
chatInputBuffer[0] = '\0';
return;
}
// /stand command
if (cmdLower == "stand") {
gameHandler.setStandState(0); // 0 = stand
chatInputBuffer[0] = '\0';
return;
}
// /kneel command
if (cmdLower == "kneel") {
gameHandler.setStandState(8); // 8 = kneel
chatInputBuffer[0] = '\0';
return;
}
// /logout command (already exists but using /logout instead of going to login)
if (cmdLower == "logout" || cmdLower == "camp") {
gameHandler.requestLogout();
chatInputBuffer[0] = '\0';
return;
}
// /cancellogout command
if (cmdLower == "cancellogout") {
gameHandler.cancelLogout();
chatInputBuffer[0] = '\0';
return;
}
// Chat channel slash commands
bool isChannelCommand = false;
if (cmdLower == "s" || cmdLower == "say") {