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;