From 743fd45b175981c819d8dcee857554ad23e27f53 Mon Sep 17 00:00:00 2001 From: dannuic Date: Sun, 26 Apr 2026 00:29:12 -0600 Subject: [PATCH] Added component-based patch system (#5070) --- CMakeLists.txt | 2 +- common/CMakeLists.txt | 9 +- common/emu_versions.h | 4 +- common/links.cpp | 25 ++- common/links.h | 54 ++++++- common/patches/IMessage.h | 51 ++++++ common/patches/client_version.cpp | 86 ++++++++++ common/patches/client_version.h | 13 ++ common/patches/rof.h | 12 ++ common/patches/rof2.h | 12 ++ common/patches/sod.h | 12 ++ common/patches/sof.h | 12 ++ common/patches/titanium.cpp | 96 +++++++++++ common/patches/titanium.h | 28 ++++ common/patches/tob.cpp | 256 +++++++++++++++++++++--------- common/patches/tob.h | 27 +++- common/patches/tob_ops.h | 1 - common/patches/tob_structs.h | 6 +- common/patches/uf.h | 12 ++ tob/opcodes.md | 2 +- zone/CMakeLists.txt | 4 + zone/bot.cpp | 4 +- zone/client.cpp | 159 ++++++------------- zone/client.h | 9 +- zone/client_packet.cpp | 37 ++--- zone/client_process.cpp | 4 +- zone/client_version.cpp | 29 ++++ zone/client_version.h | 150 +++++++++++++++++ zone/spells.cpp | 75 +++------ zone/string_ids.h | 116 +++++++------- 30 files changed, 955 insertions(+), 352 deletions(-) create mode 100644 common/patches/IMessage.h create mode 100644 common/patches/client_version.cpp create mode 100644 common/patches/client_version.h create mode 100644 zone/client_version.cpp create mode 100644 zone/client_version.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bbbeda68..b5f332e98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,7 +71,7 @@ if(UNIX) endif() endif() -find_package(Boost REQUIRED COMPONENTS dynamic_bitset foreach tuple) +find_package(Boost REQUIRED COMPONENTS dynamic_bitset foreach tuple CONFIG REQUIRED) find_package(cereal CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED) find_package(glm CONFIG REQUIRED) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 327aa2ce5..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 @@ -135,6 +136,7 @@ set(common_sources util/directory.cpp util/uuid.cpp zone_store.cpp + links.cpp ) set(repositories @@ -654,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 @@ -733,9 +737,8 @@ set(common_headers util/memory_stream.h util/uuid.h version.h - zone_store.h - links.h - links.cpp + zone_store.h + links.h ) source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${common_sources}) diff --git a/common/emu_versions.h b/common/emu_versions.h index c2605d877..c732e994b 100644 --- a/common/emu_versions.h +++ b/common/emu_versions.h @@ -61,8 +61,8 @@ namespace EQ maskAllClients = 0xFFFFFFFF }; - const ClientVersion LastClientVersion = ClientVersion::TOB; - const size_t ClientVersionCount = (static_cast(LastClientVersion) + 1); + inline constexpr ClientVersion LastClientVersion = ClientVersion::TOB; + inline constexpr size_t ClientVersionCount = (static_cast(LastClientVersion) + 1); bool IsValidClientVersion(ClientVersion client_version); ClientVersion ValidateClientVersion(ClientVersion client_version); diff --git a/common/links.cpp b/common/links.cpp index b8fa758af..6508fa44a 100644 --- a/common/links.cpp +++ b/common/links.cpp @@ -4,7 +4,28 @@ #include "links.h" -std::string Links::FormatSpellLink(uint32_t SpellID, const std::string& SpellName) +#include "spdat.h" + +void Links::FormatItemLink(char* Buffer, size_t BufferSize, const EQ::ItemInstance* item) { - return fmt::format("{}63^{}^0^'{}{}", ITEM_TAG_CHAR, SpellID, SpellName.c_str(), ITEM_TAG_CHAR); + // TODO: Reverse 0x14064B220 to get definition of this function +} + +void Links::FormatSpellLink(char* Buffer, size_t BufferSize, uint32_t SpellID, + const char* spellNameOverride) +{ + snprintf(Buffer, BufferSize, "%c%d3^%d^0^'%s%c", ITEM_TAG_CHAR, ETAG_SPELL, SpellID, + spellNameOverride && spellNameOverride[0] ? spellNameOverride : GetSpellName(SpellID), ITEM_TAG_CHAR); +} + +void Links::FormatDialogLink(char* Buffer, size_t BufferSize, std::string_view keyword, std::string_view text) +{ + if (text.empty()) { + snprintf(Buffer, BufferSize, "%c%d%.*s%c", ITEM_TAG_CHAR, ETAG_DIALOG_RESPONSE, + static_cast(keyword.length()), keyword.data(), ITEM_TAG_CHAR); + } else { + snprintf(Buffer, BufferSize, "%c%d%.*s:%.*s%c", ITEM_TAG_CHAR, ETAG_DIALOG_RESPONSE, + static_cast(keyword.length()), keyword.data(), + static_cast(text.length()), text.data(), ITEM_TAG_CHAR); + } } diff --git a/common/links.h b/common/links.h index 49d2e8fab..d01940cba 100644 --- a/common/links.h +++ b/common/links.h @@ -3,9 +3,59 @@ // #pragma once +#include "item_instance.h" + +namespace EQ { class ItemInstance; } namespace Links { - constexpr char ITEM_TAG_CHAR = '\x12'; - std::string FormatSpellLink(uint32_t SpellID, const std::string& SpellName); +// Max Link Size in bytes +constexpr size_t MAX_LINK_SIZE = 512; + +// Universal link tag character +constexpr char ITEM_TAG_CHAR = '\x12'; + +// Enumeration of different types of item tags +enum ETagCodes +{ + ETAG_ITEM = 0, + ETAG_PLAYER, + ETAG_SPAM, + ETAG_ACHIEVEMENT, + ETAG_DIALOG_RESPONSE, + ETAG_COMMAND, + ETAG_SPELL, + ETAG_FACTION, + ETAG_COMMAND2, + ETAG_UNKNOWN9, + + ETAG_COUNT, + ETAG_FIRST = ETAG_ITEM, + ETAG_LAST = ETAG_UNKNOWN9, + + ETAG_INVALID = -1, +}; + +//---------------------------------------------------------------------------- +// Link Formatting -- Pulled from MQ code + +// Create an achievement link for the given achievement. +// TODO: implement this when achievements are added, leave the signature here for reference. Code in eqlib's ItemLinks.cpp +// void FormatAchievementLink(char* Buffer, size_t BufferSize, const Achievement* achievement, +// std::string_view playerName); + +// Create an item link from the given item. +void FormatItemLink(char* Buffer, size_t BufferSize, const EQ::ItemInstance* item); + +// Create a spell link for the given spell, with optional spell name override. Spells on items often have +// spell name overrides that changes the display name of the spell. +void FormatSpellLink(char* Buffer, size_t BufferSize, uint32_t SpellID, + const char* spellNameOverride = nullptr); + +// Format text into a clickable dialog link. The keyword is the text that will be displayed in the chat window, +// and the text is the text that will be sent to the server when the link is clicked. If no text is provided, +// then the keyword will be used as the text. +void FormatDialogLink(char* Buffer, size_t BufferSize, std::string_view keyword, + std::string_view text = {}); + } diff --git a/common/patches/IMessage.h b/common/patches/IMessage.h new file mode 100644 index 000000000..80bd15501 --- /dev/null +++ b/common/patches/IMessage.h @@ -0,0 +1,51 @@ +/* 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 + virtual std::unique_ptr Simple(uint32_t color, uint32_t id) const = 0; + virtual std::unique_ptr 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 std::unique_ptr InterruptSpell(uint32_t message, uint32_t spawn_id, + const char* spell_link) const = 0; + virtual std::unique_ptr InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, + const char* name, const char* spell_link) const = 0; +}; + +} // namespace Message diff --git a/common/patches/client_version.cpp b/common/patches/client_version.cpp new file mode 100644 index 000000000..0c00906f5 --- /dev/null +++ b/common/patches/client_version.cpp @@ -0,0 +1,86 @@ +/* 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 "client_version.h" + +#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" + +#include + +using Version = EQ::versions::ClientVersion; + +struct ClientComponents +{ + explicit ClientComponents(Version version) : version(version) + { + switch (version) { + case Version::TOB: + messageComponent = std::make_unique(); + break; + case Version::RoF2: + messageComponent = std::make_unique(); + break; + case Version::RoF: + messageComponent = std::make_unique(); + break; + case Version::UF: + messageComponent = std::make_unique(); + break; + case Version::SoD: + messageComponent = std::make_unique(); + break; + case Version::SoF: + messageComponent = std::make_unique(); + break; + case Version::Titanium: + messageComponent = std::make_unique(); + break; + default: + break; + } + } + + const Version version; + std::unique_ptr messageComponent; +}; + +// this array must be in the same order as the Version enum because it converts Version to index directly +static const std::array s_patches = { + { + ClientComponents(Version::Unknown), // empty + ClientComponents(Version::Client62), // empty + ClientComponents(Version::Titanium), + ClientComponents(Version::SoF), + ClientComponents(Version::SoD), + ClientComponents(Version::UF), + ClientComponents(Version::RoF), + ClientComponents(Version::RoF2), + ClientComponents(Version::TOB), + } +}; + +const std::unique_ptr& GetMessageComponent(Version version) +{ + return s_patches.at(static_cast(version)).messageComponent; +} diff --git a/common/patches/client_version.h b/common/patches/client_version.h new file mode 100644 index 000000000..a0ebf9c3a --- /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, this can return nullptr for unsupported patches +const std::unique_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..731cc1488 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,98 @@ namespace Titanium return index; // as long as we guard against bad slots server side, we should be fine } } /*Titanium*/ + +namespace Message { +std::unique_ptr Titanium::Simple(uint32_t color, uint32_t id) const +{ + uint32_t string_id = ResolveID(id); + if (string_id > 0) { + auto outapp = std::make_unique(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; +} + +std::unique_ptr 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 std::make_unique(OP_FormattedMessage, std::move(buf)); + } + + return nullptr; +} + +std::unique_ptr Titanium::InterruptSpell(uint32_t message, uint32_t spawn_id, + const char* spell_link) const +{ + auto outapp = std::make_unique(OP_InterruptCast, sizeof(InterruptCast_Struct)); + auto ic = reinterpret_cast(outapp->pBuffer); + ic->messageid = ResolveID(message); + ic->spawnid = spawn_id; + outapp->priority = 5; + + return outapp; +} + +std::unique_ptr Titanium::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, + const char* name, + const char* spell_link) const +{ + auto outapp = std::make_unique(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..e31c4ef63 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,30 @@ namespace Titanium }; } /*Titanium*/ + +// out-going message packets +namespace Message { + +class Titanium : public IMessage +{ +public: + Titanium() = default; + ~Titanium() override = default; + + std::unique_ptr Simple(uint32_t color, uint32_t id) const override; + std::unique_ptr Formatted(uint32_t color, uint32_t id, + const std::array& args) const override; + + std::unique_ptr InterruptSpell(uint32_t message, uint32_t spawn_id, + const char* spell_link) const override; + std::unique_ptr 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 691ae49e9..41b014448 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); @@ -388,8 +389,7 @@ namespace TOB VARSTRUCT_ENCODE_STRING(OutBuffer, emu->sender); VARSTRUCT_ENCODE_STRING(OutBuffer, emu->targetname); - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // Unknown - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // Unknown + VARSTRUCT_ENCODE_TYPE(uint64, OutBuffer, 0); // Unknown VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, emu->language); VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, emu->chan_num); VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // Unknown @@ -397,11 +397,13 @@ namespace TOB VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, emu->skill_in_language); VARSTRUCT_ENCODE_STRING(OutBuffer, new_message.c_str()); - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // Unknown - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // Unknown - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // Unknown - VARSTRUCT_ENCODE_TYPE(uint16, OutBuffer, 0); // Unknown - VARSTRUCT_ENCODE_TYPE(uint8, OutBuffer, 0); // Unknown + VARSTRUCT_ENCODE_TYPE(uint8, OutBuffer, 0); // Unknown + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0);// Unknown + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0);// Unknown + + VARSTRUCT_ENCODE_STRING(OutBuffer, ""); + VARSTRUCT_ENCODE_TYPE(uint8, OutBuffer, 0); // Unknown + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0);// Unknown delete[] __emu_buffer; dest->FastQueuePacket(&in, ack_req); @@ -651,43 +653,6 @@ namespace TOB FINISH_ENCODE(); } - ENCODE(OP_FormattedMessage) - { - EQApplicationPacket* in = *p; - *p = nullptr; - - FormattedMessage_Struct* emu = (FormattedMessage_Struct*)in->pBuffer; - - char* old_message_ptr = (char*)in->pBuffer; - old_message_ptr += sizeof(FormattedMessage_Struct); - - std::string old_message_array[9]; - - for (int i = 0; i < 9; ++i) { - if (*old_message_ptr == 0) { break; } - old_message_array[i] = old_message_ptr; - old_message_ptr += old_message_array[i].length() + 1; - } - - SerializeBuffer buffer; - buffer.WriteUInt32(0); // This is a string written like the message arrays - buffer.WriteUInt8(emu->unknown0); - buffer.WriteUInt32(emu->string_id); - buffer.WriteUInt32(emu->type); - - for (int i = 0; i < 9; ++i) { - std::string new_message; - ServerToTOBConvertLinks(new_message, old_message_array[i]); - buffer.WriteLengthString(new_message); - } - - auto outapp = new EQApplicationPacket(OP_FormattedMessage, buffer.size()); - outapp->WriteData(buffer.buffer(), buffer.size()); - dest->FastQueuePacket(&outapp, ack_req); - - delete in; - } - ENCODE(OP_GMTraining) { ENCODE_LENGTH_EXACT(GMTrainee_Struct); SETUP_DIRECT_ENCODE(GMTrainee_Struct, structs::GMTrainee_Struct); @@ -3676,10 +3641,13 @@ namespace TOB uint32 Skill = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); + // this has a size limit of 11k in the client std::string old_message = InBuffer; std::string new_message; TOBToServerConvertLinks(new_message, old_message); + // there are 15 bytes after this, part of which is an unk string, check the ENCODE for the layout + __packet->size = sizeof(ChannelMessage_Struct) + new_message.length() + 1; __packet->pBuffer = new unsigned char[__packet->size]; ChannelMessage_Struct* emu = (ChannelMessage_Struct*)__packet->pBuffer; @@ -4780,60 +4748,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); @@ -4867,14 +4834,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; } @@ -5598,3 +5564,135 @@ 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; + } +} + +std::unique_ptr 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 std::make_unique(OP_FormattedMessage, std::move(buffer)); + } + + return nullptr; +} + +std::unique_ptr TOB::InterruptSpell(uint32_t message, uint32_t spawn_id, + const char* spell_link) const +{ + auto outapp = std::make_unique(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; +} + +std::unique_ptr TOB::InterruptSpellOther(Mob* sender, uint32_t message, uint32_t spawn_id, + const char* name, + const char* spell_link) const +{ + auto outapp = std::make_unique(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..39eb708cf 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,25 @@ namespace TOB }; /*TOB*/ -#endif /*COMMON_LAURION_H*/ +namespace Message { + +class TOB : public RoF2 +{ +public: + TOB() {} + ~TOB() override {} + + std::unique_ptr Formatted(uint32_t color, uint32_t id, + const std::array& args) const override; + + std::unique_ptr InterruptSpell(uint32_t message, uint32_t spawn_id, + const char* spell_link) const override; + std::unique_ptr 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/tob_ops.h b/common/patches/tob_ops.h index 40a7c9adf..0136ef5ba 100644 --- a/common/patches/tob_ops.h +++ b/common/patches/tob_ops.h @@ -24,7 +24,6 @@ E(OP_DeleteSpawn) E(OP_DisciplineUpdate) E(OP_ExpansionInfo) E(OP_ExpUpdate) -E(OP_FormattedMessage) E(OP_GMTraining) E(OP_GMTrainSkillConfirm) E(OP_GroundSpawn) diff --git a/common/patches/tob_structs.h b/common/patches/tob_structs.h index db3cd4ad7..e60cad405 100644 --- a/common/patches/tob_structs.h +++ b/common/patches/tob_structs.h @@ -741,8 +741,8 @@ namespace TOB { /*132*/ float y; /*136*/ float x; /*140*/ float z; - /*144*/ uint8 level; - /*145*/ uint8 type; + /*144*/ uint8 type; + /*145*/ uint8 level; /*146*/ uint8 charges; //no idea if these are right; eqlib doesn't seem to know either /*147*/ uint8 activatable; /*148*/ uint32 unknown1; //might be some timer, not sure though @@ -754,7 +754,7 @@ namespace TOB { /*004*/ int32 unknown004; /*008*/ EQAffect_Struct affect; /*160*/ uint32 slot_id; - /*164*/ uint32 buff_fade; + /*164*/ uint32 buff_fade; // 1: remove, 2: modify, 3: add new /*168*/ }; 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/tob/opcodes.md b/tob/opcodes.md index 62db50a03..9c60dc50d 100644 --- a/tob/opcodes.md +++ b/tob/opcodes.md @@ -81,7 +81,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th | `OP_CashReward` | 🟡 Unverified | | | | `OP_CastSpell` | 🟢 Verified | | | | `OP_ChangeSize` | 🟢 Verified | | | -| `OP_ChannelMessage` | 🟡 Unverified | | | +| `OP_ChannelMessage` | 🟢 Verified | | | | `OP_ChangePetName` | 🔴 Not-Set | | | | `OP_CharacterCreate` | 🟢 Verified | Sends heroic type, can be used for something? | | | `OP_CharacterCreateRequest` | 🟢 Verified | | | diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index afb2ca531..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 @@ -673,6 +675,8 @@ set_property(TARGET gm_commands_zone PROPERTY FOLDER libraries) add_executable(zone ${zone_sources} ${zone_headers}) +target_include_directories(zone PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + install(TARGETS zone RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) if(EQEMU_BUILD_PCH) diff --git a/zone/bot.cpp b/zone/bot.cpp index 00662389c..f48f4a175 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -5922,13 +5922,13 @@ bool Bot::CastSpell( if (DivineAura()) { LogSpellsDetail("Spell casting canceled: cannot cast while Divine Aura is in effect"); - InterruptSpell(SPELL_FIZZLE, 0x121, false); + InterruptSpell(SPELL_FIZZLE, Chat::SpellFailure, false); return false; } if (slot < EQ::spells::CastingSlot::MaxGems && !CheckFizzle(spell_id)) { int fizzle_msg = IsBardSong(spell_id) ? MISS_NOTE : SPELL_FIZZLE; - InterruptSpell(fizzle_msg, 0x121, spell_id); + InterruptSpell(fizzle_msg, Chat::SpellFailure, spell_id); uint32 use_mana = ((spells[spell_id].mana) / 4); LogSpellsDetail("Spell casting canceled: fizzled. [{}] mana has been consumed", use_mana); diff --git a/zone/client.cpp b/zone/client.cpp index d50cb927e..dd9f0e6b2 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -46,6 +46,7 @@ #include "common/zone_store.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" @@ -1792,7 +1793,7 @@ void Client::Message(uint32 type, const char* message, ...) { } void Client::FilteredMessage(Mob *sender, uint32 type, eqFilterType filter, const char* message, ...) { - if (!FilteredMessageCheck(sender, filter)) + if (!ShouldGetPacket(sender, filter)) return; va_list argptr; @@ -3808,18 +3809,12 @@ void Client::MessageString(uint32 type, uint32 string_id, uint32 distance) return; if (GetFilter(FilterSpellCrits) == FilterHide && type == Chat::SpellCrit) return; - auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); - SimpleMessage_Struct* sms = (SimpleMessage_Struct*)outapp->pBuffer; - sms->color=type; - sms->string_id=string_id; - sms->unknown8=0; - - if(distance>0) - entity_list.QueueCloseClients(this,outapp,false,distance); + if (distance > 0) + Message::CloseMessageString(this, false, static_cast(distance))( + type, string_id); else - QueuePacket(outapp); - safe_delete(outapp); + Message::MessageString(this, type, string_id); } // @@ -3829,9 +3824,9 @@ void Client::MessageString(uint32 type, uint32 string_id, uint32 distance) // This hack sucks but it's gonna work for now. // void Client::MessageString(uint32 type, uint32 string_id, const char* message1, - const char* message2,const char* message3,const char* message4, - const char* message5,const char* message6,const char* message7, - const char* message8,const char* message9, uint32 distance) + const char* message2, const char* message3, const char* message4, + const char* message5, const char* message6, const char* message7, + const char* message8, const char* message9, uint32 distance) { if (GetFilter(FilterSpellDamage) == FilterHide && type == Chat::NonMelee) return; @@ -3847,34 +3842,12 @@ void Client::MessageString(uint32 type, uint32 string_id, const char* message1, if (type == Chat::Emote) type = 4; - if (!message1) { - MessageString(type, string_id); // use the simple message instead - return; - } - - const char *message_arg[] = { - message1, message2, message3, message4, message5, - message6, message7, message8, message9 - }; - - SerializeBuffer buf(20); - buf.WriteInt32(0); // unknown - buf.WriteInt32(string_id); - buf.WriteInt32(type); - for (auto &m : message_arg) { - if (m == nullptr) - break; - buf.WriteString(m); - } - - buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime - - auto outapp = std::make_unique(OP_FormattedMessage, std::move(buf)); - if (distance > 0) - entity_list.QueueCloseClients(this, outapp.get(), false, distance); + Message::CloseMessageString(this, false, static_cast(distance))(type, string_id, message1, + message2, message3, message4, message5, message6, message7, message8, message9); else - QueuePacket(outapp.get()); + Message::MessageString(this, type, string_id, message1, message2, message3, message4, message5, + message6, message7, message8, message9); } void Client::MessageString(const CZClientMessageString_Struct* msg) @@ -3898,60 +3871,49 @@ void Client::MessageString(const CZClientMessageString_Struct* msg) } } -// helper function, returns true if we should see the message -bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) +// helper function, returns true if the client should get the packet based on the filter and sender +bool Client::ShouldGetPacket(Mob *sender, eqFilterType filter) { eqFilterMode mode = GetFilter(filter); - // easy ones first - if (mode == FilterShow) { - return true; - } else if (mode == FilterHide) { - return false; - } - if (sender != this && mode == FilterShowSelfOnly) { + // easy ones first + if (mode == FilterShow) + return true; + + if (mode == FilterHide) return false; - } else if (sender) { - if (mode == FilterShowGroupOnly) { - auto g = GetGroup(); - auto r = GetRaid(); - if (g) { - if (g->IsGroupMember(sender)) { - return true; - } - } else if (r && sender->IsClient()) { - auto rgid1 = r->GetGroup(this); - auto rgid2 = r->GetGroup(sender->CastToClient()); - if (rgid1 != RAID_GROUPLESS && rgid1 == rgid2) { - return true; - } - } else { - return false; - } + + if (sender != this && mode == FilterShowSelfOnly) + return false; + + if (sender != nullptr && mode == FilterShowGroupOnly) { + if (sender == this) + return true; + + Group* g = GetGroup(); + if (g && g->IsGroupMember(sender)) + return true; + + Raid* r = GetRaid(); + if (r && sender->IsClient()) { + uint32 rgid1 = r->GetGroup(this); + uint32 rgid2 = r->GetGroup(sender->CastToClient()); + if (rgid1 != RAID_GROUPLESS && rgid1 == rgid2) + return true; + } else { + return false; } } - // we passed our checks + // fallback case (send by default) return true; } void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id) { - if (!FilteredMessageCheck(sender, filter)) - return; - - auto outapp = new EQApplicationPacket(OP_SimpleMessage, 12); - SimpleMessage_Struct *sms = (SimpleMessage_Struct *)outapp->pBuffer; - sms->color = type; - sms->string_id = string_id; - - sms->unknown8 = 0; - - QueuePacket(outapp); - safe_delete(outapp); - - return; + if (ShouldGetPacket(sender, filter)) + MessageString(type, string_id); } void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id, @@ -3959,37 +3921,16 @@ void Client::FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter const char *message4, const char *message5, const char *message6, const char *message7, const char *message8, const char *message9) { - if (!FilteredMessageCheck(sender, filter)) - return; - - if (type == Chat::Emote) - type = 4; - if (!message1) { FilteredMessageString(sender, type, filter, string_id); // use the simple message instead - return; + } else if (ShouldGetPacket(sender, filter)) { + if (type == Chat::Emote) + type = 4; + + MessageString( + type, string_id, message1, message2, message3, message4, + message5, message6, message7, message8, message9); } - - const char *message_arg[] = { - message1, message2, message3, message4, message5, - message6, message7, message8, message9 - }; - - SerializeBuffer buf(20); - buf.WriteInt32(0); // unknown - buf.WriteInt32(string_id); - buf.WriteInt32(type); - for (auto &m : message_arg) { - if (m == nullptr) - break; - buf.WriteString(m); - } - - buf.WriteInt8(0); // prevent oob in packet translation, maybe clean that up sometime - - auto outapp = std::make_unique(OP_FormattedMessage, std::move(buf)); - - QueuePacket(outapp.get()); } void Client::Tell_StringID(uint32 string_id, const char *who, const char *message) diff --git a/zone/client.h b/zone/client.h index 915a6a383..0fe3dd597 100644 --- a/zone/client.h +++ b/zone/client.h @@ -343,10 +343,10 @@ public: void DyeArmor(EQ::TintProfile* dye); void DyeArmorBySlot(uint8 slot, uint8 red, uint8 green, uint8 blue, uint8 use_tint = 0x00); uint8 SlotConvert(uint8 slot,bool bracer=false); - void MessageString(uint32 type, uint32 string_id, uint32 distance = 0); - void MessageString(uint32 type, uint32 string_id, const char* message,const char* message2=0,const char* message3=0,const char* message4=0,const char* message5=0,const char* message6=0,const char* message7=0,const char* message8=0,const char* message9=0, uint32 distance = 0); + void MessageString(uint32 type, uint32 string_id, uint32 distance = 0) override; + void MessageString(uint32 type, uint32 string_id, const char* message,const char* message2=0,const char* message3=0,const char* message4=0,const char* message5=0,const char* message6=0,const char* message7=0,const char* message8=0,const char* message9=0, uint32 distance = 0) override; void MessageString(const CZClientMessageString_Struct* msg); - bool FilteredMessageCheck(Mob *sender, eqFilterType filter); + bool ShouldGetPacket(Mob *sender, eqFilterType filter); void FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id); void FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id, const char *message1, const char *message2 = nullptr, @@ -1550,7 +1550,8 @@ public: inline const EQ::versions::ClientVersion ClientVersion() const { return m_ClientVersion; } inline const uint32 ClientVersionBit() const { return m_ClientVersionBit; } - inline void SetClientVersion(EQ::versions::ClientVersion client_version) { m_ClientVersion = client_version; } + void SetClientVersion(EQ::versions::ClientVersion client_version); + EQ::versions::ClientVersion GetClientVersion() const; /** Adventure Stuff **/ void SendAdventureError(const char *error); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 6568d6357..7dc0dab17 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -8912,31 +8912,20 @@ void Client::Handle_OP_Hide(const EQApplicationPacket *app) tmHidden = Timer::GetCurrentTime(); } if (GetClass() == Class::Rogue) { - auto outapp = new EQApplicationPacket(OP_SimpleMessage, sizeof(SimpleMessage_Struct)); - SimpleMessage_Struct *msg = (SimpleMessage_Struct *)outapp->pBuffer; - msg->color = 0x010E; - Mob *evadetar = GetTarget(); - if (!auto_attack && (evadetar && evadetar->CheckAggro(this) - && evadetar->IsNPC())) { + uint32 string_id = HIDE_FAIL; + Mob* evadetar = GetTarget(); + + if (!auto_attack && evadetar && evadetar->CheckAggro(this) && evadetar->IsNPC()) { if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide)) { - msg->string_id = EVADE_SUCCESS; + string_id = EVADE_SUCCESS; RogueEvade(evadetar); - } - else { - msg->string_id = EVADE_FAIL; - } - } - else { - if (hidden) { - msg->string_id = HIDE_SUCCESS; - } - else { - msg->string_id = HIDE_FAIL; - } - } - FastQueuePacket(&outapp); + } else + string_id = EVADE_FAIL; + } else if (hidden) + string_id = HIDE_SUCCESS; + + MessageString(Chat::Skills, string_id); } - return; } void Client::Handle_OP_HideCorpse(const EQApplicationPacket *app) @@ -10325,10 +10314,10 @@ void Client::Handle_OP_ManaChange(const EQApplicationPacket *app) if (app->size == 0) { // i think thats the sign to stop the songs if (IsBardSong(casting_spell_id) || HasActiveSong()) { - InterruptSpell(SONG_ENDS, 0x121); //Live doesn't send song end message anymore (~Kayen 1/26/22) + InterruptSpell(SONG_ENDS, Chat::SpellFailure); //Live doesn't send song end message anymore (~Kayen 1/26/22) } else { - InterruptSpell(INTERRUPT_SPELL, 0x121); + InterruptSpell(INTERRUPT_SPELL, Chat::SpellFailure); } return; } diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 2fc551830..6953a6728 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -229,11 +229,11 @@ bool Client::Process() { } if (song_target == nullptr) { - InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); + InterruptSpell(SONG_ENDS_ABRUPTLY, Chat::SpellFailure, bardsong); } else { if (!ApplyBardPulse(bardsong, song_target, bardsong_slot)) { - InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); + InterruptSpell(SONG_ENDS_ABRUPTLY, Chat::SpellFailure, bardsong); } } } diff --git a/zone/client_version.cpp b/zone/client_version.cpp new file mode 100644 index 000000000..1efc15835 --- /dev/null +++ b/zone/client_version.cpp @@ -0,0 +1,29 @@ +/* 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 "client_version.h" + +using Version = EQ::versions::ClientVersion; + +void Client::SetClientVersion(Version client_version) +{ + 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..80032c452 --- /dev/null +++ b/zone/client_version.h @@ -0,0 +1,150 @@ +// +// 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 using ComponentGetter = std::function; + +template + requires std::is_member_function_pointer_v +static void QueuePacket(Client* c, Fun fun, Obj* obj, Args&&... args) +{ + if (obj != nullptr) { + std::unique_ptr app = std::invoke(fun, obj, std::forward(args)...); + if (app) + c->QueuePacket(app.get()); + } +} + +// packet generator queue functions +static auto QueueClients(Mob* sender, bool ignore_sender = false, bool ackreq = true) +{ + return [=](Fun fun, const ComponentGetter& component, Args&&... args) + requires std::is_member_function_pointer_v + { + std::array, EQ::versions::ClientVersionCount> build_packets; + std::unordered_map client_list = entity_list.GetClientList(); + + for (auto [_, ent] : client_list) { + if (!ignore_sender || ent != sender) { + auto& packet = build_packets.at(static_cast(ent->ClientVersion())); + if (!packet) + if (auto comp = component(ent); comp != nullptr) + packet = std::invoke(fun, comp, std::forward(args)...); + + if (packet) + ent->QueuePacket(packet.get(), ackreq, Client::CLIENT_CONNECTED); + } + } + }; +} + +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, const ComponentGetter& component, Args&&... args) + requires std::is_member_function_pointer_v + { + if (sender == nullptr) { + QueueClients(sender, ignore_sender, is_ack_required)(fun, component, std::forward(args)...); + } else { + float distance_squared = distance * distance; + std::array, EQ::versions::ClientVersionCount> 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.at(static_cast(client->ClientVersion())); + if (!packet) + if (auto comp = component(client); comp != nullptr) + packet = std::invoke(fun, comp, std::forward(args)...); + + if (packet) + client->QueuePacket(packet.get(), is_ack_required, Client::CLIENT_CONNECTED); + } + } + } + } + }; +} + +} // namespace ClientPatch + +// Helpers for the Message interface to send message packets +namespace Message { + +// this can return nullptr when the component doesn't exist for the version +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) + requires (sizeof...(Args) <= 9) + { + 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/spells.cpp b/zone/spells.cpp index 409cbadf3..888b86406 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -82,6 +82,7 @@ #include "common/strings.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" @@ -95,7 +96,6 @@ #include #include "common/links.h" -#include "common/packet_dump.h" extern Zone *zone; extern volatile bool is_zone_loaded; @@ -335,35 +335,21 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, Mob::SetMana(GetMana() - use_mana); // We send StopCasting which will update mana StopCasting(); - // TODO: can handle spell name overrides here - std::string spell_name(GetSpellName(spell_id)); - std::string spell_link = Links::FormatSpellLink(spell_id, spell_name); + char spell_link[Links::MAX_LINK_SIZE]; + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id); - // pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches - MessageString(Chat::SpellFailure, fizzle_msg, spell_link.c_str()); + if (IsClient()) + Message::MessageString(CastToClient(), Chat::SpellFailure, fizzle_msg, spell_link); /** * Song Failure message - * pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches */ - entity_list.FilteredMessageCloseString( - this, - true, - RuleI(Range, SpellMessages), - Chat::SpellFailure, - (IsClient() ? FilterPCSpells : FilterNPCSpells), - (fizzle_msg == MISS_NOTE ? MISSED_NOTE_OTHER : SPELL_FIZZLE_OTHER), - 0, - /* - MessageFormat: A missed note brings %1's song to a close! (TOB: A missed note brings %1's %2 to a close!) - MessageFormat: %1's spell fizzles! (TOB: %1's %2 spell fizzles!) - */ - GetName(), - spell_link.c_str() - ); + 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); TryTriggerOnCastRequirement(); - return(false); + return false; } SaveSpellLoc(); @@ -1254,7 +1240,6 @@ void Mob::InterruptSpell(uint16 spellid) // color not used right now void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) { - EQApplicationPacket *outapp = nullptr; uint16 message_other; bool bard_song_mode = false; //has the bard song gone to auto repeat mode @@ -1312,24 +1297,13 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) if(!message) message = IsBardSong(spellid) ? SONG_ENDS_ABRUPTLY : INTERRUPT_SPELL; - // TODO: can handle spell name overrides here - std::string spellname(GetSpellName(spellid)); - std::string spelllink = Links::FormatSpellLink(spellid, spellname); - // clients need some packets if (IsClient() && message != SONG_ENDS) { // the interrupt message - outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + spelllink.size() + 1); - InterruptCast_Struct* ic = (InterruptCast_Struct*) outapp->pBuffer; - ic->messageid = message; - ic->spawnid = GetID(); - // pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches - fmt::format_to_n(ic->message, spelllink.size(), "{}", spelllink); - outapp->priority = 5; - CastToClient()->QueuePacket(outapp); - safe_delete(outapp); - + char spell_link[Links::MAX_LINK_SIZE]; + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid); + Message::InterruptSpell(CastToClient(), message, GetID(), spell_link); SendSpellBarEnable(spellid); } @@ -1355,15 +1329,9 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) } // this is the actual message, it works the same as a formatted message - outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct) + strlen(GetCleanName()) + spelllink.size() + 2); - InterruptCast_Struct* ic = (InterruptCast_Struct*) outapp->pBuffer; - ic->messageid = message_other; - ic->spawnid = GetID(); - // pre-TOB clients will just discard the extra argument here, so don't worry about patching them out in patches - fmt::format_to_n(ic->message, sizeof(GetCleanName()) + spelllink.size() + 1, "{}\x00{}", GetCleanName(), spelllink); - entity_list.QueueCloseClients(this, outapp, true, RuleI(Range, SongMessages), 0, true, IsClient() ? FilterPCSpells : FilterNPCSpells); - safe_delete(outapp); - + char spell_link[Links::MAX_LINK_SIZE]; + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid); + Message::InterruptSpellOther(this, message_other, GetID(), GetCleanName(), spell_link); } // this is like interrupt, just it doesn't spam interrupt packets to everyone @@ -7299,16 +7267,11 @@ void Mob::DoBardCastingFromItemClick(bool is_casting_bard_song, uint32 cast_time Known bug: When a bard uses an augment with a clicky that has a cast time, the cast won't display. This issue only affects bards. */ if (is_casting_bard_song) { - //For spells with cast times. Cancel song cast, stop pusling and start item cast. + //For spells with cast times. Cancel song cast, stop pulsing and start item cast. if (cast_time != 0) { - EQApplicationPacket *outapp = nullptr; - outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct)); - InterruptCast_Struct* ic = (InterruptCast_Struct*)outapp->pBuffer; - ic->messageid = SONG_ENDS; - ic->spawnid = GetID(); - outapp->priority = 5; - CastToClient()->QueuePacket(outapp); - safe_delete(outapp); + char spell_link[Links::MAX_LINK_SIZE]; + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id); + Message::InterruptSpell(CastToClient(), SONG_ENDS, GetID(), spell_link); ZeroCastingVars(); ZeroBardPulseVars(); diff --git a/zone/string_ids.h b/zone/string_ids.h index 40293af0e..540b745cb 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -41,8 +41,8 @@ #define DOORS_INSUFFICIENT_SKILL 132 //You are not sufficiently skilled to pick this lock. #define DOORS_GM 133 //You opened the locked door with your magic GM key. #define ITEMS_INSUFFICIENT_LEVEL 136 //You are not sufficient level to use this item. -#define GAIN_XP 138 //You gain experience!! -#define GAIN_GROUPXP 139 //You gain party experience!! +#define GAIN_XP 138 //You gain experience!! // TODO: TOB added experience value - You gain experience!%1 +#define GAIN_GROUPXP 139 //You gain party experience!! // TODO: TOB added experience value - You gain party experience!%1 #define BOW_DOUBLE_DAMAGE 143 //Your bow shot did double dmg. #define YOU_ARE_BEING_BANDAGED 147 //Someone is bandaging you. #define FORAGE_GRUBS 150 //You have scrounged up some fishing grubs. @@ -69,7 +69,7 @@ #define SPELL_FIZZLE 173 //Your spell fizzles! #define MUST_EQUIP_ITEM 179 //You cannot use this item unless it is equipped. #define MISS_NOTE 180 //You miss a note, bringing your song to a close! -#define CANNOT_USE_ITEM 181 //Your race, class, or deity cannot use this item. +#define CANNOT_USE_ITEM 181 //Your race, class, or deity cannot use this item. // TODO: TOB moved this: 6611 Your class, deity, and/or race may not equip %1. #define ITEM_OUT_OF_CHARGES 182 //Item is out of charges. #define ALREADY_ON_A_MOUNT 189 //You are already on a mount. #define TARGET_NO_MANA 191 //Your target has no mana to affect @@ -106,7 +106,7 @@ #define NO_PET 255 //You do not have a pet. #define GATE_FAIL 260 //Your gate is too unstable, and collapses. #define CORPSE_CANT_SENSE 262 //You cannot sense any corpses for this PC in this zone. -#define SPELL_NO_HOLD 263 //Your spell did not take hold. +#define SPELL_NO_HOLD 263 //Your spell did not take hold. // TODO: This one is complex. There are 4 replacement strings, all taking arguments 9164, 9209, 9210, 9211 #define CANNOT_CHARM 267 //This NPC cannot be charmed. #define SPELL_NO_EFFECT 268 //Your target looks unaffected. #define NO_INSTRUMENT_SKILL 269 //Stick to singing until you learn to play this instrument. @@ -123,9 +123,9 @@ #define DISARMED_TRAP 305 //You have disarmed the trap. #define LDON_SENSE_TRAP1 306 //You do not Sense any traps. #define TRADESKILL_NOCOMBINE 334 //You cannot combine these items in this container type! -#define TRADESKILL_FAILED 336 //You lacked the skills to fashion the items together. +#define TRADESKILL_FAILED 336 //You lacked the skills to fashion the items together. // TODO: TOB - 336 You lacked the skills to fashion %1. #define TRADESKILL_TRIVIAL 338 //You can no longer advance your skill from making this item. -#define TRADESKILL_SUCCEED 339 //You have fashioned the items together to create something new! +#define TRADESKILL_SUCCEED 339 //You have fashioned the items together to create something new! // TODO: TOB - 339 You have %2fashioned the items together to create something new: %1. #define EVADE_SUCCESS 343 //You have momentarily ducked away from the main combat. #define EVADE_FAIL 344 //Your attempts at ducking clear of combat fail. #define HIDE_FAIL 345 //You failed to hide yourself. @@ -136,11 +136,11 @@ #define MEND_SUCCESS 350 //You mend your wounds and heal some damage. #define MEND_WORSEN 351 //You have worsened your wounds! #define MEND_FAIL 352 //You have failed to mend your wounds. -#define LDON_SENSE_TRAP2 367 //You have not detected any traps. -#define TRAP_TOO_FAR 368 //You are too far away from that trap to affect it. -#define FAIL_DISARM_DETECTED_TRAP 370 //You fail to disarm the detected trap. +#define LDON_SENSE_TRAP2 367 //You have not detected any traps. // TODO: TOB - unk +#define TRAP_TOO_FAR 368 //You are too far away from that trap to affect it. // TODO: TOB - unk +#define FAIL_DISARM_DETECTED_TRAP 370 //You fail to disarm the detected trap. // TODO: TOB - unk #define LOOT_LORE_ERROR 371 //You cannot loot this Lore Item. You already have one. -#define PICK_LORE 379 //You cannot pick up a lore item you already possess. +#define PICK_LORE 379 //You cannot pick up a lore item you already possess. // TODO: TOB - 379 You cannot pick up %1 because it is a lore item you already possess. #define POISON_TOO_HIGH 382 //This poison is too high level for you to apply. #define TAUNT_TOO_FAR 386 //You are too far away from your target to taunt. #define CORPSE_TOO_FAR 389 //The corpse is too far away to summon. @@ -154,17 +154,17 @@ #define SONG_NEEDS_WIND 406 //You need to play a wind instrument for this song #define SONG_NEEDS_STRINGS 407 //You need to play a stringed instrument for this song #define SONG_NEEDS_BRASS 408 //You need to play a brass instrument for this song -#define AA_GAIN_ABILITY 410 //You have gained the ability "%T1" at a cost of %2 ability %T3. -#define AA_IMPROVE 411 //You have improved %T1 %2 at a cost of %3 ability %T4. -#define TAUNT_SUCCESS 412 //You taunt %1 to ignore others and attack you! +#define AA_GAIN_ABILITY 410 //You have gained the ability "%T1" at a cost of %2 ability %T3. // TODO: TOB - You have gained the ability "%B1(1)" at a cost of %2 ability %T3. +#define AA_IMPROVE 411 //You have improved %T1 %2 at a cost of %3 ability %T4. // TODO: TOB - You have improved %B1(1) %2 at a cost of %3 ability %T4. +#define TAUNT_SUCCESS 412 //You taunt %1 to ignore others and attack you! // TODO: TOB - unk #define AA_REUSE_MSG 413 //You can use the ability %B1(1) again in %2 hour(s) %3 minute(s) %4 seconds. #define AA_REUSE_MSG2 414 //You can use the ability %B1(1) again in %2 minute(s) %3 seconds. -#define YOU_HEALED 419 //%1 has healed you for %2 points of damage. +#define YOU_HEALED 419 //%1 has healed you for %2 points of damage. // TODO: TOB - 12998 %1 healed you for %2 hit points by %3. #define BEGINS_TO_GLOW 422 //Your %1 begins to glow. #define ALREADY_INVIS 423 //%1 tries to cast an invisibility spell on you, but you are already invisible. #define YOU_ARE_PROTECTED 424 //%1 tries to cast a spell on you, but you are protected. -#define TARGET_RESISTED 425 //Your target resisted the %1 spell. -#define YOU_RESIST 426 //You resist the %1 spell! +#define TARGET_RESISTED 425 //Your target resisted the %1 spell. // TODO: TOB - 425 %1 resisted your %2! +#define YOU_RESIST 426 //You resist the %1 spell! // TODO: TOB - 426 You resist %1! #define YOU_CRIT_HEAL 427 //You perform an exceptional heal! (%1) #define YOU_CRIT_BLAST 428 //You deliver a critical blast! (%1) #define SUMMONING_CORPSE 429 //Summoning your corpse. @@ -176,15 +176,15 @@ #define PET_TAUNTING 438 //Taunting attacker, Master. #define INTERRUPT_SPELL 439 //Your spell is interrupted. #define LOSE_LEVEL 442 //You LOST a level! You are now level %1! -#define GAIN_ABILITY_POINT 446 //You have gained an ability point! You now have %1 ability point%2. +#define GAIN_ABILITY_POINT 446 //You have gained an ability point! You now have %1 ability point%2. // TODO: TOB - 446 You have gained an ability point! You now have %1 ability points. (Actual system moved to 8019-8021 and can handle multiples) #define GAIN_LEVEL 447 //You have gained a level! Welcome to level %1! #define LANG_SKILL_IMPROVED 449 //Your language skills have improved. -#define OTHER_LOOTED_MESSAGE 466 //--%1 has looted a %2-- -#define LOOTED_MESSAGE 467 //--You have looted a %1-- -#define FACTION_WORST 469 //Your faction standing with %1 could not possibly get any worse. -#define FACTION_WORSE 470 //Your faction standing with %1 got worse. -#define FACTION_BEST 471 //Your faction standing with %1 could not possibly get any better. -#define FACTION_BETTER 472 //Your faction standing with %1 got better. +#define OTHER_LOOTED_MESSAGE 466 //--%1 has looted a %2-- // TODO: TOB - 466 --%1 has looted %2 %3 from %4.-- +#define LOOTED_MESSAGE 467 //--You have looted a %1-- // TODO: TOB - 467 --You have looted %1 %2 from %3.-- +#define FACTION_WORST 469 //Your faction standing with %1 could not possibly get any worse. // TODO: TOB - 469 Your faction standing with %B1(45) could not possibly get any worse. +#define FACTION_WORSE 470 //Your faction standing with %1 got worse. // TODO: TOB - 14261 Your faction standing with %B1(45) has been adjusted by %2. +#define FACTION_BEST 471 //Your faction standing with %1 could not possibly get any better. // TODO: TOB - 471 Your faction standing with %B1(45) could not possibly get any better. +#define FACTION_BETTER 472 //Your faction standing with %1 got better. // TODO: TOB - 14261 Your faction standing with %B1(45) has been adjusted by %2. #define PET_REPORT_HP 488 //I have %1 percent of my hit points left. #define PET_NO_TAUNT 489 //No longer taunting attackers, Master. #define PET_DO_TAUNT 490 //Taunting attackers as normal, Master. @@ -241,7 +241,7 @@ #define NPC_RAMPAGE 1044 //%1 goes on a RAMPAGE! #define NPC_FLURRY 1045 //%1 executes a FLURRY of attacks on %2! #define DISCIPLINE_FEARLESS 1076 //%1 becomes fearless. -#define DUEL_FINISHED 1088 //dont know text +#define DUEL_FINISHED 1088 //%1 has defeated %2 in a duel to the death! #define EATING_MESSAGE 1091 //Chomp, chomp, chomp... %1 takes a bite from a %2. #define DRINKING_MESSAGE 1093 //Glug, glug, glug... %1 takes a drink from a %2. #define SUCCESSFUL_TAUNT 1095 //I'll teach you to interfere with me %3. @@ -285,8 +285,8 @@ #define MERCHANT_CLOSED_TWO 1200 //Can't you see I'm doing something here? #define MERCHANT_CLOSED_THREE 1201 //I am not open for business right now. #define AA_POINTS 1215 //points -#define SPELL_FIZZLE_OTHER 1218 //%1's spell fizzles! -#define MISSED_NOTE_OTHER 1219 //A missed note brings %1's song to a close! +#define SPELL_FIZZLE_OTHER 1218 //%1's spell fizzles! // TODO: TOB - 1218 %1's %2 spell fizzles! +#define MISSED_NOTE_OTHER 1219 //A missed note brings %1's song to a close! // TODO: TOB - 1219 A missed note brings %1's %2 to a close! #define SPELL_LEVEL_REQ 1226 //This spell only works on people who are level %1 and under. #define CORPSE_DECAY_NOW 1227 //This corpse is waiting to expire. #define CORPSE_ITEM_LOST 1228 //Your items will no longer stay with you when you respawn on death. You will now need to return to your corpse for your items. @@ -327,27 +327,27 @@ #define SENSE_CORPSE_DIRECTION 1563 //You sense a corpse in this direction. #define DUPE_LORE_MERCHANT 1573 //%1 tells you, 'You already have the lore item, %2, on your person, on your shroud, in the bank, in a real estate, or as an augment in another item. You cannot have more than one of a particular lore item at a time.' #define QUEUED_TELL 2458 //[queued] -#define QUEUE_TELL_FULL 2459 //[zoing and queue is full] +#define QUEUE_TELL_FULL 2459 //[zoning and queue is full] #define TRADER_BUSY_TWO 3192 //Sorry, that action cannot be performed while trading. #define SUSPEND_MINION_UNSUSPEND 3267 //%1 tells you, 'I live again...' #define SUSPEND_MINION_SUSPEND 3268 //%1 tells you, 'By your command, master.' -#define ONLY_SUMMONED_PETS 3269 //3269 This effect only works with summoned pets. +#define ONLY_SUMMONED_PETS 3269 //This effect only works with summoned pets. #define SUSPEND_MINION_FIGHTING 3270 //Your pet must be at peace, first. #define SHIELD_TARGET_NPC 3278 //You must first target a living Player Character. #define ALREADY_SHIELDED 3279 //Either you or your target is already being shielded. #define ALREADY_SHIELDING 3280 //Either you or your target is already shielding another. #define START_SHIELDING 3281 //%1 begins to use %2 as a living shield! #define END_SHIELDING 3282 //%1 ceases protecting %2. -#define OVER_AA_CAP 3374 //Warning: You are currently over the earned Advancement point limit of %1. Please spend some of your stored AA points. The NEXT time you zone all of your AA points over %2 will be deleted. You MUST spend the extra points now. +#define OVER_AA_CAP 3374 //Warning: You are currently over the earned Advancement point limit of %1. Please spend some of your stored AA points. The NEXT time you zone all of your AA points over %2 will be deleted. You MUST spend the extra points now. // TODO: TOB - 3374 You are currently over the earned Advancement point limit of %1. You must spend some of your ability points before you can earn more. #define TRADESKILL_MISSING_ITEM 3455 //You are missing a %1. #define TRADESKILL_MISSING_COMPONENTS 3456 //Sorry, but you don't have everything you need for this recipe in your general inventory. #define TRADESKILL_LEARN_RECIPE 3457 //You have learned the recipe %1! #define TASK_UPDATED 3471 //Your task '%1' has been updated. #define YOU_ASSIGNED_TASK 3472 //You have been assigned the task '%1'. #define DZ_YOU_BELONG 3500 //You cannot create this expedition since you already belong to another. -#define DZ_REPLAY_YOU 3501 //You cannot create this expedition for another %1d:%2h:%3m since you have recently played here. -#define DZ_PLAYER_COUNT 3503 //You do not meet the player count requirement. You have %1 players. You must have at least %2 and no more than %3. -#define DZ_REPLAY_OTHER 3504 //%1 cannot be added to this expedition for another %2D:%3H:%4M since they have recently played in this area. +#define DZ_REPLAY_YOU 3501 //You cannot create this expedition for another %1d:%2h:%3m since you have recently played here. // TODO: TOB - 3501 You cannot create this expedition for another %1d:%2h:%3m:%4s since you have recently played here. +#define DZ_PLAYER_COUNT 3503 //You do not meet the player count requirement. You have %1 players. You must have at least %2 and no more than %3. // TODO: TOB - 3503 You do not meet the player count requirement. You have %1 players. You must have at least %2. +#define DZ_REPLAY_OTHER 3504 //%1 cannot be added to this expedition for another %2D:%3H:%4M since they have recently played in this area. // TODO: TOB - 3504 %1 cannot be added to this expedition for another %2d:%3h:%4m:%5s since they have recently played in this area. #define DZ_AVAILABLE 3507 //%1 is now available to you. #define DZADD_INVITE 3508 //Sending an invitation to: %1. #define DZ_PREVENT_ENTERING 3510 //A strange magical presence prevents you from entering. It's too dangerous to enter at the moment. @@ -358,7 +358,7 @@ #define DZ_REMOVED 3516 //%1 has been removed from %2. #define DZSWAP_INVITE 3517 //Sending an invitation to: %1. They must accept in order to swap party members. #define DZ_LEADER_OFFLINE 3518 //%1 is not currently online. You can only transfer leadership to an online member of the expedition you are in. -#define DZ_TIMER 3519 //You have %1d:%2h:%3m remaining until you may enter %4. +#define DZ_TIMER 3519 //You have %1d:%2h:%3m remaining until you may enter %4. // TODO: TOB - 3519 You have %1d:%2h:%3m:%4s remaining until you may enter %5. #define DZ_LEADER_NAME 3520 //%1 has been made the leader for this expedition. #define DZ_LEADER_YOU 3521 //You have been made the leader of this expedition. #define DZ_INVITE_ACCEPTED 3522 //%1 has accepted your offer to join your expedition. @@ -371,8 +371,8 @@ #define DZ_MINUTES_REMAIN 3551 //You only have %1 minutes remaining before this expedition comes to an end. #define DZ_LEADER 3552 //Expedition Leader: %1 #define DZ_MEMBERS 3553 //Expedition Members: %1 -#define DZ_EVENT_TIMER 3561 //%1 cannot be added to this expedition since they have recently experienced %2. They must wait another %3D:%4H:%5M until they can experience it again. They may be added to the expedition later, once %2 has been completed. -#define LOOT_NOT_ALLOWED 3562 //You are not allowed to loot the item: %1. +#define DZ_EVENT_TIMER 3561 //%1 cannot be added to this expedition since they have recently experienced %2. They must wait another %3D:%4H:%5M until they can experience it again. They may be added to the expedition later, once %2 has been completed. // TODO: TOB - 3561 %1 cannot be added to this expedition since they have recently experienced %2. They must wait another %3d:%4h:%5m:%6s until they can experience it again. They may be added to the expedition later, once %2 has been completed. +#define LOOT_NOT_ALLOWED 3562 //You are not allowed to loot the item: %1. // TODO: TOB - 8337 & 8338 (has a reason now) #define DZ_UNABLE_RETRIEVE_LEADER 3583 //Unable to retrieve dynamic zone leader to check permissions. #define DZADD_NOT_ALLOWING 3585 //The expedition is not allowing players to be added. #define DZADD_NOT_ONLINE 3586 //%1 is not currently online. A player needs to be online to be added to a Dynamic Zone @@ -380,8 +380,8 @@ #define DZADD_ALREADY_PART 3588 //You can not add %1 since they are already part of this zone. #define DZADD_LEAVE_ZONE 3589 //You can not add %1 since they first need to leave the zone before being allowed back in. #define DZADD_ALREADY_OTHER 3590 //%1 can not be added to this dynamic zone since they are already assigned to another dynamic zone. -#define DZADD_REPLAY_TIMER 3591 //%1 can not be added to this dynamic zone for another %2D:%3H:%4M since they have recently played this zone. -#define DZADD_EVENT_TIMER 3592 //%1 can not be added to this dynamic zone since they have recently experienced %2. They must wait for another %3D:%4H:%5M, or until event %2 has occurred. +#define DZADD_REPLAY_TIMER 3591 //%1 can not be added to this dynamic zone for another %2D:%3H:%4M since they have recently played this zone. // TODO: TOB - 3591 %1 can not be added to this dynamic zone for another %2d:%3h:%4m:%5s since they have recently played this zone. +#define DZADD_EVENT_TIMER 3592 //%1 can not be added to this dynamic zone since they have recently experienced %2. They must wait for another %3D:%4H:%5M, or until event %2 has occurred. // TODO: TOB - 3592 %1 can not be added to this dynamic zone since they have recently experienced %2. They must wait for another %3d:%4h:%5m:%6s, or until event %2 has occurred. #define DZADD_PENDING 3593 //%1 currently has an outstanding invitation to join this Dynamic Zone. #define DZADD_PENDING_OTHER 3594 //%1 currently has an outstanding invitation to join another Dynamic Zone. Players may only have one invitation outstanding. #define DZSWAP_CANNOT_REMOVE 3595 //%1 can not be removed from this dynamic zone since they are not assigned to it. @@ -405,9 +405,9 @@ #define PETITION_NO_DELETE 5053 //You do not have a petition in the queue. #define PETITION_DELETED 5054 //Your petition was successfully deleted. #define ALREADY_IN_RAID 5060 //%1 is already in a raid. -#define ALREADY_IN_YOUR_RAID 5077 //%1 is already in your raid. +#define ALREADY_IN_YOUR_RAID 5077 //%1 is already in your raid. // TODO: TOB - 5077 That person is already in your raid. #define NOT_IN_YOUR_RAID 5082 //%1 is not in your raid. -#define GAIN_RAIDEXP 5085 //You gained raid experience! +#define GAIN_RAIDEXP 5085 //You gained raid experience! // TODO: TOB - 5085 You gained raid experience!%1 #define ALREADY_IN_GRP_RAID 5088 //% 1 rejects your invite because they are in a raid and you are not in theirs, or they are a raid group leader #define DUNGEON_SEALED 5141 //The gateway to the dungeon is sealed off to you. Perhaps you would be able to enter if you needed to adventure there. #define ADVENTURE_COMPLETE 5147 //You received %1 points for successfully completing the adventure. @@ -423,8 +423,8 @@ #define MELEE_SILENCE 5806 //You *CANNOT* use this melee ability, you are suffering from amnesia! #define DISCIPLINE_REUSE_MSG 5807 //You can use the ability %1 again in %2 hour(s) %3 minute(s) %4 seconds. #define DISCIPLINE_REUSE_MSG2 5808 //You can use the ability %1 again in %2 minute(s) %3 seconds. -#define FAILED_TAUNT 5811 //You have failed to taunt your target. -#define PHYSICAL_RESIST_FAIL 5817 //Your target avoided your %1 ability. +#define FAILED_TAUNT 5811 //You have failed to taunt your target. // TODO: TOB - 5811 You have failed to taunt %1. +#define PHYSICAL_RESIST_FAIL 5817 //Your target avoided your %1 ability. // TODO: TOB - 5817 %1 avoided your %2! #define AA_NO_TARGET 5825 //You must first select a target for this ability! #define YOU_RECEIVE 5941 //You receive %1. #define NO_TASK_OFFERS 6009 //Sorry %3, I don't have anything for someone with your abilities. @@ -447,7 +447,7 @@ #define TRANSFORM_COMPLETE 6327 //You have successfully transformed your %1. #define DETRANSFORM_FAILED 6341 //%1 has no transformation that can be removed. #define GUILD_PERMISSION_FAILED 6418 //You do not have permission to change access options. -#define PARCEL_DELIVERY_ARRIVED 6465 //You have received a new parcel delivery! +#define PARCEL_DELIVERY_ARRIVED 6465 //You have received a new parcel delivery! // TODO: TOB - 6465 You have received a new parcel delivery %1! #define PARCEL_DELIVERY 6466 //%1 tells you, 'I will deliver the %2 to %3 as soon as possible!' #define PARCEL_UNKNOWN_NAME 6467 //%1 tells you, 'Unfortunately, I don't know anyone by the name of %2. Here is your %3 back.'' #define PARCEL_DELIVERED 6471 //%1 hands you the %2 that was sent from %3. @@ -474,7 +474,7 @@ #define LDON_CERTAIN_TRAP 7557 //You are certain that %1 is trapped. #define LDON_CERTAIN_NOT_TRAP 7558 //You are certain that %1 is not trapped. #define LDON_CANT_DETERMINE_TRAP 7559 //You are unable to determine if %1 is trapped. -#define LDON_PICKLOCK_SUCCESS 7560 //You have successfully picked %1! +#define LDON_PICKLOCK_SUCCESS 7560 //You have successfully picked %1! // TODO: TOB - 7560 You have successfully picked %1%2! #define LDON_PICKLOCK_FAILURE 7561 //You have failed to pick %1. #define LDON_STILL_LOCKED 7562 //You cannot open %1, it is locked. #define LDON_BASH_CHEST 7563 //%1 try to %2 %3, but do no damage. @@ -497,7 +497,7 @@ #define NOT_DELEGATED_MARKER 8794 //You have not been delegated Raid Mark #define GAIN_GROUP_LEADERSHIP_EXP 8788 // #define GAIN_RAID_LEADERSHIP_EXP 8789 // -#define BUFF_MINUTES_REMAINING 8799 //%1 (%2 minutes remaining) +#define BUFF_MINUTES_REMAINING 8799 //%1 (%2 minutes remaining) // TODO: TOB - 8799 --You sense %1%2 on %3 has %4.-- #define RAID_NO_LONGER_MARKED 8801 //%1 is no longer marked #define YOU_HAVE_BEEN_GIVEN 8994 //You have been given: %1 #define NO_MORE_TRAPS 9002 //You have already placed your maximum number of traps. @@ -508,13 +508,13 @@ #define SPELL_OPPOSITE_EFFECT 9032 //Your spell may have had the opposite effect of what you desired. #define HAS_BEEN_AWAKENED 9037 //%1 has been awakened by %2. #define YOU_HEAL 9068 //You have healed %1 for %2 points of damage. -#define YOUR_HIT_DOT 9072 //%1 has taken %2 damage from your %3. -#define HIT_NON_MELEE 9073 //%1 hit %2 for %3 points of non-melee damage. +#define YOUR_HIT_DOT 9072 //%1 has taken %2 damage from your %3. // TODO: TOB - 9072 %1 has taken %2 damage from your %3.%4 +#define HIT_NON_MELEE 9073 //%1 hit %2 for %3 points of non-melee damage. // TODO: TOB - 9073 %1 hit %2 for %3 points of %4 damage by %5.%6 #define GLOWS_BLUE 9074 //Your %1 glows blue. #define GLOWS_RED 9075 //Your %1 glows red. #define SHAKE_OFF_STUN 9077 //You shake off the stun effect! #define STRIKETHROUGH_STRING 9078 //You strike through your opponent's defenses! -#define SPELL_REFLECT 9082 //%1's spell has been reflected by %2. +#define SPELL_REFLECT 9082 //%1's spell has been reflected by %2. // TODO: TOB - 9082 %1's %2 spell has been reflected by %3. #define NO_MORE_AURAS 9160 //You do not have sufficient focus to maintain that ability. #define NEW_SPELLS_AVAIL 9149 //You have new spells available to you. Check the merchants near your guild master. #define FD_CAST_ON_NO_BREAK 9174 //The strength of your will allows you to resume feigning death. @@ -527,17 +527,17 @@ #define NO_CAST_OUT_OF_COMBAT 9191 //You can not cast this spell while out of combat. #define NO_ABILITY_IN_COMBAT 9192 //You can not use this ability while in combat. #define NO_ABILITY_OUT_OF_COMBAT 9194 //You can not use this ability while out of combat. -#define GAIN_GROUPXP_BONUS 9298 //You gain party experience (with a bonus)! -#define GAIN_GROUPXP_PENALTY 9301 //You gain party experience (with a penalty)! -#define GAIN_RAIDXP_BONUS 9302 //You gained raid experience (with a bonus)! -#define GAIN_RAIDXP_PENALTY 9303 //You gained raid experience (with a penalty)! +#define GAIN_GROUPXP_BONUS 9298 //You gain party experience (with a bonus)! // TODO: TOB - 9298 You gain party experience (with a bonus)!%1 +#define GAIN_GROUPXP_PENALTY 9301 //You gain party experience (with a penalty)! // TODO: TOB - 9301 You gain party experience (with a penalty)!%1 +#define GAIN_RAIDXP_BONUS 9302 //You gained raid experience (with a bonus)! // TODO: TOB - 9302 You gained raid experience (with a bonus)!%1 +#define GAIN_RAIDXP_PENALTY 9303 //You gained raid experience (with a penalty)! // TODO: TOB - 9303 You gained raid experience (with a penalty)!%1 #define LESSER_SPELL_VERSION 11004 //%1 is a lesser version of %2 and cannot be scribed #define AE_RAMPAGE 11015 //%1 goes on a WILD RAMPAGE! #define GROUP_IS_FULL 12000 //You cannot join that group, it is full. #define FACE_ACCEPTED 12028 //Facial features accepted. #define TRACKING_BEGIN 12040 //You begin tracking %1. #define SPELL_LEVEL_TO_LOW 12048 //You will have to achieve level %1 before you can scribe the %2. -#define YOU_RECEIVE_AS_SPLIT 12071 //You receive %1 as your split. +#define YOU_RECEIVE_AS_SPLIT 12071 //You receive %1 as your split. // TODO: TOB - 12072 You receive %1 from the corpse%2. #define ATTACKFAILED 12158 //%1 try to %2 %3, but %4! #define HIT_STRING 12183 //hit #define CRUSH_STRING 12191 //crush @@ -554,7 +554,7 @@ #define TARGET_PLAYER_FOR_GUILD_STATUS 12260 #define TARGET_ALREADY_IN_GROUP 12265 //% 1 is already in another group. #define GROUP_INVITEE_NOT_FOUND 12268 //You must target a player or use /invite to invite someone to your group. -#define GROUP_INVITEE_SELF 12270 //12270 You cannot invite yourself. +#define GROUP_INVITEE_SELF 12270 //You cannot invite yourself. #define ALREADY_IN_PARTY 12272 //That person is already in your party. #define TALKING_TO_SELF 12323 //Talking to yourself again? #define SPLIT_NO_GROUP 12328 //You are not in a group! Keep it all. @@ -593,7 +593,7 @@ #define RANGED_TOO_CLOSE 12698 //Your target is too close to use a ranged weapon! #define BACKSTAB_WEAPON 12874 //You need a piercing weapon as your primary weapon in order to backstab #define DISARMED 12889 //You have been disarmed! -#define DISARM_SUCCESS 12890 //You disarmed %1! +#define DISARM_SUCCESS 12890 //You disarmed %1! // TODO: TOB - 12890 You %2disarmed %1! #define DISARM_FAILED 12891 //Your attempt to disarm failed. #define MORE_SKILLED_THAN_I 12931 //%1 tells you, 'You are more skilled than I! What could I possibly teach you?' #define SURNAME_EXISTS 12939 //You already have a surname. Operation failed. @@ -602,7 +602,7 @@ #define REPORT_ONCE 12945 //You may only submit a report once per time that you zone. Thank you. #define NOW_INVISIBLE 12950 //%1 is now Invisible. #define NOW_VISIBLE 12951 //%1 is now Visible. -#define YOU_TAKE_DOT 12954 //You have taken %1 damage from %2 by %3 +#define YOU_TAKE_DOT 12954 //You have taken %1 damage from %2 by %3 // TODO: TOB - 12954 You have taken %1 damage from %2 by %3.%4 #define GUILD_NOT_MEMBER2 12966 //You are not in a guild. #define HOT_HEAL_SELF 12976 //You have been healed for %1 hit points by your %2. #define HOT_HEAL_OTHER 12997 //You have healed %1 for %2 hit points with your %3. @@ -612,7 +612,7 @@ #define TOGGLE_ON 13172 //Asking server to turn ON your incoming tells. #define TOGGLE_OFF 13173 //Asking server to turn OFF all incoming tells for you. #define DUEL_INPROGRESS 13251 //You have already accepted a duel with someone else cowardly dog. -#define OTHER_HIT_DOT 13327 //%1 has taken %2 damage from %3 by %4. -#define GAIN_XP_BONUS 14541 //You gain experience (with a bonus)! -#define GAIN_XP_PENALTY 14542 //You gain experience (with a penalty)! +#define OTHER_HIT_DOT 13327 //%1 has taken %2 damage from %3 by %4. // TODO: TOB - 13327 %1 has taken %2 damage from %3 by %4.%5 +#define GAIN_XP_BONUS 14541 //You gain experience (with a bonus)! // TODO: TOB - 14541 You gain experience (with a bonus)!%1 +#define GAIN_XP_PENALTY 14542 //You gain experience (with a penalty)! // TODO: TOB - 14542 You gain experience (with a penalty)!%1 #define GENERIC_MISS 15041 //%1 missed %2