mirror of
https://github.com/EQEmu/Server.git
synced 2026-06-21 06:28:22 +00:00
Moved formatted message into the new tob patch and cleaned up a bit
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
set(message_component_sources
|
||||
titanium.cpp
|
||||
rof2.cpp
|
||||
tob.cpp
|
||||
titanium.cpp
|
||||
rof2.cpp
|
||||
tob.cpp
|
||||
)
|
||||
|
||||
set(message_component_headers
|
||||
IMessage.h
|
||||
titanium.h
|
||||
sof.h
|
||||
sod.h
|
||||
uf.h
|
||||
rof.h
|
||||
rof2.h
|
||||
tob.h
|
||||
IMessage.h
|
||||
titanium.h
|
||||
sof.h
|
||||
sod.h
|
||||
uf.h
|
||||
rof.h
|
||||
rof2.h
|
||||
tob.h
|
||||
)
|
||||
|
||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${message_component_sources})
|
||||
|
||||
@@ -26,8 +26,8 @@ class Mob;
|
||||
class EQApplicationPacket;
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class IMessage {
|
||||
class IMessage
|
||||
{
|
||||
public:
|
||||
constexpr IMessage() {}
|
||||
constexpr virtual ~IMessage() {}
|
||||
@@ -35,19 +35,19 @@ public:
|
||||
// these two are the basic string message packets
|
||||
virtual EQApplicationPacket* Simple(uint32_t color, uint32_t id) const = 0;
|
||||
virtual EQApplicationPacket* Formatted(uint32_t color, uint32_t id,
|
||||
const char* a1 = nullptr, const char* a2 = nullptr, const char* a3 = nullptr,
|
||||
const char* a4 = nullptr, const char* a5 = nullptr, const char* a6 = nullptr,
|
||||
const char* a7 = nullptr, const char* a8 = nullptr, const char* a9 = nullptr) const = 0;
|
||||
const char* a1 = nullptr, const char* a2 = nullptr, const char* a3 = nullptr,
|
||||
const char* a4 = nullptr, const char* a5 = nullptr, const char* a6 = nullptr,
|
||||
const char* a7 = nullptr, const char* a8 = nullptr, const char* a9 = nullptr) const = 0;
|
||||
|
||||
// These aren't technically messages, but they use the same format and are similar enough to include here
|
||||
virtual EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override = "") const = 0;
|
||||
virtual EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override = "") const = 0;
|
||||
const char* spell_name_override = "") const = 0;
|
||||
virtual EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id,
|
||||
uint32_t spell_id, const char* spell_name_override = "") const = 0;
|
||||
|
||||
// Everything else is specializations of logic needed to build strings that differ between patches
|
||||
virtual EQApplicationPacket* Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const = 0;
|
||||
virtual EQApplicationPacket* FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const = 0;
|
||||
virtual EQApplicationPacket* FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id,
|
||||
const char* caster) const = 0;
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -20,11 +20,10 @@
|
||||
#include "zone/patch/components/message/uf.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class RoF : public UF {
|
||||
class RoF : public UF
|
||||
{
|
||||
public:
|
||||
constexpr RoF() {}
|
||||
constexpr ~RoF() override {}
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -20,11 +20,10 @@
|
||||
#include "zone/patch/components/message/rof.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class RoF2 : public RoF {
|
||||
class RoF2 : public RoF
|
||||
{
|
||||
public:
|
||||
constexpr RoF2() {}
|
||||
constexpr ~RoF2() override {}
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -20,11 +20,10 @@
|
||||
#include "zone/patch/components/message/sof.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class SoD : public SoF {
|
||||
class SoD : public SoF
|
||||
{
|
||||
public:
|
||||
constexpr SoD() {}
|
||||
constexpr ~SoD() override {}
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -20,11 +20,10 @@
|
||||
#include "zone/patch/components/message/titanium.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class SoF : public Titanium {
|
||||
class SoF : public Titanium
|
||||
{
|
||||
public:
|
||||
constexpr SoF() {}
|
||||
constexpr ~SoF() override {}
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
#include "common/serialize_buffer.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
EQApplicationPacket* Titanium::Simple(uint32_t color, uint32_t id) const {
|
||||
EQApplicationPacket* Titanium::Simple(uint32_t color, uint32_t id) const
|
||||
{
|
||||
uint32_t string_id = ResolveID(id);
|
||||
if (string_id > 0) {
|
||||
auto outapp = new EQApplicationPacket(OP_SimpleMessage, sizeof(SimpleMessage_Struct));
|
||||
@@ -39,26 +40,25 @@ EQApplicationPacket* Titanium::Simple(uint32_t color, uint32_t id) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EQApplicationPacket* Titanium::Formatted(uint32_t color, uint32_t id,
|
||||
const char* a1, const char* a2, const char* a3,
|
||||
const char* a4, const char* a5, const char* a6,
|
||||
const char* a7, const char* a8, const char* a9) const {
|
||||
EQApplicationPacket* Titanium::Formatted(
|
||||
uint32_t color, uint32_t id,
|
||||
const char* a1, const char* a2, const char* a3,
|
||||
const char* a4, const char* a5, const char* a6,
|
||||
const char* a7, const char* a8, const char* a9) const
|
||||
{
|
||||
uint32_t string_id = ResolveID(id);
|
||||
if (string_id > 0) {
|
||||
if (!a1)
|
||||
return Simple(color, id);
|
||||
|
||||
const char* args[] = {a1, a2, a3, a4, a5, a6, a7, a8, a9};
|
||||
|
||||
SerializeBuffer buf(20);
|
||||
buf.WriteInt32(0);
|
||||
buf.WriteInt32(string_id);
|
||||
buf.WriteInt32(color);
|
||||
|
||||
for (const auto* arg : args) {
|
||||
if (!arg)
|
||||
break;
|
||||
buf.WriteString(arg);
|
||||
for (const auto* a : {a1, a2, a3, a4, a5, a6, a7, a8, a9}) {
|
||||
if (a != nullptr)
|
||||
buf.WriteString(a);
|
||||
}
|
||||
|
||||
buf.WriteInt8(0);
|
||||
@@ -69,8 +69,10 @@ EQApplicationPacket* Titanium::Formatted(uint32_t color, uint32_t id,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EQApplicationPacket* Titanium::InterruptSpell(uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const {
|
||||
EQApplicationPacket* Titanium::InterruptSpell(
|
||||
uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const
|
||||
{
|
||||
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct));
|
||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||
ic->messageid = ResolveID(message);
|
||||
@@ -80,8 +82,10 @@ EQApplicationPacket* Titanium::InterruptSpell(uint32_t message, uint32_t spawn_i
|
||||
return outapp;
|
||||
}
|
||||
|
||||
EQApplicationPacket* Titanium::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const {
|
||||
EQApplicationPacket* Titanium::InterruptSpellOther(
|
||||
Mob* sender, uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const
|
||||
{
|
||||
auto name = sender->GetCleanName();
|
||||
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(name) + 1);
|
||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||
@@ -91,19 +95,21 @@ EQApplicationPacket* Titanium::InterruptSpellOther(Mob* sender, uint32_t message
|
||||
return outapp;
|
||||
}
|
||||
|
||||
EQApplicationPacket* Titanium::Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const {
|
||||
EQApplicationPacket* Titanium::Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const
|
||||
{
|
||||
return Simple(type, message);
|
||||
}
|
||||
|
||||
EQApplicationPacket* Titanium::FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const {
|
||||
EQApplicationPacket* Titanium::FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const
|
||||
{
|
||||
return Formatted(type, message, caster);
|
||||
}
|
||||
|
||||
// A value of 0 means that the string isn't mapped in this client, valid string ids start at 1
|
||||
uint32_t Titanium::ResolveID(uint32_t id) const {
|
||||
uint32_t Titanium::ResolveID(uint32_t id) const
|
||||
{
|
||||
// passthrough — string IDs are defined at the base client level;
|
||||
// override in patches where IDs need remapping
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace ZoneClient::Message
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
#include "zone/patch/components/message/IMessage.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class Titanium : public IMessage {
|
||||
class Titanium : public IMessage
|
||||
{
|
||||
public:
|
||||
constexpr Titanium() {}
|
||||
constexpr ~Titanium() override {}
|
||||
@@ -29,20 +29,20 @@ public:
|
||||
EQApplicationPacket* Simple(uint32_t color, uint32_t id) const override;
|
||||
|
||||
EQApplicationPacket* Formatted(uint32_t color, uint32_t id,
|
||||
const char* a1 = nullptr, const char* a2 = nullptr, const char* a3 = nullptr,
|
||||
const char* a4 = nullptr, const char* a5 = nullptr, const char* a6 = nullptr,
|
||||
const char* a7 = nullptr, const char* a8 = nullptr, const char* a9 = nullptr) const override;
|
||||
const char* a1 = nullptr, const char* a2 = nullptr, const char* a3 = nullptr,
|
||||
const char* a4 = nullptr, const char* a5 = nullptr, const char* a6 = nullptr,
|
||||
const char* a7 = nullptr, const char* a8 = nullptr, const char* a9 = nullptr) const override;
|
||||
|
||||
EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override = "") const override;
|
||||
const char* spell_name_override = "") const override;
|
||||
EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override = "") const override;
|
||||
const char* spell_name_override = "") const override;
|
||||
|
||||
EQApplicationPacket* Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const override;
|
||||
EQApplicationPacket* FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const override;
|
||||
EQApplicationPacket* FizzleOther(uint32_t type, uint32_t message,
|
||||
uint32_t spell_id, const char* caster) const override;
|
||||
|
||||
protected:
|
||||
virtual uint32_t ResolveID(uint32_t id) const;
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
#include "common/links.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
struct TOBStringIDs {
|
||||
struct TOBStringIDs
|
||||
{
|
||||
static constexpr uint32_t DisarmedTrap = 1458; // You successfully disarmed the trap
|
||||
};
|
||||
|
||||
uint32_t TOB::ResolveID(uint32_t id) const {
|
||||
uint32_t TOB::ResolveID(uint32_t id) const
|
||||
{
|
||||
switch (id) {
|
||||
case YOU_FLURRY:
|
||||
case BOW_DOUBLE_DAMAGE:
|
||||
@@ -72,10 +73,134 @@ uint32_t TOB::ResolveID(uint32_t id) const {
|
||||
}
|
||||
}
|
||||
|
||||
EQApplicationPacket* TOB::InterruptSpell(uint32_t message, uint32_t spawn_id, uint32_t spell_id, const char* spell_name_override) const {
|
||||
// TOB is the first patch to fully support links in the client. This helper function is therefore internal to TOB
|
||||
// because any future patches would default to the TOB message strings
|
||||
static void ServerToTOBConvertLinks(std::string& message_out, const std::string& message_in)
|
||||
{
|
||||
if (message_in.find('\x12') == std::string::npos) {
|
||||
message_out = message_in;
|
||||
return;
|
||||
}
|
||||
|
||||
auto segments = Strings::Split(message_in, '\x12');
|
||||
for (size_t segment_iter = 0; segment_iter < segments.size(); ++segment_iter) {
|
||||
if (segment_iter & 1) {
|
||||
auto etag = std::stoi(segments[segment_iter].substr(0, 1));
|
||||
|
||||
switch (etag) {
|
||||
case 0: {
|
||||
size_t index = 1;
|
||||
auto item_id = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto aug1 = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto aug2 = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto aug3 = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto aug4 = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto aug5 = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto aug6 = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto is_evolving = segments[segment_iter].substr(index, 1);
|
||||
index += 1;
|
||||
|
||||
auto evolutionGroup = segments[segment_iter].substr(index, 4);
|
||||
index += 4;
|
||||
|
||||
auto evolutionLevel = segments[segment_iter].substr(index, 2);
|
||||
index += 2;
|
||||
|
||||
auto ornamentationIconID = segments[segment_iter].substr(index, 5);
|
||||
index += 5;
|
||||
|
||||
auto itemHash = segments[segment_iter].substr(index, 8);
|
||||
index += 8;
|
||||
|
||||
auto text = segments[segment_iter].substr(index);
|
||||
|
||||
message_out.push_back('\x12');
|
||||
message_out.push_back('0'); //etag item
|
||||
message_out.append(item_id);
|
||||
message_out.append(aug1);
|
||||
message_out.append("00000");
|
||||
message_out.append(aug2);
|
||||
message_out.append("00000");
|
||||
message_out.append(aug3);
|
||||
message_out.append("00000");
|
||||
message_out.append(aug4);
|
||||
message_out.append("00000");
|
||||
message_out.append(aug5);
|
||||
message_out.append("00000");
|
||||
message_out.append(aug6);
|
||||
message_out.append("00000");
|
||||
message_out.append(is_evolving);
|
||||
message_out.append(evolutionGroup);
|
||||
message_out.append(evolutionLevel);
|
||||
message_out.append(ornamentationIconID);
|
||||
message_out.append("00000");
|
||||
message_out.append(itemHash);
|
||||
message_out.append(text);
|
||||
message_out.push_back('\x12');
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
//unsupported etag right now; just pass it as is
|
||||
message_out.push_back('\x12');
|
||||
message_out.append(segments[segment_iter]);
|
||||
message_out.push_back('\x12');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
message_out.append(segments[segment_iter]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EQApplicationPacket* TOB::Formatted(uint32_t color, uint32_t id, const char* a1, const char* a2, const char* a3,
|
||||
const char* a4, const char* a5, const char* a6, const char* a7, const char* a8, const char* a9) const
|
||||
{
|
||||
uint32_t string_id = ResolveID(id);
|
||||
if (string_id > 0) {
|
||||
SerializeBuffer buffer(49);
|
||||
// 49 is the minimum size needed for this packet since each arg writes at least 4 bytes
|
||||
buffer.WriteUInt32(0);
|
||||
// This is a string written like the message arrays, but it seems to be discarded by the client
|
||||
buffer.WriteUInt8(0); // 0 is a zone packet, 1 is a world packet -- these are always sent from zone from here
|
||||
buffer.WriteUInt32(string_id);
|
||||
buffer.WriteUInt32(color);
|
||||
|
||||
for (auto a : {a1, a2, a3, a4, a5, a6, a7, a8, a9}) {
|
||||
if (a != nullptr) {
|
||||
std::string new_message;
|
||||
ServerToTOBConvertLinks(new_message, a);
|
||||
buffer.WriteLengthString(new_message);
|
||||
} else
|
||||
buffer.WriteUInt32(0);
|
||||
}
|
||||
|
||||
return new EQApplicationPacket(OP_FormattedMessage, std::move(buffer));
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EQApplicationPacket* TOB::InterruptSpell(uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const
|
||||
{
|
||||
std::string spell_name = spell_name_override == nullptr || *spell_name_override == '\0'
|
||||
? GetSpellName(spell_id)
|
||||
: spell_name_override;
|
||||
? GetSpellName(spell_id)
|
||||
: spell_name_override;
|
||||
|
||||
std::string spell_link = Links::FormatSpellLink(spell_id, spell_name);
|
||||
|
||||
@@ -90,15 +215,17 @@ EQApplicationPacket* TOB::InterruptSpell(uint32_t message, uint32_t spawn_id, ui
|
||||
}
|
||||
|
||||
EQApplicationPacket* TOB::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const {
|
||||
const char* spell_name_override) const
|
||||
{
|
||||
std::string spell_name = spell_name_override == nullptr || *spell_name_override == '\0'
|
||||
? GetSpellName(spell_id)
|
||||
: spell_name_override;
|
||||
? GetSpellName(spell_id)
|
||||
: spell_name_override;
|
||||
|
||||
std::string spell_link = Links::FormatSpellLink(spell_id, spell_name);
|
||||
|
||||
auto name = sender->GetCleanName();
|
||||
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(name) + spell_link.size() + 2);
|
||||
auto outapp = new EQApplicationPacket(OP_InterruptCast,
|
||||
sizeof(InterruptCast_Struct) + strlen(name) + spell_link.size() + 2);
|
||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||
ic->messageid = ResolveID(message);
|
||||
ic->spawnid = spawn_id;
|
||||
@@ -107,14 +234,16 @@ EQApplicationPacket* TOB::InterruptSpellOther(Mob* sender, uint32_t message, uin
|
||||
return outapp;
|
||||
}
|
||||
|
||||
EQApplicationPacket* TOB::Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const {
|
||||
EQApplicationPacket* TOB::Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const
|
||||
{
|
||||
std::string spell_name(GetSpellName(spell_id));
|
||||
std::string spell_link = Links::FormatSpellLink(spell_id, spell_name);
|
||||
|
||||
return Formatted(type, message, spell_link.c_str());
|
||||
}
|
||||
|
||||
EQApplicationPacket* TOB::FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const {
|
||||
EQApplicationPacket* TOB::FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const
|
||||
{
|
||||
std::string spell_name(GetSpellName(spell_id));
|
||||
std::string spell_link = Links::FormatSpellLink(spell_id, spell_name);
|
||||
|
||||
|
||||
@@ -20,21 +20,27 @@
|
||||
#include "zone/patch/components/message/rof2.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
class TOB : public RoF2 {
|
||||
class TOB : public RoF2
|
||||
{
|
||||
public:
|
||||
constexpr TOB() {}
|
||||
constexpr ~TOB() override {}
|
||||
|
||||
EQApplicationPacket* Formatted(uint32_t color, uint32_t id,
|
||||
const char* a1 = nullptr, const char* a2 = nullptr, const char* a3 = nullptr,
|
||||
const char* a4 = nullptr, const char* a5 = nullptr, const char* a6 = nullptr,
|
||||
const char* a7 = nullptr, const char* a8 = nullptr, const char* a9 = nullptr) const override;
|
||||
|
||||
EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const override;
|
||||
const char* spell_name_override) const override;
|
||||
EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, uint32_t spell_id,
|
||||
const char* spell_name_override) const override;
|
||||
const char* spell_name_override) const override;
|
||||
|
||||
EQApplicationPacket* Fizzle(uint32_t type, uint32_t message, uint32_t spell_id) const override;
|
||||
EQApplicationPacket* FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const override;
|
||||
EQApplicationPacket*
|
||||
FizzleOther(uint32_t type, uint32_t message, uint32_t spell_id, const char* caster) const override;
|
||||
|
||||
protected:
|
||||
uint32_t ResolveID(uint32_t id) const override;
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
@@ -20,11 +20,10 @@
|
||||
#include "patch/components/message/sod.h"
|
||||
|
||||
namespace ZoneClient::Message {
|
||||
|
||||
class UF : public SoD {
|
||||
class UF : public SoD
|
||||
{
|
||||
public:
|
||||
constexpr UF() {}
|
||||
constexpr ~UF() override {}
|
||||
};
|
||||
|
||||
} // namespace Zone::Message
|
||||
|
||||
Reference in New Issue
Block a user