Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions modules/phoenix/cpp/chat_adjustments.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/************************************************************************
* Chat Adjustments Module
*
* Phoenix chat rules, applied by catching the standard chat packet
* before the base handler. Each section below is one rule.
************************************************************************/
#include "common/earth_time.h"
#include "common/ipc_structs.h"
#include "common/logging.h"
#include "map/entities/char_entity.h"
#include "map/enums/chat_message_type.h"
#include "map/enums/msg_std.h"
#include "map/ipc_client.h"
#include "map/packets/basic.h"
#include "map/packets/c2s/0x0b5_chat_std.h"
#include "map/packets/s2c/0x009_message.h"
#include "map/packets/s2c/0x017_chat_std.h"
#include "map/utils/moduleutils.h"
#include "map/zone.h"

#include <format>
#include <string>
#include <string_view>

namespace
{

//-----------------------------------
// Section: GM system messages.
//-----------------------------------
// A GM message starting with ### broadcasts the text after it to the whole
// server. # and ## are refused with a hint, so a mistyped message cannot
// broadcast. Players are not affected.

constexpr std::string_view systemMessagePrefix = "###";

const std::string systemMessageHint = "System messages start with ###.";

// The base handler broadcasts any GM # message before it looks at the chat
// kind. Every GM # message must be consumed here.
auto handleSystemMessage(CCharEntity* PChar, const std::string& message) -> bool
{
const auto body = message.starts_with(systemMessagePrefix) ? message.substr(systemMessagePrefix.size()) : std::string();

if (body.empty())
{
PChar->pushPacket<GP_SERV_COMMAND_CHAT_STD>(PChar, MESSAGE_SYSTEM_1, systemMessageHint);

return true;
}

message::send(ipc::ChatMessageServerMessage{
.senderId = PChar->id,
.senderName = PChar->getName(),
.message = body,
.zoneId = PChar->getZone(),
.gmLevel = PChar->m_GMlevel,
});

return true;
}

//-----------------------------------
// Section: Yell restrictions.
//-----------------------------------
// Yell requires level 10 on some job. The cooldown is 10 minutes instead of
// the base 30 seconds. Which zones allow yell at all is still the Yell flag
// in zone_settings.misc.

constexpr uint8 minYellLevel = 10;

// The base handler's own 30 second cooldown expires inside this one.
constexpr uint32 yellCooldown = 600;

// Separate from the base handler's [YELL]Cooldown so the two do not fight.
// Self-expires and persists through zoning.
const std::string yellCooldownVar = "[YELL]CooldownPhx";

auto handleYell(CCharEntity* PChar) -> bool
{
// GMs are exempt. That also keeps their ! commands working through this
// channel. A zone without the Yell flag and a banned character both keep
// the base handler's own refusal. Neither burns the cooldown.
//
// Everyone else is caught before the base handler sees the message.
// An unresolved ! command from a non-GM falls through to the yell path there.
if (PChar->m_GMlevel > 0 ||
!PChar->loc.zone->CanUseMisc(xi::ZoneMisc::Yell) ||
PChar->getCharVar("[YELL]Banned") == 1)
{
return false;
}

// Level 10 on any job is enough. A level sync or a job change does not take yell away.
if (PChar->getHighestJobLevel() < minYellLevel)
{
PChar->pushPacket<GP_SERV_COMMAND_CHAT_STD>(PChar, MESSAGE_SYSTEM_1, std::format("You must reach level {} before you can use /yell.", minYellLevel));

return true;
}

if (PChar->getCharVar(yellCooldownVar) == 1)
{
PChar->pushPacket<GP_SERV_COMMAND_MESSAGE>(PChar, 0, MsgStd::WaitLonger);

return true;
}

// CharVar will self-expire and set to zero after the cooldown period.
// A ! command that resolves in yell mode burns this without a yell going
// out. The module cannot see whether the command resolved. Skipping !
// messages would let unresolved ones yell past the cooldown.
PChar->setCharVar(yellCooldownVar, 1, earth_time::timestamp() + yellCooldown);

return false;
}

} // namespace

class ChatAdjustments : public CPPModule
{
void OnInit() override
{
}

auto OnIncomingPacket(MapSession* PSession, CCharEntity* PChar, CBasicPacket& data) -> bool override
{
// Return if the packet isn't standard chat
if (data.getType() != static_cast<uint16>(GP_CLI_COMMAND_CHAT_STD::packetId))
{
return false;
}

const auto* packet = data.as<GP_CLI_COMMAND_CHAT_STD>();

// Same length math as the base handler. The message may not be NULL-terminated.
const auto messageLength = std::min<std::size_t>((packet->header.size * 4) - 0x6, sizeof(packet->Str));
const auto rawMessage = asStringFromUntrustedSource(packet->Str, messageLength);

if (PChar->m_GMlevel > 0 && !rawMessage.empty() && rawMessage[0] == '#')
{
return handleSystemMessage(PChar, rawMessage);
}

if (static_cast<GP_CLI_COMMAND_CHAT_STD_KIND>(packet->Kind) == GP_CLI_COMMAND_CHAT_STD_KIND::Yell)
{
return handleYell(PChar);
}

return false;
}
};

REGISTER_CPP_MODULE(ChatAdjustments);