diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 5a74d520a..63a303e38 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -86,6 +86,7 @@ set(common_sources
packet_dump_file.cpp
packet_dump.cpp
packet_functions.cpp
+ patches/client_version.cpp
patches/patches.cpp
patches/rof_limits.cpp
patches/rof.cpp
@@ -655,6 +656,8 @@ set(common_headers
packet_dump_file.h
packet_dump.h
packet_functions.h
+ patches/IMessage.h
+ patches/client_version.h
patches/patches.h
patches/rof_limits.h
patches/rof_ops.h
diff --git a/common/patches/IMessage.h b/common/patches/IMessage.h
new file mode 100644
index 000000000..57ab52286
--- /dev/null
+++ b/common/patches/IMessage.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 .
+*/
+#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
+concept AllConstChar = (std::is_convertible_v && ...);
+
+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& 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
diff --git a/zone/patch/client_version.cpp b/common/patches/client_version.cpp
similarity index 72%
rename from zone/patch/client_version.cpp
rename to common/patches/client_version.cpp
index 2318dfb6e..20a506899 100644
--- a/zone/patch/client_version.cpp
+++ b/common/patches/client_version.cpp
@@ -18,19 +18,15 @@
#include "client_version.h"
-#include "zone/patch/components/message/titanium.h"
-#include "zone/patch/components/message/sof.h"
-#include "zone/patch/components/message/sod.h"
-#include "zone/patch/components/message/uf.h"
-#include "zone/patch/components/message/rof.h"
-#include "zone/patch/components/message/rof2.h"
-#include "zone/patch/components/message/tob.h"
-
-#include "client.h"
-#include "websocketpp/http/parser.hpp"
+#include "common/patches/titanium.h"
+#include "common/patches/sof.h"
+#include "common/patches/sod.h"
+#include "common/patches/uf.h"
+#include "common/patches/rof.h"
+#include "common/patches/rof2.h"
+#include "common/patches/tob.h"
using Version = EQ::versions::ClientVersion;
-using namespace ZoneClient;
struct ClientComponents
{
@@ -82,16 +78,7 @@ static const ClientComponents& GetComponents(Version version)
return patches.at(version);
}
-const std::shared_ptr& ClientPatch::GetMessageComponent(Version version)
+const std::shared_ptr& GetMessageComponent(Version version)
{
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; }
diff --git a/common/patches/client_version.h b/common/patches/client_version.h
new file mode 100644
index 000000000..5c1ede856
--- /dev/null
+++ b/common/patches/client_version.h
@@ -0,0 +1,13 @@
+//
+// Created by dannu on 4/21/2026.
+//
+
+#pragma once
+
+#include "common/emu_versions.h"
+#include
+
+namespace Message { class IMessage; }
+
+// store all static functions for the different patches here
+const std::shared_ptr& GetMessageComponent(EQ::versions::ClientVersion version);
diff --git a/common/patches/rof.h b/common/patches/rof.h
index 229d371a6..0d08c6e68 100644
--- a/common/patches/rof.h
+++ b/common/patches/rof.h
@@ -17,6 +17,7 @@
*/
#pragma once
+#include "uf.h"
#include "common/struct_strategy.h"
class EQStreamIdentifier;
@@ -48,3 +49,14 @@ namespace RoF
};
} /*RoF*/
+
+namespace Message {
+
+class RoF : public UF
+{
+public:
+ RoF() = default;
+ ~RoF() override = default;
+};
+
+} // namespace Message
diff --git a/common/patches/rof2.h b/common/patches/rof2.h
index fffc1be23..3613fa354 100644
--- a/common/patches/rof2.h
+++ b/common/patches/rof2.h
@@ -17,6 +17,7 @@
*/
#pragma once
+#include "rof.h"
#include "common/struct_strategy.h"
class EQStreamIdentifier;
@@ -48,3 +49,14 @@ namespace RoF2
};
}; /*RoF2*/
+
+namespace Message {
+
+class RoF2 : public RoF
+{
+public:
+ RoF2() = default;
+ ~RoF2() override = default;
+};
+
+} // namespace Message
diff --git a/common/patches/sod.h b/common/patches/sod.h
index 6820e6e72..203d52f19 100644
--- a/common/patches/sod.h
+++ b/common/patches/sod.h
@@ -17,6 +17,7 @@
*/
#pragma once
+#include "sof.h"
#include "common/struct_strategy.h"
class EQStreamIdentifier;
@@ -48,3 +49,14 @@ namespace SoD
};
} /*SoD*/
+
+namespace Message {
+
+class SoD : public SoF
+{
+public:
+ SoD() = default;
+ ~SoD() override = default;
+};
+
+} // namespace Message
diff --git a/common/patches/sof.h b/common/patches/sof.h
index bef47bb4d..3c0206d17 100644
--- a/common/patches/sof.h
+++ b/common/patches/sof.h
@@ -17,6 +17,7 @@
*/
#pragma once
+#include "titanium.h"
#include "common/struct_strategy.h"
class EQStreamIdentifier;
@@ -48,3 +49,14 @@ namespace SoF
};
} /*SoF*/
+
+namespace Message {
+
+class SoF : public Titanium
+{
+public:
+ SoF() = default;
+ ~SoF() override = default;
+};
+
+} // namespace Message
diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp
index 395094306..49ead949e 100644
--- a/common/patches/titanium.cpp
+++ b/common/patches/titanium.cpp
@@ -32,6 +32,7 @@
#include "common/raid.h"
#include "common/rulesys.h"
#include "common/strings.h"
+#include "zone/string_ids.h"
#include
@@ -3919,3 +3920,97 @@ namespace Titanium
return index; // as long as we guard against bad slots server side, we should be fine
}
} /*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(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& args) const
+{
+ uint32_t string_id = ResolveID(id);
+ if (string_id > 0) {
+ std::array 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(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(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& 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
diff --git a/common/patches/titanium.h b/common/patches/titanium.h
index 431e8346d..1d96475a2 100644
--- a/common/patches/titanium.h
+++ b/common/patches/titanium.h
@@ -17,6 +17,7 @@
*/
#pragma once
+#include "IMessage.h"
#include "common/struct_strategy.h"
class EQStreamIdentifier;
@@ -48,3 +49,27 @@ namespace 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& 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& args) const;
+};
+
+} // namespace Message
+
diff --git a/common/patches/tob.cpp b/common/patches/tob.cpp
index f53794951..39e5c5efc 100644
--- a/common/patches/tob.cpp
+++ b/common/patches/tob.cpp
@@ -27,6 +27,7 @@
#include "common/packet_dump.h"
#include "world/sof_char_create_data.h"
+#include "zone/string_ids.h"
namespace TOB
{
@@ -37,8 +38,8 @@ namespace TOB
void SerializeItem(SerializeBuffer &buffer, const EQ::ItemInstance* inst, int16 slot_id, uint8 depth, ItemPacketType packet_type);
// message link converters
- static inline 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 ServerToTOBConvertLinks(std::string& message_out, const std::string& message_in);
+ static void TOBToServerConvertLinks(std::string& message_out, const std::string& message_in);
// SpawnAppearance
static inline uint32 ServerToTOBSpawnAppearanceType(uint32 server_type);
@@ -4743,60 +4744,59 @@ namespace TOB
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) {
message_out = message_in;
return;
}
- auto segments = Strings::Split(message_in, '\x12');
+ std::vector 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:
- {
+ case 0: {
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;
-
- auto aug1 = segments[segment_iter].substr(index, 5);
+
+ std::string aug1 = segments[segment_iter].substr(index, 5);
index += 5;
-
- auto aug2 = segments[segment_iter].substr(index, 5);
+
+ std::string aug2 = segments[segment_iter].substr(index, 5);
index += 5;
-
- auto aug3 = segments[segment_iter].substr(index, 5);
+
+ std::string aug3 = segments[segment_iter].substr(index, 5);
index += 5;
-
- auto aug4 = segments[segment_iter].substr(index, 5);
+
+ std::string aug4 = segments[segment_iter].substr(index, 5);
index += 5;
-
- auto aug5 = segments[segment_iter].substr(index, 5);
+
+ std::string aug5 = segments[segment_iter].substr(index, 5);
index += 5;
-
- auto aug6 = segments[segment_iter].substr(index, 5);
+
+ std::string aug6 = segments[segment_iter].substr(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;
-
- auto evolutionGroup = segments[segment_iter].substr(index, 4);
+
+ std::string evolutionGroup = segments[segment_iter].substr(index, 4);
index += 4;
-
- auto evolutionLevel = segments[segment_iter].substr(index, 2);
+
+ std::string evolutionLevel = segments[segment_iter].substr(index, 2);
index += 2;
-
- auto ornamentationIconID = segments[segment_iter].substr(index, 5);
+
+ std::string ornamentationIconID = segments[segment_iter].substr(index, 5);
index += 5;
-
- auto itemHash = segments[segment_iter].substr(index, 8);
+
+ std::string itemHash = segments[segment_iter].substr(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('0'); //etag item
message_out.append(item_id);
@@ -4830,14 +4830,13 @@ namespace TOB
message_out.push_back('\x12');
break;
}
- }
- else {
+ } else {
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;
}
@@ -5561,3 +5560,132 @@ namespace 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& 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& args) const
+{
+ uint32_t string_id = ResolveID(id);
+ if (string_id > 0) {
+ std::array 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(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(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
+
diff --git a/common/patches/tob.h b/common/patches/tob.h
index 52aac01cb..c3cc9e2c9 100644
--- a/common/patches/tob.h
+++ b/common/patches/tob.h
@@ -1,6 +1,6 @@
-#ifndef COMMON_LAURION_H
-#define COMMON_LAURION_H
+#pragma once
+#include "rof2.h"
#include "../struct_strategy.h"
class EQStreamIdentifier;
@@ -34,4 +34,22 @@ namespace 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& 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& args) const override;
+};
+
+} // namespace Message
diff --git a/common/patches/uf.h b/common/patches/uf.h
index e1172951b..05c27875b 100644
--- a/common/patches/uf.h
+++ b/common/patches/uf.h
@@ -17,6 +17,7 @@
*/
#pragma once
+#include "sod.h"
#include "common/struct_strategy.h"
class EQStreamIdentifier;
@@ -48,3 +49,14 @@ namespace UF
};
}; /*UF*/
+
+namespace Message {
+
+class UF : public SoD
+{
+public:
+ UF() = default;
+ ~UF() override = default;
+};
+
+} // namespace Message
diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt
index a03be5051..0a9d97178 100644
--- a/zone/CMakeLists.txt
+++ b/zone/CMakeLists.txt
@@ -28,6 +28,7 @@ set(zone_sources
client_mods.cpp
client_packet.cpp
client_process.cpp
+ client_version.cpp
combat_record.cpp
corpse.cpp
dialogue_window.cpp
@@ -130,6 +131,7 @@ set(zone_headers
cheat_manager.h
client.h
client_packet.h
+ client_version.h
combat_record.h
command.h
common.h
@@ -675,8 +677,6 @@ add_executable(zone ${zone_sources} ${zone_headers})
target_include_directories(zone PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
-add_subdirectory(patch)
-
install(TARGETS zone RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
if(EQEMU_BUILD_PCH)
diff --git a/zone/client.cpp b/zone/client.cpp
index 66a4f7523..dd9f0e6b2 100644
--- a/zone/client.cpp
+++ b/zone/client.cpp
@@ -44,10 +44,9 @@
#include "common/spdat.h"
#include "common/strings.h"
#include "common/zone_store.h"
-#include "patch/client_version.h"
-#include "patch/components/message/IMessage.h"
#include "zone/bot_command.h"
#include "zone/cheat_manager.h"
+#include "zone/client_version.h"
#include "zone/command.h"
#include "zone/dialogue_window.h"
#include "zone/dynamic_zone.h"
@@ -3812,10 +3811,10 @@ void Client::MessageString(uint32 type, uint32 string_id, uint32 distance)
return;
if (distance > 0)
- ZoneClient::Message::CloseMessageString(this, false, static_cast(distance))(
+ Message::CloseMessageString(this, false, static_cast(distance))(
type, string_id);
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;
if (distance > 0)
- ZoneClient::Message::CloseMessageString(this, false, static_cast(distance))(type, string_id, message1,
+ Message::CloseMessageString(this, false, static_cast(distance))(type, string_id, message1,
message2, message3, message4, message5, message6, message7, message8, message9);
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);
}
diff --git a/zone/client.h b/zone/client.h
index 2b416840b..0fe3dd597 100644
--- a/zone/client.h
+++ b/zone/client.h
@@ -19,7 +19,6 @@
class Client;
class EQApplicationPacket;
-namespace ZoneClient::Message { class IMessage; }
class DynamicZone;
class DzLockout;
class ExpeditionRequest;
@@ -1553,7 +1552,6 @@ public:
inline const uint32 ClientVersionBit() const { return m_ClientVersionBit; }
void SetClientVersion(EQ::versions::ClientVersion client_version);
EQ::versions::ClientVersion GetClientVersion() const;
- const std::shared_ptr& GetMessageComponent() const { return m_messageComponent; }
/** Adventure Stuff **/
void SendAdventureError(const char *error);
@@ -2283,7 +2281,6 @@ private:
EQ::versions::ClientVersion m_ClientVersion;
uint32 m_ClientVersionBit;
- std::shared_ptr m_messageComponent;
int XPRate;
diff --git a/zone/patch/components/message/rof.h b/zone/client_version.cpp
similarity index 68%
rename from zone/patch/components/message/rof.h
rename to zone/client_version.cpp
index 487df6586..1efc15835 100644
--- a/zone/patch/components/message/rof.h
+++ b/zone/client_version.cpp
@@ -15,15 +15,15 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-#pragma once
-#include "zone/patch/components/message/uf.h"
+#include "client_version.h"
-namespace ZoneClient::Message {
-class RoF : public UF
+using Version = EQ::versions::ClientVersion;
+
+void Client::SetClientVersion(Version client_version)
{
-public:
- RoF() {}
- ~RoF() override {}
-};
-} // namespace Zone::Message
+ m_ClientVersion = client_version;
+ m_ClientVersionBit = EQ::versions::ConvertClientVersionToClientVersionBit(client_version);
+}
+
+Version Client::GetClientVersion() const { return m_ClientVersion; }
diff --git a/zone/client_version.h b/zone/client_version.h
new file mode 100644
index 000000000..595f9bad9
--- /dev/null
+++ b/zone/client_version.h
@@ -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;
+
+template
+static void QueuePacket(Client* c, Fun fun, Obj* obj, Args&&... args)
+{
+ static_assert(std::is_member_function_pointer_v);
+ EQApplicationPacket* app = std::invoke(fun, obj, std::forward(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 [=](Fun fun,
+ std::function component_getter, Args&&... args) {
+ static_assert(std::is_member_function_pointer_v && "Function is required to be a member function");
+
+ std::unordered_map build_packets;
+ std::unordered_map 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)...));
+
+ 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(zone->GetClientUpdateRange());
+
+ return [=](Fun fun,
+ std::function component_getter, Args&&... args) {
+ static_assert(std::is_member_function_pointer_v, "Function is required to be a member function");
+
+ if (sender == nullptr) {
+ QueueClients(sender, ignore_sender, is_ack_required)(fun, component_getter, std::forward(args)...);
+ } else {
+ float distance_squared = distance * distance;
+ std::unordered_map 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)...));
+
+ 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
+ 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 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 [=](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 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
diff --git a/zone/patch/CMakeLists.txt b/zone/patch/CMakeLists.txt
deleted file mode 100644
index 81abcf1eb..000000000
--- a/zone/patch/CMakeLists.txt
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/zone/patch/client_version.h b/zone/patch/client_version.h
deleted file mode 100644
index 5733b7ac7..000000000
--- a/zone/patch/client_version.h
+++ /dev/null
@@ -1,103 +0,0 @@
-//
-// Created by dannu on 4/21/2026.
-//
-
-#pragma once
-
-#include
-
-#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;
- static const std::shared_ptr& GetMessageComponent(EQ::versions::ClientVersion version);
-
- template
- static void QueuePacket(Client* c, Fun fun, Obj* obj, Args&&... args)
- {
- static_assert(std::is_member_function_pointer_v);
- EQApplicationPacket* app = std::invoke(fun, obj, std::forward(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 [=](Fun fun, std::function component_getter, Args&&... args) {
- static_assert(std::is_member_function_pointer_v && "Function is required to be a member function");
-
- std::unordered_map build_packets;
- std::unordered_map 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)...));
-
- 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(zone->GetClientUpdateRange());
-
- return [=](Fun fun, std::function component_getter, Args&&... args) {
- static_assert(std::is_member_function_pointer_v && "Function is required to be a member function");
-
- if (sender == nullptr) {
- QueueClients(sender, ignore_sender, is_ack_required)(fun, component_getter, std::forward(args)...);
- } else {
- float distance_squared = distance * distance;
- std::unordered_map 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)...));
-
- if (packet->second != nullptr)
- client->QueuePacket(packet->second, is_ack_required, Client::CLIENT_CONNECTED);
- }
- }
- }
-
- for (auto [_, packet] : build_packets)
- if (packet != nullptr)
- delete packet;;
- }
- };
- }
-};
-
-}
diff --git a/zone/patch/components/CMakeLists.txt b/zone/patch/components/CMakeLists.txt
deleted file mode 100644
index de9dd636c..000000000
--- a/zone/patch/components/CMakeLists.txt
+++ /dev/null
@@ -1 +0,0 @@
-add_subdirectory(message)
\ No newline at end of file
diff --git a/zone/patch/components/message/CMakeLists.txt b/zone/patch/components/message/CMakeLists.txt
deleted file mode 100644
index 4130fe840..000000000
--- a/zone/patch/components/message/CMakeLists.txt
+++ /dev/null
@@ -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})
\ No newline at end of file
diff --git a/zone/patch/components/message/IMessage.h b/zone/patch/components/message/IMessage.h
deleted file mode 100644
index 7b48b2604..000000000
--- a/zone/patch/components/message/IMessage.h
+++ /dev/null
@@ -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 .
-*/
-#pragma once
-
-#include "patch/client_version.h"
-
-#include
-
-// 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
-concept AllConstChar = (std::is_convertible_v && ...);
-
-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& 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
- 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 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 [=](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 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
diff --git a/zone/patch/components/message/rof2.cpp b/zone/patch/components/message/rof2.cpp
deleted file mode 100644
index 7b2120e31..000000000
--- a/zone/patch/components/message/rof2.cpp
+++ /dev/null
@@ -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 .
-*/
-#include "zone/patch/components/message/rof2.h"
-
-namespace ZoneClient::Message {
-
-} // namespace Zone::Message
diff --git a/zone/patch/components/message/rof2.h b/zone/patch/components/message/rof2.h
deleted file mode 100644
index 1318c007a..000000000
--- a/zone/patch/components/message/rof2.h
+++ /dev/null
@@ -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 .
-*/
-#pragma once
-
-#include "zone/patch/components/message/rof.h"
-
-namespace ZoneClient::Message {
-class RoF2 : public RoF
-{
-public:
- RoF2() {}
- ~RoF2() override {}
-};
-} // namespace Zone::Message
diff --git a/zone/patch/components/message/sod.h b/zone/patch/components/message/sod.h
deleted file mode 100644
index 16b14b10b..000000000
--- a/zone/patch/components/message/sod.h
+++ /dev/null
@@ -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 .
-*/
-#pragma once
-
-#include "zone/patch/components/message/sof.h"
-
-namespace ZoneClient::Message {
-class SoD : public SoF
-{
-public:
- SoD() {}
- ~SoD() override {}
-};
-} // namespace Zone::Message
diff --git a/zone/patch/components/message/sof.h b/zone/patch/components/message/sof.h
deleted file mode 100644
index 3b8c1d1ad..000000000
--- a/zone/patch/components/message/sof.h
+++ /dev/null
@@ -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 .
-*/
-#pragma once
-
-#include "zone/patch/components/message/titanium.h"
-
-namespace ZoneClient::Message {
-class SoF : public Titanium
-{
-public:
- SoF() {}
- ~SoF() override {}
-};
-} // namespace Zone::Message
diff --git a/zone/patch/components/message/titanium.cpp b/zone/patch/components/message/titanium.cpp
deleted file mode 100644
index 4efc3b2c1..000000000
--- a/zone/patch/components/message/titanium.cpp
+++ /dev/null
@@ -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 .
-*/
-#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(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& args) const
-{
- uint32_t string_id = ResolveID(id);
- if (string_id > 0) {
- std::array 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(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(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& 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
diff --git a/zone/patch/components/message/titanium.h b/zone/patch/components/message/titanium.h
deleted file mode 100644
index ebf7dfb7b..000000000
--- a/zone/patch/components/message/titanium.h
+++ /dev/null
@@ -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 .
-*/
-#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& 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& args) const;
-};
-} // namespace ZoneClient::Message
diff --git a/zone/patch/components/message/tob.cpp b/zone/patch/components/message/tob.cpp
deleted file mode 100644
index 0d8c888d5..000000000
--- a/zone/patch/components/message/tob.cpp
+++ /dev/null
@@ -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 .
-*/
-#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& 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 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& 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 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(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(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
diff --git a/zone/patch/components/message/tob.h b/zone/patch/components/message/tob.h
deleted file mode 100644
index a6084535a..000000000
--- a/zone/patch/components/message/tob.h
+++ /dev/null
@@ -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 .
-*/
-#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& 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& args) const override;
-};
-} // namespace ZoneClient::Message
diff --git a/zone/patch/components/message/uf.h b/zone/patch/components/message/uf.h
deleted file mode 100644
index f19677ae0..000000000
--- a/zone/patch/components/message/uf.h
+++ /dev/null
@@ -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 .
-*/
-#pragma once
-
-#include "patch/components/message/sod.h"
-
-namespace ZoneClient::Message {
-class UF : public SoD
-{
-public:
- UF() {}
- ~UF() override {}
-};
-} // namespace Zone::Message
diff --git a/zone/spells.cpp b/zone/spells.cpp
index 4bff2cf81..888b86406 100644
--- a/zone/spells.cpp
+++ b/zone/spells.cpp
@@ -80,9 +80,9 @@
#include "common/rulesys.h"
#include "common/spdat.h"
#include "common/strings.h"
-#include "patch/client_version.h"
#include "zone/bot.h"
#include "zone/client.h"
+#include "zone/client_version.h"
#include "zone/fastmath.h"
#include "zone/lua_parser.h"
#include "zone/mob_movement_manager.h"
@@ -96,7 +96,6 @@
#include
#include "common/links.h"
-#include "patch/components/message/IMessage.h"
extern Zone *zone;
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);
if (IsClient())
- ZoneClient::Message::MessageString(CastToClient(), Chat::SpellFailure, fizzle_msg, spell_link);
+ Message::MessageString(CastToClient(), Chat::SpellFailure, fizzle_msg, spell_link);
/**
* Song Failure message
*/
- ZoneClient::Message::CloseMessageString(this, true, RuleI(Range, SpellMessages),
+ Message::CloseMessageString(this, true, RuleI(Range, SpellMessages),
nullptr, true, IsClient() ? FilterPCSpells : FilterNPCSpells)(
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
char spell_link[Links::MAX_LINK_SIZE];
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);
}
@@ -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
char spell_link[Links::MAX_LINK_SIZE];
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
@@ -7272,7 +7271,7 @@ void Mob::DoBardCastingFromItemClick(bool is_casting_bard_song, uint32 cast_time
if (cast_time != 0) {
char spell_link[Links::MAX_LINK_SIZE];
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();
ZeroBardPulseVars();