mirror of
https://github.com/EQEmu/Server.git
synced 2026-05-31 04:56:20 +00:00
Reorganized into more logical units
This commit is contained in:
@@ -86,6 +86,7 @@ set(common_sources
|
|||||||
packet_dump_file.cpp
|
packet_dump_file.cpp
|
||||||
packet_dump.cpp
|
packet_dump.cpp
|
||||||
packet_functions.cpp
|
packet_functions.cpp
|
||||||
|
patches/client_version.cpp
|
||||||
patches/patches.cpp
|
patches/patches.cpp
|
||||||
patches/rof_limits.cpp
|
patches/rof_limits.cpp
|
||||||
patches/rof.cpp
|
patches/rof.cpp
|
||||||
@@ -655,6 +656,8 @@ set(common_headers
|
|||||||
packet_dump_file.h
|
packet_dump_file.h
|
||||||
packet_dump.h
|
packet_dump.h
|
||||||
packet_functions.h
|
packet_functions.h
|
||||||
|
patches/IMessage.h
|
||||||
|
patches/client_version.h
|
||||||
patches/patches.h
|
patches/patches.h
|
||||||
patches/rof_limits.h
|
patches/rof_limits.h
|
||||||
patches/rof_ops.h
|
patches/rof_ops.h
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* EQEmu: EQEmulator
|
||||||
|
|
||||||
|
Copyright (C) 2001-2026 EQEmu Development Team
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "client_version.h"
|
||||||
|
|
||||||
|
// Migration path: replace string_ids.h usage with ID enum values one call site at a time.
|
||||||
|
|
||||||
|
class Client;
|
||||||
|
class Mob;
|
||||||
|
class EQApplicationPacket;
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
concept AllConstChar = (std::is_convertible_v<Args, const char*> && ...);
|
||||||
|
|
||||||
|
class IMessage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IMessage() = default;
|
||||||
|
virtual ~IMessage() = default;
|
||||||
|
|
||||||
|
// these two are the basic string message packets
|
||||||
|
[[nodiscard]] virtual EQApplicationPacket* Simple(uint32_t color, uint32_t id) const = 0;
|
||||||
|
[[nodiscard]] virtual EQApplicationPacket* Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) 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, const char* spell_link) const = 0;
|
||||||
|
virtual EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id,
|
||||||
|
const char* name, const char* spell_link) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
@@ -18,19 +18,15 @@
|
|||||||
|
|
||||||
#include "client_version.h"
|
#include "client_version.h"
|
||||||
|
|
||||||
#include "zone/patch/components/message/titanium.h"
|
#include "common/patches/titanium.h"
|
||||||
#include "zone/patch/components/message/sof.h"
|
#include "common/patches/sof.h"
|
||||||
#include "zone/patch/components/message/sod.h"
|
#include "common/patches/sod.h"
|
||||||
#include "zone/patch/components/message/uf.h"
|
#include "common/patches/uf.h"
|
||||||
#include "zone/patch/components/message/rof.h"
|
#include "common/patches/rof.h"
|
||||||
#include "zone/patch/components/message/rof2.h"
|
#include "common/patches/rof2.h"
|
||||||
#include "zone/patch/components/message/tob.h"
|
#include "common/patches/tob.h"
|
||||||
|
|
||||||
#include "client.h"
|
|
||||||
#include "websocketpp/http/parser.hpp"
|
|
||||||
|
|
||||||
using Version = EQ::versions::ClientVersion;
|
using Version = EQ::versions::ClientVersion;
|
||||||
using namespace ZoneClient;
|
|
||||||
|
|
||||||
struct ClientComponents
|
struct ClientComponents
|
||||||
{
|
{
|
||||||
@@ -82,16 +78,7 @@ static const ClientComponents& GetComponents(Version version)
|
|||||||
return patches.at(version);
|
return patches.at(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::shared_ptr<Message::IMessage>& ClientPatch::GetMessageComponent(Version version)
|
const std::shared_ptr<Message::IMessage>& GetMessageComponent(Version version)
|
||||||
{
|
{
|
||||||
return GetComponents(version).messageComponent;
|
return GetComponents(version).messageComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::SetClientVersion(Version client_version)
|
|
||||||
{
|
|
||||||
m_ClientVersion = client_version;
|
|
||||||
m_ClientVersionBit = EQ::versions::ConvertClientVersionToClientVersionBit(client_version);
|
|
||||||
m_messageComponent = GetComponents(client_version).messageComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
Version Client::GetClientVersion() const { return m_ClientVersion; }
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// Created by dannu on 4/21/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/emu_versions.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace Message { class IMessage; }
|
||||||
|
|
||||||
|
// store all static functions for the different patches here
|
||||||
|
const std::shared_ptr<Message::IMessage>& GetMessageComponent(EQ::versions::ClientVersion version);
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "uf.h"
|
||||||
#include "common/struct_strategy.h"
|
#include "common/struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -48,3 +49,14 @@ namespace RoF
|
|||||||
};
|
};
|
||||||
|
|
||||||
} /*RoF*/
|
} /*RoF*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
class RoF : public UF
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RoF() = default;
|
||||||
|
~RoF() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "rof.h"
|
||||||
#include "common/struct_strategy.h"
|
#include "common/struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -48,3 +49,14 @@ namespace RoF2
|
|||||||
};
|
};
|
||||||
|
|
||||||
}; /*RoF2*/
|
}; /*RoF2*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
class RoF2 : public RoF
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RoF2() = default;
|
||||||
|
~RoF2() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "sof.h"
|
||||||
#include "common/struct_strategy.h"
|
#include "common/struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -48,3 +49,14 @@ namespace SoD
|
|||||||
};
|
};
|
||||||
|
|
||||||
} /*SoD*/
|
} /*SoD*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
class SoD : public SoF
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SoD() = default;
|
||||||
|
~SoD() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "titanium.h"
|
||||||
#include "common/struct_strategy.h"
|
#include "common/struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -48,3 +49,14 @@ namespace SoF
|
|||||||
};
|
};
|
||||||
|
|
||||||
} /*SoF*/
|
} /*SoF*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
class SoF : public Titanium
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SoF() = default;
|
||||||
|
~SoF() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "common/raid.h"
|
#include "common/raid.h"
|
||||||
#include "common/rulesys.h"
|
#include "common/rulesys.h"
|
||||||
#include "common/strings.h"
|
#include "common/strings.h"
|
||||||
|
#include "zone/string_ids.h"
|
||||||
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
@@ -3919,3 +3920,97 @@ namespace Titanium
|
|||||||
return index; // as long as we guard against bad slots server side, we should be fine
|
return index; // as long as we guard against bad slots server side, we should be fine
|
||||||
}
|
}
|
||||||
} /*Titanium*/
|
} /*Titanium*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
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));
|
||||||
|
auto* sms = reinterpret_cast<SimpleMessage_Struct*>(outapp->pBuffer);
|
||||||
|
sms->string_id = string_id;
|
||||||
|
sms->color = color;
|
||||||
|
sms->unknown8 = 0;
|
||||||
|
|
||||||
|
return outapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
EQApplicationPacket* Titanium::Formatted(
|
||||||
|
uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const
|
||||||
|
{
|
||||||
|
uint32_t string_id = ResolveID(id);
|
||||||
|
if (string_id > 0) {
|
||||||
|
std::array<const char*, 9> resolved_args = args;
|
||||||
|
ResolveArguments(id, resolved_args);
|
||||||
|
if (!resolved_args[0])
|
||||||
|
return Simple(color, id);
|
||||||
|
|
||||||
|
SerializeBuffer buf(20);
|
||||||
|
buf.WriteUInt32(0);
|
||||||
|
buf.WriteUInt32(string_id);
|
||||||
|
buf.WriteUInt32(color);
|
||||||
|
|
||||||
|
for (const auto* a : resolved_args) {
|
||||||
|
if (a != nullptr)
|
||||||
|
buf.WriteString(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteUInt8(0);
|
||||||
|
|
||||||
|
return new EQApplicationPacket(OP_FormattedMessage, std::move(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
EQApplicationPacket* Titanium::InterruptSpell(uint32_t message, uint32_t spawn_id, const char* spell_link) const
|
||||||
|
{
|
||||||
|
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct));
|
||||||
|
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||||
|
ic->messageid = ResolveID(message);
|
||||||
|
ic->spawnid = spawn_id;
|
||||||
|
outapp->priority = 5;
|
||||||
|
|
||||||
|
return outapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
EQApplicationPacket* Titanium::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
||||||
|
const char* spell_link) const
|
||||||
|
{
|
||||||
|
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(name) + 1);
|
||||||
|
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||||
|
ic->messageid = ResolveID(message);
|
||||||
|
ic->spawnid = spawn_id;
|
||||||
|
fmt::format_to_n(ic->message, strlen(name) + 1, "{}\0", name);
|
||||||
|
return outapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
// passthrough — string IDs are defined at the base client level;
|
||||||
|
// override in patches where IDs need remapping
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Titanium::ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case SPELL_FIZZLE:
|
||||||
|
case MISS_NOTE:
|
||||||
|
args[0] = nullptr; // drop spell link
|
||||||
|
break;
|
||||||
|
case SPELL_FIZZLE_OTHER:
|
||||||
|
case MISSED_NOTE_OTHER:
|
||||||
|
args[1] = nullptr; // drop spell link
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "IMessage.h"
|
||||||
#include "common/struct_strategy.h"
|
#include "common/struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -48,3 +49,27 @@ namespace Titanium
|
|||||||
};
|
};
|
||||||
|
|
||||||
} /*Titanium*/
|
} /*Titanium*/
|
||||||
|
|
||||||
|
// out-going message packets
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
class Titanium : public IMessage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Titanium() = default;
|
||||||
|
~Titanium() override = default;
|
||||||
|
|
||||||
|
[[nodiscard]] EQApplicationPacket* Simple(uint32_t color, uint32_t id) const override;
|
||||||
|
[[nodiscard]] EQApplicationPacket* Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const override;
|
||||||
|
|
||||||
|
EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, const char* spell_link) const override;
|
||||||
|
EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
||||||
|
const char* spell_link) const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
[[nodiscard]] virtual uint32_t ResolveID(uint32_t id) const;
|
||||||
|
virtual void ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|
||||||
|
|||||||
+163
-35
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
#include "common/packet_dump.h"
|
#include "common/packet_dump.h"
|
||||||
#include "world/sof_char_create_data.h"
|
#include "world/sof_char_create_data.h"
|
||||||
|
#include "zone/string_ids.h"
|
||||||
|
|
||||||
namespace TOB
|
namespace TOB
|
||||||
{
|
{
|
||||||
@@ -37,8 +38,8 @@ namespace TOB
|
|||||||
void SerializeItem(SerializeBuffer &buffer, const EQ::ItemInstance* inst, int16 slot_id, uint8 depth, ItemPacketType packet_type);
|
void SerializeItem(SerializeBuffer &buffer, const EQ::ItemInstance* inst, int16 slot_id, uint8 depth, ItemPacketType packet_type);
|
||||||
|
|
||||||
// message link converters
|
// message link converters
|
||||||
static inline void ServerToTOBConvertLinks(std::string& message_out, const std::string& message_in);
|
static void ServerToTOBConvertLinks(std::string& message_out, const std::string& message_in);
|
||||||
static inline void TOBToServerConvertLinks(std::string& message_out, const std::string& message_in);
|
static void TOBToServerConvertLinks(std::string& message_out, const std::string& message_in);
|
||||||
|
|
||||||
// SpawnAppearance
|
// SpawnAppearance
|
||||||
static inline uint32 ServerToTOBSpawnAppearanceType(uint32 server_type);
|
static inline uint32 ServerToTOBSpawnAppearanceType(uint32 server_type);
|
||||||
@@ -4743,60 +4744,59 @@ namespace TOB
|
|||||||
buffer.WriteInt32(0); //unsupported atm
|
buffer.WriteInt32(0); //unsupported atm
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void ServerToTOBConvertLinks(std::string& message_out, const std::string& message_in)
|
static void ServerToTOBConvertLinks(std::string& message_out, const std::string& message_in)
|
||||||
{
|
{
|
||||||
if (message_in.find('\x12') == std::string::npos) {
|
if (message_in.find('\x12') == std::string::npos) {
|
||||||
message_out = message_in;
|
message_out = message_in;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto segments = Strings::Split(message_in, '\x12');
|
std::vector<std::string> segments = Strings::Split(message_in, '\x12');
|
||||||
for (size_t segment_iter = 0; segment_iter < segments.size(); ++segment_iter) {
|
for (size_t segment_iter = 0; segment_iter < segments.size(); ++segment_iter) {
|
||||||
if (segment_iter & 1) {
|
if (segment_iter & 1) {
|
||||||
auto etag = std::stoi(segments[segment_iter].substr(0, 1));
|
auto etag = std::stoi(segments[segment_iter].substr(0, 1));
|
||||||
|
|
||||||
switch (etag) {
|
switch (etag) {
|
||||||
case 0:
|
case 0: {
|
||||||
{
|
|
||||||
size_t index = 1;
|
size_t index = 1;
|
||||||
auto item_id = segments[segment_iter].substr(index, 5);
|
std::string item_id = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto aug1 = segments[segment_iter].substr(index, 5);
|
std::string aug1 = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto aug2 = segments[segment_iter].substr(index, 5);
|
std::string aug2 = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto aug3 = segments[segment_iter].substr(index, 5);
|
std::string aug3 = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto aug4 = segments[segment_iter].substr(index, 5);
|
std::string aug4 = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto aug5 = segments[segment_iter].substr(index, 5);
|
std::string aug5 = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto aug6 = segments[segment_iter].substr(index, 5);
|
std::string aug6 = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto is_evolving = segments[segment_iter].substr(index, 1);
|
std::string is_evolving = segments[segment_iter].substr(index, 1);
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
auto evolutionGroup = segments[segment_iter].substr(index, 4);
|
std::string evolutionGroup = segments[segment_iter].substr(index, 4);
|
||||||
index += 4;
|
index += 4;
|
||||||
|
|
||||||
auto evolutionLevel = segments[segment_iter].substr(index, 2);
|
std::string evolutionLevel = segments[segment_iter].substr(index, 2);
|
||||||
index += 2;
|
index += 2;
|
||||||
|
|
||||||
auto ornamentationIconID = segments[segment_iter].substr(index, 5);
|
std::string ornamentationIconID = segments[segment_iter].substr(index, 5);
|
||||||
index += 5;
|
index += 5;
|
||||||
|
|
||||||
auto itemHash = segments[segment_iter].substr(index, 8);
|
std::string itemHash = segments[segment_iter].substr(index, 8);
|
||||||
index += 8;
|
index += 8;
|
||||||
|
|
||||||
auto text = segments[segment_iter].substr(index);
|
std::string text = segments[segment_iter].substr(index);
|
||||||
|
|
||||||
message_out.push_back('\x12');
|
message_out.push_back('\x12');
|
||||||
message_out.push_back('0'); //etag item
|
message_out.push_back('0'); //etag item
|
||||||
message_out.append(item_id);
|
message_out.append(item_id);
|
||||||
@@ -4830,14 +4830,13 @@ namespace TOB
|
|||||||
message_out.push_back('\x12');
|
message_out.push_back('\x12');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
message_out.append(segments[segment_iter]);
|
message_out.append(segments[segment_iter]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void TOBToServerConvertLinks(std::string& message_out, const std::string& message_in) {
|
static void TOBToServerConvertLinks(std::string& message_out, const std::string& message_in) {
|
||||||
message_out = message_in;
|
message_out = message_in;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5561,3 +5560,132 @@ namespace TOB
|
|||||||
}
|
}
|
||||||
} /*TOB*/
|
} /*TOB*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
struct TOBStringIDs
|
||||||
|
{
|
||||||
|
static constexpr uint32_t DisarmedTrap = 1458; // You successfully disarmed the trap
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t TOB::ResolveID(uint32_t id) const
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case YOU_FLURRY:
|
||||||
|
case BOW_DOUBLE_DAMAGE:
|
||||||
|
case NO_INSTRUMENT_SKILL:
|
||||||
|
case DISCIPLINE_CONLOST:
|
||||||
|
case TGB_ON:
|
||||||
|
case TGB_OFF:
|
||||||
|
case DISCIPLINE_RDY:
|
||||||
|
case SONG_NEEDS_DRUM:
|
||||||
|
case SONG_NEEDS_WIND:
|
||||||
|
case SONG_NEEDS_STRINGS:
|
||||||
|
case SONG_NEEDS_BRASS:
|
||||||
|
case YOU_CRIT_HEAL:
|
||||||
|
case YOU_CRIT_BLAST:
|
||||||
|
case SPELL_WORN_OFF:
|
||||||
|
case PET_TAUNTING:
|
||||||
|
case DISC_LEVEL_ERROR:
|
||||||
|
case MALE_SLAYUNDEAD:
|
||||||
|
case FEMALE_SLAYUNDEAD:
|
||||||
|
case FINISHING_BLOW:
|
||||||
|
case ASSASSINATES:
|
||||||
|
case CRIPPLING_BLOW:
|
||||||
|
case CRITICAL_HIT:
|
||||||
|
case DEADLY_STRIKE:
|
||||||
|
case OTHER_CRIT_HEAL:
|
||||||
|
case OTHER_CRIT_BLAST:
|
||||||
|
case NPC_RAMPAGE:
|
||||||
|
case NPC_FLURRY:
|
||||||
|
case DISCIPLINE_FEARLESS:
|
||||||
|
case CORPSE_ITEM_LOST:
|
||||||
|
case FATAL_BOW_SHOT:
|
||||||
|
case CURRENT_SPELL_EFFECTS:
|
||||||
|
case NOT_DELEGATED_MARKER:
|
||||||
|
case STRIKETHROUGH_STRING:
|
||||||
|
case AE_RAMPAGE:
|
||||||
|
case DISC_LEVEL_USE_ERROR:
|
||||||
|
case SPLIT_FAIL:
|
||||||
|
// removed from the client
|
||||||
|
return 0;
|
||||||
|
case DISARMED_TRAP:
|
||||||
|
return TOBStringIDs::DisarmedTrap;
|
||||||
|
default:
|
||||||
|
return RoF2::ResolveID(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TOB::ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case SPELL_FIZZLE:
|
||||||
|
case MISS_NOTE:
|
||||||
|
case SPELL_FIZZLE_OTHER:
|
||||||
|
case MISSED_NOTE_OTHER:
|
||||||
|
// take all arguments (spell link)
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
RoF2::ResolveArguments(id, args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EQApplicationPacket* TOB::Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const
|
||||||
|
{
|
||||||
|
uint32_t string_id = ResolveID(id);
|
||||||
|
if (string_id > 0) {
|
||||||
|
std::array<const char*, 9> resolved_args = args;
|
||||||
|
ResolveArguments(id, resolved_args);
|
||||||
|
if (!resolved_args[0])
|
||||||
|
return Simple(color, id);
|
||||||
|
|
||||||
|
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 : resolved_args) {
|
||||||
|
if (a != nullptr) {
|
||||||
|
std::string new_message;
|
||||||
|
::TOB::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, const char* spell_link) const
|
||||||
|
{
|
||||||
|
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(spell_link) + 1);
|
||||||
|
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||||
|
ic->messageid = ResolveID(message);
|
||||||
|
ic->spawnid = spawn_id;
|
||||||
|
fmt::format_to_n(ic->message, strlen(spell_link) + 1, "{}\0", spell_link);
|
||||||
|
outapp->priority = 5;
|
||||||
|
|
||||||
|
return outapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
EQApplicationPacket* TOB::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
||||||
|
const char* spell_link) const
|
||||||
|
{
|
||||||
|
auto outapp = new EQApplicationPacket(OP_InterruptCast,
|
||||||
|
sizeof(InterruptCast_Struct) + strlen(name) + strlen(spell_link) + 2);
|
||||||
|
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
||||||
|
ic->messageid = ResolveID(message);
|
||||||
|
ic->spawnid = spawn_id;
|
||||||
|
fmt::format_to_n(ic->message, strlen(name) + strlen(spell_link) + 2, "{}\0{}\0", name, spell_link);
|
||||||
|
|
||||||
|
return outapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|
||||||
|
|||||||
+21
-3
@@ -1,6 +1,6 @@
|
|||||||
#ifndef COMMON_LAURION_H
|
#pragma once
|
||||||
#define COMMON_LAURION_H
|
|
||||||
|
|
||||||
|
#include "rof2.h"
|
||||||
#include "../struct_strategy.h"
|
#include "../struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -34,4 +34,22 @@ namespace TOB
|
|||||||
|
|
||||||
}; /*TOB*/
|
}; /*TOB*/
|
||||||
|
|
||||||
#endif /*COMMON_LAURION_H*/
|
namespace Message {
|
||||||
|
|
||||||
|
class TOB : public RoF2
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TOB() {}
|
||||||
|
~TOB() override {}
|
||||||
|
|
||||||
|
[[nodiscard]] EQApplicationPacket* Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const override;
|
||||||
|
|
||||||
|
EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, const char* spell_link) const override;
|
||||||
|
EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name, const char* spell_link) const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
[[nodiscard]] uint32_t ResolveID(uint32_t id) const override;
|
||||||
|
void ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "sod.h"
|
||||||
#include "common/struct_strategy.h"
|
#include "common/struct_strategy.h"
|
||||||
|
|
||||||
class EQStreamIdentifier;
|
class EQStreamIdentifier;
|
||||||
@@ -48,3 +49,14 @@ namespace UF
|
|||||||
};
|
};
|
||||||
|
|
||||||
}; /*UF*/
|
}; /*UF*/
|
||||||
|
|
||||||
|
namespace Message {
|
||||||
|
|
||||||
|
class UF : public SoD
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
UF() = default;
|
||||||
|
~UF() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
|
|||||||
+2
-2
@@ -28,6 +28,7 @@ set(zone_sources
|
|||||||
client_mods.cpp
|
client_mods.cpp
|
||||||
client_packet.cpp
|
client_packet.cpp
|
||||||
client_process.cpp
|
client_process.cpp
|
||||||
|
client_version.cpp
|
||||||
combat_record.cpp
|
combat_record.cpp
|
||||||
corpse.cpp
|
corpse.cpp
|
||||||
dialogue_window.cpp
|
dialogue_window.cpp
|
||||||
@@ -130,6 +131,7 @@ set(zone_headers
|
|||||||
cheat_manager.h
|
cheat_manager.h
|
||||||
client.h
|
client.h
|
||||||
client_packet.h
|
client_packet.h
|
||||||
|
client_version.h
|
||||||
combat_record.h
|
combat_record.h
|
||||||
command.h
|
command.h
|
||||||
common.h
|
common.h
|
||||||
@@ -675,8 +677,6 @@ add_executable(zone ${zone_sources} ${zone_headers})
|
|||||||
|
|
||||||
target_include_directories(zone PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(zone PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
add_subdirectory(patch)
|
|
||||||
|
|
||||||
install(TARGETS zone RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
|
install(TARGETS zone RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
|
||||||
|
|
||||||
if(EQEMU_BUILD_PCH)
|
if(EQEMU_BUILD_PCH)
|
||||||
|
|||||||
+5
-6
@@ -44,10 +44,9 @@
|
|||||||
#include "common/spdat.h"
|
#include "common/spdat.h"
|
||||||
#include "common/strings.h"
|
#include "common/strings.h"
|
||||||
#include "common/zone_store.h"
|
#include "common/zone_store.h"
|
||||||
#include "patch/client_version.h"
|
|
||||||
#include "patch/components/message/IMessage.h"
|
|
||||||
#include "zone/bot_command.h"
|
#include "zone/bot_command.h"
|
||||||
#include "zone/cheat_manager.h"
|
#include "zone/cheat_manager.h"
|
||||||
|
#include "zone/client_version.h"
|
||||||
#include "zone/command.h"
|
#include "zone/command.h"
|
||||||
#include "zone/dialogue_window.h"
|
#include "zone/dialogue_window.h"
|
||||||
#include "zone/dynamic_zone.h"
|
#include "zone/dynamic_zone.h"
|
||||||
@@ -3812,10 +3811,10 @@ void Client::MessageString(uint32 type, uint32 string_id, uint32 distance)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (distance > 0)
|
if (distance > 0)
|
||||||
ZoneClient::Message::CloseMessageString(this, false, static_cast<float>(distance))(
|
Message::CloseMessageString(this, false, static_cast<float>(distance))(
|
||||||
type, string_id);
|
type, string_id);
|
||||||
else
|
else
|
||||||
ZoneClient::Message::MessageString(this, type, string_id);
|
Message::MessageString(this, type, string_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -3844,10 +3843,10 @@ void Client::MessageString(uint32 type, uint32 string_id, const char* message1,
|
|||||||
type = 4;
|
type = 4;
|
||||||
|
|
||||||
if (distance > 0)
|
if (distance > 0)
|
||||||
ZoneClient::Message::CloseMessageString(this, false, static_cast<float>(distance))(type, string_id, message1,
|
Message::CloseMessageString(this, false, static_cast<float>(distance))(type, string_id, message1,
|
||||||
message2, message3, message4, message5, message6, message7, message8, message9);
|
message2, message3, message4, message5, message6, message7, message8, message9);
|
||||||
else
|
else
|
||||||
ZoneClient::Message::MessageString(this, type, string_id, message1, message2, message3, message4, message5,
|
Message::MessageString(this, type, string_id, message1, message2, message3, message4, message5,
|
||||||
message6, message7, message8, message9);
|
message6, message7, message8, message9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
class Client;
|
class Client;
|
||||||
class EQApplicationPacket;
|
class EQApplicationPacket;
|
||||||
namespace ZoneClient::Message { class IMessage; }
|
|
||||||
class DynamicZone;
|
class DynamicZone;
|
||||||
class DzLockout;
|
class DzLockout;
|
||||||
class ExpeditionRequest;
|
class ExpeditionRequest;
|
||||||
@@ -1553,7 +1552,6 @@ public:
|
|||||||
inline const uint32 ClientVersionBit() const { return m_ClientVersionBit; }
|
inline const uint32 ClientVersionBit() const { return m_ClientVersionBit; }
|
||||||
void SetClientVersion(EQ::versions::ClientVersion client_version);
|
void SetClientVersion(EQ::versions::ClientVersion client_version);
|
||||||
EQ::versions::ClientVersion GetClientVersion() const;
|
EQ::versions::ClientVersion GetClientVersion() const;
|
||||||
const std::shared_ptr<ZoneClient::Message::IMessage>& GetMessageComponent() const { return m_messageComponent; }
|
|
||||||
|
|
||||||
/** Adventure Stuff **/
|
/** Adventure Stuff **/
|
||||||
void SendAdventureError(const char *error);
|
void SendAdventureError(const char *error);
|
||||||
@@ -2283,7 +2281,6 @@ private:
|
|||||||
|
|
||||||
EQ::versions::ClientVersion m_ClientVersion;
|
EQ::versions::ClientVersion m_ClientVersion;
|
||||||
uint32 m_ClientVersionBit;
|
uint32 m_ClientVersionBit;
|
||||||
std::shared_ptr<ZoneClient::Message::IMessage> m_messageComponent;
|
|
||||||
|
|
||||||
int XPRate;
|
int XPRate;
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,15 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "zone/patch/components/message/uf.h"
|
#include "client_version.h"
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
using Version = EQ::versions::ClientVersion;
|
||||||
class RoF : public UF
|
|
||||||
|
void Client::SetClientVersion(Version client_version)
|
||||||
{
|
{
|
||||||
public:
|
m_ClientVersion = client_version;
|
||||||
RoF() {}
|
m_ClientVersionBit = EQ::versions::ConvertClientVersionToClientVersionBit(client_version);
|
||||||
~RoF() override {}
|
}
|
||||||
};
|
|
||||||
} // namespace Zone::Message
|
Version Client::GetClientVersion() const { return m_ClientVersion; }
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
//
|
||||||
|
// Created by dannu on 4/21/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
#include "common/emu_versions.h"
|
||||||
|
#include "common/patches/client_version.h"
|
||||||
|
#include "common/patches/IMessage.h"
|
||||||
|
|
||||||
|
#include "zone/client.h"
|
||||||
|
#include "zone/mob.h"
|
||||||
|
|
||||||
|
// store all _generic_ static functions for the different patches here
|
||||||
|
namespace ClientPatch {
|
||||||
|
|
||||||
|
using ClientList = std::unordered_map<uint16, Client*>;
|
||||||
|
|
||||||
|
template <typename Fun, typename Obj, typename... Args>
|
||||||
|
static void QueuePacket(Client* c, Fun fun, Obj* obj, Args&&... args)
|
||||||
|
{
|
||||||
|
static_assert(std::is_member_function_pointer_v<Fun>);
|
||||||
|
EQApplicationPacket* app = std::invoke(fun, obj, std::forward<Args>(args)...);
|
||||||
|
if (app != nullptr) {
|
||||||
|
c->QueuePacket(app);
|
||||||
|
delete app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// packet generator queue functions
|
||||||
|
static auto QueueClients(Mob* sender, bool ignore_sender = false, bool ackreq = true)
|
||||||
|
{
|
||||||
|
return [=]<typename Fun, typename Obj, typename... Args>(Fun fun,
|
||||||
|
std::function<Obj*(const Client*)> component_getter, Args&&... args) {
|
||||||
|
static_assert(std::is_member_function_pointer_v<Fun> && "Function is required to be a member function");
|
||||||
|
|
||||||
|
std::unordered_map<EQ::versions::ClientVersion, EQApplicationPacket*> build_packets;
|
||||||
|
std::unordered_map<uint16, Client*> client_list = entity_list.GetClientList();
|
||||||
|
|
||||||
|
for (auto [_, ent] : client_list) {
|
||||||
|
if (!ignore_sender || ent != sender) {
|
||||||
|
auto [packet, _] = build_packets.try_emplace(
|
||||||
|
ent->ClientVersion(),
|
||||||
|
std::invoke(fun, component_getter(ent), std::forward<Args>(args)...));
|
||||||
|
|
||||||
|
if (packet->second != nullptr)
|
||||||
|
ent->QueuePacket(packet->second, ackreq, Client::CLIENT_CONNECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto [_, packet] : build_packets)
|
||||||
|
if (packet != nullptr)
|
||||||
|
delete packet;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto QueueCloseClients(
|
||||||
|
Mob* sender, bool ignore_sender = false, float distance = 200,
|
||||||
|
Mob* skipped_mob = nullptr, bool is_ack_required = true,
|
||||||
|
eqFilterType filter = FilterNone)
|
||||||
|
{
|
||||||
|
if (distance <= 0) distance = static_cast<float>(zone->GetClientUpdateRange());
|
||||||
|
|
||||||
|
return [=]<typename Fun, typename Obj, typename... Args>(Fun fun,
|
||||||
|
std::function<Obj*(const Client*)> component_getter, Args&&... args) {
|
||||||
|
static_assert(std::is_member_function_pointer_v<Fun>, "Function is required to be a member function");
|
||||||
|
|
||||||
|
if (sender == nullptr) {
|
||||||
|
QueueClients(sender, ignore_sender, is_ack_required)(fun, component_getter, std::forward<Args>(args)...);
|
||||||
|
} else {
|
||||||
|
float distance_squared = distance * distance;
|
||||||
|
std::unordered_map<EQ::versions::ClientVersion, EQApplicationPacket*> build_packets;
|
||||||
|
|
||||||
|
for (auto& [_, mob] : sender->GetCloseMobList(distance)) {
|
||||||
|
if (mob && mob->IsClient()) {
|
||||||
|
Client* client = mob->CastToClient();
|
||||||
|
if ((!ignore_sender || client != sender)
|
||||||
|
&& client != skipped_mob
|
||||||
|
&& DistanceSquared(client->GetPosition(), sender->GetPosition()) < distance_squared
|
||||||
|
&& client->Connected()
|
||||||
|
&& client->ShouldGetPacket(sender, filter)) {
|
||||||
|
auto [packet, _] = build_packets.try_emplace(
|
||||||
|
client->ClientVersion(),
|
||||||
|
std::invoke(fun, component_getter(client), std::forward<Args>(args)...));
|
||||||
|
|
||||||
|
if (packet->second != nullptr)
|
||||||
|
client->QueuePacket(packet->second, is_ack_required, Client::CLIENT_CONNECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto [_, packet] : build_packets)
|
||||||
|
if (packet != nullptr)
|
||||||
|
delete packet;;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ClientPatch
|
||||||
|
|
||||||
|
// Helpers for the Message interface to send message packets
|
||||||
|
namespace Message {
|
||||||
|
static std::function GetComponent = [](const Client* c) -> IMessage* {
|
||||||
|
return GetMessageComponent(c->GetClientVersion()).get();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions to wrap the packet construction in sends
|
||||||
|
template <AllConstChar... Args>
|
||||||
|
requires (sizeof...(Args) <= 9)
|
||||||
|
void MessageString(Client* c, uint32_t type, uint32_t id, Args&&... args)
|
||||||
|
{
|
||||||
|
if constexpr (sizeof...(Args) == 0) {
|
||||||
|
ClientPatch::QueuePacket(c, &IMessage::Simple, GetComponent(c), type, id);
|
||||||
|
} else {
|
||||||
|
std::array<const char*, 9> a = {args...};
|
||||||
|
ClientPatch::QueuePacket(c, &IMessage::Formatted, GetComponent(c), type, id, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto CloseMessageString(
|
||||||
|
Mob* sender, bool ignore_sender = false, float distance = 200.f,
|
||||||
|
Mob* skipped_mob = nullptr, bool is_ack_required = true,
|
||||||
|
eqFilterType filter = FilterNone)
|
||||||
|
{
|
||||||
|
return [=]<AllConstChar... Args>(uint32_t type, uint32_t id, Args&&... args) {
|
||||||
|
static_assert(sizeof...(Args) <= 9, "Too many arguments");
|
||||||
|
|
||||||
|
auto queue_close_clients = ClientPatch::QueueCloseClients(sender, ignore_sender, distance, skipped_mob,
|
||||||
|
is_ack_required, filter);
|
||||||
|
|
||||||
|
if constexpr (sizeof...(Args) == 0) {
|
||||||
|
return queue_close_clients(&IMessage::Simple, GetComponent, type, id);
|
||||||
|
} else {
|
||||||
|
std::array<const char*, 9> a = {args...};
|
||||||
|
return queue_close_clients(&IMessage::Formatted, GetComponent, type, id, a);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void InterruptSpell(Client* c, uint32_t message, uint32_t spawn_id, const char* spell_link)
|
||||||
|
{
|
||||||
|
ClientPatch::QueuePacket(c, &IMessage::InterruptSpell, GetComponent(c), message, spawn_id, spell_link);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
||||||
|
const char* spell_link)
|
||||||
|
{
|
||||||
|
ClientPatch::QueueCloseClients(sender, true, RuleI(Range, SongMessages), nullptr, true,
|
||||||
|
sender->IsClient() ? FilterPCSpells : FilterNPCSpells)(
|
||||||
|
&IMessage::InterruptSpellOther, GetComponent, sender, message, spawn_id, name, spell_link);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Message
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
set(patch_sources
|
|
||||||
client_version.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
set(patch_headers
|
|
||||||
client_version.h
|
|
||||||
)
|
|
||||||
|
|
||||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${patch_sources})
|
|
||||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Header Files" FILES ${patch_headers})
|
|
||||||
|
|
||||||
target_sources(zone PRIVATE ${patch_sources} ${patch_headers})
|
|
||||||
|
|
||||||
add_subdirectory(components)
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by dannu on 4/21/2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "common/emu_versions.h"
|
|
||||||
|
|
||||||
#include "zone/client.h"
|
|
||||||
#include "zone/mob.h"
|
|
||||||
|
|
||||||
namespace ZoneClient {
|
|
||||||
|
|
||||||
namespace Message { class IMessage; }
|
|
||||||
|
|
||||||
// store all static functions for the different patches here
|
|
||||||
class ClientPatch {
|
|
||||||
public:
|
|
||||||
using ClientList = std::unordered_map<uint16, Client*>;
|
|
||||||
static const std::shared_ptr<Message::IMessage>& GetMessageComponent(EQ::versions::ClientVersion version);
|
|
||||||
|
|
||||||
template<typename Fun, typename Obj, typename... Args>
|
|
||||||
static void QueuePacket(Client* c, Fun fun, Obj* obj, Args&&... args)
|
|
||||||
{
|
|
||||||
static_assert(std::is_member_function_pointer_v<Fun>);
|
|
||||||
EQApplicationPacket* app = std::invoke(fun, obj, std::forward<Args>(args)...);
|
|
||||||
if (app != nullptr) {
|
|
||||||
c->QueuePacket(app);
|
|
||||||
delete app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// packet generator queue functions
|
|
||||||
static auto QueueClients(Mob* sender, bool ignore_sender = false, bool ackreq = true)
|
|
||||||
{
|
|
||||||
return [=]<typename Fun, typename Obj, typename... Args>(Fun fun, std::function<Obj*(const Client*)> component_getter, Args&&... args) {
|
|
||||||
static_assert(std::is_member_function_pointer_v<Fun> && "Function is required to be a member function");
|
|
||||||
|
|
||||||
std::unordered_map<EQ::versions::ClientVersion, EQApplicationPacket*> build_packets;
|
|
||||||
std::unordered_map<uint16, Client*> client_list = entity_list.GetClientList();
|
|
||||||
|
|
||||||
for (auto [_, ent] : client_list) {
|
|
||||||
if (!ignore_sender || ent != sender) {
|
|
||||||
auto [packet, _] = build_packets.try_emplace(
|
|
||||||
ent->ClientVersion(),
|
|
||||||
std::invoke(fun, component_getter(ent), std::forward<Args>(args)...));
|
|
||||||
|
|
||||||
if (packet->second != nullptr)
|
|
||||||
ent->QueuePacket(packet->second, ackreq, Client::CLIENT_CONNECTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto [_, packet] : build_packets)
|
|
||||||
if (packet != nullptr)
|
|
||||||
delete packet;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static auto QueueCloseClients(
|
|
||||||
Mob* sender, bool ignore_sender = false, float distance = 200,
|
|
||||||
Mob* skipped_mob = nullptr, bool is_ack_required = true,
|
|
||||||
eqFilterType filter = FilterNone)
|
|
||||||
{
|
|
||||||
if (distance <= 0) distance = static_cast<float>(zone->GetClientUpdateRange());
|
|
||||||
|
|
||||||
return [=]<typename Fun, typename Obj, typename... Args>(Fun fun, std::function<Obj*(const Client*)> component_getter, Args&&... args) {
|
|
||||||
static_assert(std::is_member_function_pointer_v<Fun> && "Function is required to be a member function");
|
|
||||||
|
|
||||||
if (sender == nullptr) {
|
|
||||||
QueueClients(sender, ignore_sender, is_ack_required)(fun, component_getter, std::forward<Args>(args)...);
|
|
||||||
} else {
|
|
||||||
float distance_squared = distance * distance;
|
|
||||||
std::unordered_map<EQ::versions::ClientVersion, EQApplicationPacket*> build_packets;
|
|
||||||
|
|
||||||
for (auto& [_, mob] : sender->GetCloseMobList(distance)) {
|
|
||||||
if (mob && mob->IsClient()) {
|
|
||||||
Client* client = mob->CastToClient();
|
|
||||||
if ((!ignore_sender || client != sender)
|
|
||||||
&& client != skipped_mob
|
|
||||||
&& DistanceSquared(client->GetPosition(), sender->GetPosition()) < distance_squared
|
|
||||||
&& client->Connected()
|
|
||||||
&& client->ShouldGetPacket(sender, filter)) {
|
|
||||||
auto [packet, _] = build_packets.try_emplace(
|
|
||||||
client->ClientVersion(),
|
|
||||||
std::invoke(fun, component_getter(client), std::forward<Args>(args)...));
|
|
||||||
|
|
||||||
if (packet->second != nullptr)
|
|
||||||
client->QueuePacket(packet->second, is_ack_required, Client::CLIENT_CONNECTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto [_, packet] : build_packets)
|
|
||||||
if (packet != nullptr)
|
|
||||||
delete packet;;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
add_subdirectory(message)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
set(message_component_sources
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${message_component_sources})
|
|
||||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Header Files" FILES ${message_component_headers})
|
|
||||||
|
|
||||||
target_sources(zone PRIVATE ${message_component_sources} ${message_component_headers})
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "patch/client_version.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
// Migration path: replace string_ids.h usage with ID enum values one call site at a time.
|
|
||||||
|
|
||||||
class Client;
|
|
||||||
class Mob;
|
|
||||||
class EQApplicationPacket;
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
|
|
||||||
template<typename... Args>
|
|
||||||
concept AllConstChar = (std::is_convertible_v<Args, const char*> && ...);
|
|
||||||
|
|
||||||
class IMessage
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
IMessage() = default;
|
|
||||||
virtual ~IMessage() = default;
|
|
||||||
|
|
||||||
// these two are the basic string message packets
|
|
||||||
[[nodiscard]] virtual EQApplicationPacket* Simple(uint32_t color, uint32_t id) const = 0;
|
|
||||||
[[nodiscard]] virtual EQApplicationPacket* Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) 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, const char* spell_link) const = 0;
|
|
||||||
virtual EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id,
|
|
||||||
const char* name, const char* spell_link) const = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
static std::function GetComponent = [](const Client* c) -> IMessage* {
|
|
||||||
return ClientPatch::GetMessageComponent(c->GetClientVersion()).get();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions to wrap the packet construction in sends
|
|
||||||
template <AllConstChar... Args>
|
|
||||||
requires (sizeof...(Args) <= 9)
|
|
||||||
void MessageString(Client* c, uint32_t type, uint32_t id, Args&&... args)
|
|
||||||
{
|
|
||||||
if constexpr (sizeof...(Args) == 0) {
|
|
||||||
ClientPatch::QueuePacket(c, &IMessage::Simple, c->GetMessageComponent().get(), type, id);
|
|
||||||
} else {
|
|
||||||
std::array<const char*, 9> a = {args...};
|
|
||||||
ClientPatch::QueuePacket(c, &IMessage::Formatted, c->GetMessageComponent().get(), type, id, a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static auto CloseMessageString(
|
|
||||||
Mob* sender, bool ignore_sender = false, float distance = 200.f,
|
|
||||||
Mob* skipped_mob = nullptr, bool is_ack_required = true,
|
|
||||||
eqFilterType filter = FilterNone)
|
|
||||||
{
|
|
||||||
return [=]<AllConstChar... Args>(uint32_t type, uint32_t id, Args&&... args) {
|
|
||||||
static_assert(sizeof...(Args) <= 9, "Too many arguments");
|
|
||||||
|
|
||||||
auto queue_close_clients = ClientPatch::QueueCloseClients(sender, ignore_sender, distance, skipped_mob,
|
|
||||||
is_ack_required, filter);
|
|
||||||
|
|
||||||
if constexpr (sizeof...(Args) == 0) {
|
|
||||||
return queue_close_clients(&IMessage::Simple, GetComponent, type, id);
|
|
||||||
} else {
|
|
||||||
std::array<const char*, 9> a = {args...};
|
|
||||||
return queue_close_clients(&IMessage::Formatted, GetComponent, type, id, a);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void InterruptSpell(Client* c, uint32_t message, uint32_t spawn_id, const char* spell_link)
|
|
||||||
{
|
|
||||||
ClientPatch::QueuePacket(c, &IMessage::InterruptSpell, c->GetMessageComponent().get(), message, spawn_id, spell_link);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name, const char* spell_link)
|
|
||||||
{
|
|
||||||
ClientPatch::QueueCloseClients(sender, true, RuleI(Range, SongMessages), nullptr, true,
|
|
||||||
sender->IsClient() ? FilterPCSpells : FilterNPCSpells)(
|
|
||||||
&IMessage::InterruptSpellOther, GetComponent, sender, message, spawn_id, name, spell_link);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZoneClient::Message
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#include "zone/patch/components/message/rof2.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
|
|
||||||
} // namespace Zone::Message
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "zone/patch/components/message/rof.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
class RoF2 : public RoF
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
RoF2() {}
|
|
||||||
~RoF2() override {}
|
|
||||||
};
|
|
||||||
} // namespace Zone::Message
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "zone/patch/components/message/sof.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
class SoD : public SoF
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
SoD() {}
|
|
||||||
~SoD() override {}
|
|
||||||
};
|
|
||||||
} // namespace Zone::Message
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "zone/patch/components/message/titanium.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
class SoF : public Titanium
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
SoF() {}
|
|
||||||
~SoF() override {}
|
|
||||||
};
|
|
||||||
} // namespace Zone::Message
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#include "zone/patch/components/message/titanium.h"
|
|
||||||
|
|
||||||
#include "client.h"
|
|
||||||
|
|
||||||
#include "common/eq_packet.h"
|
|
||||||
#include "common/eq_packet_structs.h"
|
|
||||||
#include "common/serialize_buffer.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
|
|
||||||
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));
|
|
||||||
auto* sms = reinterpret_cast<SimpleMessage_Struct*>(outapp->pBuffer);
|
|
||||||
sms->string_id = string_id;
|
|
||||||
sms->color = color;
|
|
||||||
sms->unknown8 = 0;
|
|
||||||
|
|
||||||
return outapp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
EQApplicationPacket* Titanium::Formatted(
|
|
||||||
uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const
|
|
||||||
{
|
|
||||||
uint32_t string_id = ResolveID(id);
|
|
||||||
if (string_id > 0) {
|
|
||||||
std::array<const char*, 9> resolved_args = args;
|
|
||||||
ResolveArguments(id, resolved_args);
|
|
||||||
if (!resolved_args[0])
|
|
||||||
return Simple(color, id);
|
|
||||||
|
|
||||||
SerializeBuffer buf(20);
|
|
||||||
buf.WriteUInt32(0);
|
|
||||||
buf.WriteUInt32(string_id);
|
|
||||||
buf.WriteUInt32(color);
|
|
||||||
|
|
||||||
for (const auto* a : resolved_args) {
|
|
||||||
if (a != nullptr)
|
|
||||||
buf.WriteString(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteUInt8(0);
|
|
||||||
|
|
||||||
return new EQApplicationPacket(OP_FormattedMessage, std::move(buf));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
EQApplicationPacket* Titanium::InterruptSpell(uint32_t message, uint32_t spawn_id, const char* spell_link) const
|
|
||||||
{
|
|
||||||
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct));
|
|
||||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
|
||||||
ic->messageid = ResolveID(message);
|
|
||||||
ic->spawnid = spawn_id;
|
|
||||||
outapp->priority = 5;
|
|
||||||
|
|
||||||
return outapp;
|
|
||||||
}
|
|
||||||
|
|
||||||
EQApplicationPacket* Titanium::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
|
||||||
const char* spell_link) const
|
|
||||||
{
|
|
||||||
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(name) + 1);
|
|
||||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
|
||||||
ic->messageid = ResolveID(message);
|
|
||||||
ic->spawnid = spawn_id;
|
|
||||||
fmt::format_to_n(ic->message, strlen(name) + 1, "{}\0", name);
|
|
||||||
return outapp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
{
|
|
||||||
// passthrough — string IDs are defined at the base client level;
|
|
||||||
// override in patches where IDs need remapping
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Titanium::ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const
|
|
||||||
{
|
|
||||||
switch (id) {
|
|
||||||
case SPELL_FIZZLE:
|
|
||||||
case MISS_NOTE:
|
|
||||||
args[0] = nullptr; // drop spell link
|
|
||||||
break;
|
|
||||||
case SPELL_FIZZLE_OTHER:
|
|
||||||
case MISSED_NOTE_OTHER:
|
|
||||||
args[1] = nullptr; // drop spell link
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace ZoneClient::Message
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "zone/patch/components/message/IMessage.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
class Titanium : public IMessage
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
Titanium() = default;
|
|
||||||
~Titanium() override = default;
|
|
||||||
|
|
||||||
[[nodiscard]] EQApplicationPacket* Simple(uint32_t color, uint32_t id) const override;
|
|
||||||
[[nodiscard]] EQApplicationPacket* Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const override;
|
|
||||||
|
|
||||||
EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, const char* spell_link) const override;
|
|
||||||
EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
|
||||||
const char* spell_link) const override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
[[nodiscard]] virtual uint32_t ResolveID(uint32_t id) const;
|
|
||||||
virtual void ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const;
|
|
||||||
};
|
|
||||||
} // namespace ZoneClient::Message
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#include "zone/patch/components/message/tob.h"
|
|
||||||
|
|
||||||
#include "common/links.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
struct TOBStringIDs
|
|
||||||
{
|
|
||||||
static constexpr uint32_t DisarmedTrap = 1458; // You successfully disarmed the trap
|
|
||||||
};
|
|
||||||
|
|
||||||
uint32_t TOB::ResolveID(uint32_t id) const
|
|
||||||
{
|
|
||||||
switch (id) {
|
|
||||||
case YOU_FLURRY:
|
|
||||||
case BOW_DOUBLE_DAMAGE:
|
|
||||||
case NO_INSTRUMENT_SKILL:
|
|
||||||
case DISCIPLINE_CONLOST:
|
|
||||||
case TGB_ON:
|
|
||||||
case TGB_OFF:
|
|
||||||
case DISCIPLINE_RDY:
|
|
||||||
case SONG_NEEDS_DRUM:
|
|
||||||
case SONG_NEEDS_WIND:
|
|
||||||
case SONG_NEEDS_STRINGS:
|
|
||||||
case SONG_NEEDS_BRASS:
|
|
||||||
case YOU_CRIT_HEAL:
|
|
||||||
case YOU_CRIT_BLAST:
|
|
||||||
case SPELL_WORN_OFF:
|
|
||||||
case PET_TAUNTING:
|
|
||||||
case DISC_LEVEL_ERROR:
|
|
||||||
case MALE_SLAYUNDEAD:
|
|
||||||
case FEMALE_SLAYUNDEAD:
|
|
||||||
case FINISHING_BLOW:
|
|
||||||
case ASSASSINATES:
|
|
||||||
case CRIPPLING_BLOW:
|
|
||||||
case CRITICAL_HIT:
|
|
||||||
case DEADLY_STRIKE:
|
|
||||||
case OTHER_CRIT_HEAL:
|
|
||||||
case OTHER_CRIT_BLAST:
|
|
||||||
case NPC_RAMPAGE:
|
|
||||||
case NPC_FLURRY:
|
|
||||||
case DISCIPLINE_FEARLESS:
|
|
||||||
case CORPSE_ITEM_LOST:
|
|
||||||
case FATAL_BOW_SHOT:
|
|
||||||
case CURRENT_SPELL_EFFECTS:
|
|
||||||
case NOT_DELEGATED_MARKER:
|
|
||||||
case STRIKETHROUGH_STRING:
|
|
||||||
case AE_RAMPAGE:
|
|
||||||
case DISC_LEVEL_USE_ERROR:
|
|
||||||
case SPLIT_FAIL:
|
|
||||||
// removed from the client
|
|
||||||
return 0;
|
|
||||||
case DISARMED_TRAP:
|
|
||||||
return TOBStringIDs::DisarmedTrap;
|
|
||||||
default:
|
|
||||||
return RoF2::ResolveID(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TOB::ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const
|
|
||||||
{
|
|
||||||
switch (id) {
|
|
||||||
case SPELL_FIZZLE:
|
|
||||||
case MISS_NOTE:
|
|
||||||
case SPELL_FIZZLE_OTHER:
|
|
||||||
case MISSED_NOTE_OTHER:
|
|
||||||
// take all arguments (spell link)
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
RoF2::ResolveArguments(id, args);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> 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;
|
|
||||||
std::string item_id = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string aug1 = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string aug2 = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string aug3 = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string aug4 = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string aug5 = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string aug6 = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string is_evolving = segments[segment_iter].substr(index, 1);
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
std::string evolutionGroup = segments[segment_iter].substr(index, 4);
|
|
||||||
index += 4;
|
|
||||||
|
|
||||||
std::string evolutionLevel = segments[segment_iter].substr(index, 2);
|
|
||||||
index += 2;
|
|
||||||
|
|
||||||
std::string ornamentationIconID = segments[segment_iter].substr(index, 5);
|
|
||||||
index += 5;
|
|
||||||
|
|
||||||
std::string itemHash = segments[segment_iter].substr(index, 8);
|
|
||||||
index += 8;
|
|
||||||
|
|
||||||
std::string 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 std::array<const char*, 9>& args) const
|
|
||||||
// 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) {
|
|
||||||
std::array<const char*, 9> resolved_args = args;
|
|
||||||
ResolveArguments(id, resolved_args);
|
|
||||||
if (!resolved_args[0])
|
|
||||||
return Simple(color, id);
|
|
||||||
|
|
||||||
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 : resolved_args) {
|
|
||||||
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, const char* spell_link) const
|
|
||||||
{
|
|
||||||
auto outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(spell_link) + 1);
|
|
||||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
|
||||||
ic->messageid = ResolveID(message);
|
|
||||||
ic->spawnid = spawn_id;
|
|
||||||
fmt::format_to_n(ic->message, strlen(spell_link) + 1, "{}\0", spell_link);
|
|
||||||
outapp->priority = 5;
|
|
||||||
|
|
||||||
return outapp;
|
|
||||||
}
|
|
||||||
|
|
||||||
EQApplicationPacket* TOB::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name,
|
|
||||||
const char* spell_link) const
|
|
||||||
{
|
|
||||||
auto outapp = new EQApplicationPacket(OP_InterruptCast,
|
|
||||||
sizeof(InterruptCast_Struct) + strlen(name) + strlen(spell_link) + 2);
|
|
||||||
auto ic = reinterpret_cast<InterruptCast_Struct*>(outapp->pBuffer);
|
|
||||||
ic->messageid = ResolveID(message);
|
|
||||||
ic->spawnid = spawn_id;
|
|
||||||
fmt::format_to_n(ic->message, strlen(name) + strlen(spell_link) + 2, "{}\0{}\0", name, spell_link);
|
|
||||||
|
|
||||||
return outapp;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZoneClient::Message
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "zone/patch/components/message/rof2.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
class TOB : public RoF2
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TOB() {}
|
|
||||||
~TOB() override {}
|
|
||||||
|
|
||||||
[[nodiscard]] EQApplicationPacket* Formatted(uint32_t color, uint32_t id, const std::array<const char*, 9>& args) const override;
|
|
||||||
|
|
||||||
EQApplicationPacket* InterruptSpell(uint32_t message, uint32_t spawn_id, const char* spell_link) const override;
|
|
||||||
EQApplicationPacket* InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, const char* name, const char* spell_link) const override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
[[nodiscard]] uint32_t ResolveID(uint32_t id) const override;
|
|
||||||
void ResolveArguments(uint32_t id, std::array<const char*, 9>& args) const override;
|
|
||||||
};
|
|
||||||
} // namespace ZoneClient::Message
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/* EQEmu: EQEmulator
|
|
||||||
|
|
||||||
Copyright (C) 2001-2026 EQEmu Development Team
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "patch/components/message/sod.h"
|
|
||||||
|
|
||||||
namespace ZoneClient::Message {
|
|
||||||
class UF : public SoD
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
UF() {}
|
|
||||||
~UF() override {}
|
|
||||||
};
|
|
||||||
} // namespace Zone::Message
|
|
||||||
+6
-7
@@ -80,9 +80,9 @@
|
|||||||
#include "common/rulesys.h"
|
#include "common/rulesys.h"
|
||||||
#include "common/spdat.h"
|
#include "common/spdat.h"
|
||||||
#include "common/strings.h"
|
#include "common/strings.h"
|
||||||
#include "patch/client_version.h"
|
|
||||||
#include "zone/bot.h"
|
#include "zone/bot.h"
|
||||||
#include "zone/client.h"
|
#include "zone/client.h"
|
||||||
|
#include "zone/client_version.h"
|
||||||
#include "zone/fastmath.h"
|
#include "zone/fastmath.h"
|
||||||
#include "zone/lua_parser.h"
|
#include "zone/lua_parser.h"
|
||||||
#include "zone/mob_movement_manager.h"
|
#include "zone/mob_movement_manager.h"
|
||||||
@@ -96,7 +96,6 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include "common/links.h"
|
#include "common/links.h"
|
||||||
#include "patch/components/message/IMessage.h"
|
|
||||||
|
|
||||||
extern Zone *zone;
|
extern Zone *zone;
|
||||||
extern volatile bool is_zone_loaded;
|
extern volatile bool is_zone_loaded;
|
||||||
@@ -340,12 +339,12 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot,
|
|||||||
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id);
|
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id);
|
||||||
|
|
||||||
if (IsClient())
|
if (IsClient())
|
||||||
ZoneClient::Message::MessageString(CastToClient(), Chat::SpellFailure, fizzle_msg, spell_link);
|
Message::MessageString(CastToClient(), Chat::SpellFailure, fizzle_msg, spell_link);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Song Failure message
|
* Song Failure message
|
||||||
*/
|
*/
|
||||||
ZoneClient::Message::CloseMessageString(this, true, RuleI(Range, SpellMessages),
|
Message::CloseMessageString(this, true, RuleI(Range, SpellMessages),
|
||||||
nullptr, true, IsClient() ? FilterPCSpells : FilterNPCSpells)(
|
nullptr, true, IsClient() ? FilterPCSpells : FilterNPCSpells)(
|
||||||
Chat::SpellFailure, fizzle_msg == MISS_NOTE ? MISSED_NOTE_OTHER : SPELL_FIZZLE_OTHER, GetName(), spell_link);
|
Chat::SpellFailure, fizzle_msg == MISS_NOTE ? MISSED_NOTE_OTHER : SPELL_FIZZLE_OTHER, GetName(), spell_link);
|
||||||
|
|
||||||
@@ -1304,7 +1303,7 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid)
|
|||||||
// the interrupt message
|
// the interrupt message
|
||||||
char spell_link[Links::MAX_LINK_SIZE];
|
char spell_link[Links::MAX_LINK_SIZE];
|
||||||
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid);
|
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid);
|
||||||
ZoneClient::Message::InterruptSpell(CastToClient(), message, GetID(), spell_link);
|
Message::InterruptSpell(CastToClient(), message, GetID(), spell_link);
|
||||||
SendSpellBarEnable(spellid);
|
SendSpellBarEnable(spellid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,7 +1331,7 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid)
|
|||||||
// this is the actual message, it works the same as a formatted message
|
// this is the actual message, it works the same as a formatted message
|
||||||
char spell_link[Links::MAX_LINK_SIZE];
|
char spell_link[Links::MAX_LINK_SIZE];
|
||||||
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid);
|
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid);
|
||||||
ZoneClient::Message::InterruptSpellOther(this, message_other, GetID(), GetCleanName(), spell_link);
|
Message::InterruptSpellOther(this, message_other, GetID(), GetCleanName(), spell_link);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is like interrupt, just it doesn't spam interrupt packets to everyone
|
// this is like interrupt, just it doesn't spam interrupt packets to everyone
|
||||||
@@ -7272,7 +7271,7 @@ void Mob::DoBardCastingFromItemClick(bool is_casting_bard_song, uint32 cast_time
|
|||||||
if (cast_time != 0) {
|
if (cast_time != 0) {
|
||||||
char spell_link[Links::MAX_LINK_SIZE];
|
char spell_link[Links::MAX_LINK_SIZE];
|
||||||
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id);
|
Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id);
|
||||||
ZoneClient::Message::InterruptSpell(CastToClient(), SONG_ENDS, GetID(), spell_link);
|
Message::InterruptSpell(CastToClient(), SONG_ENDS, GetID(), spell_link);
|
||||||
|
|
||||||
ZeroCastingVars();
|
ZeroCastingVars();
|
||||||
ZeroBardPulseVars();
|
ZeroBardPulseVars();
|
||||||
|
|||||||
Reference in New Issue
Block a user