diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index cca106273..08cec8df8 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -6575,5 +6575,21 @@ struct EvolveGetNextItem { uint32 max_transfer_level; }; +// Character Create Data +struct RaceClassAllocation { + unsigned int Index; + unsigned int BaseStats[7]; + unsigned int DefaultPointAllocation[7]; +}; + +struct RaceClassCombos { + unsigned int ExpansionRequired; + unsigned int Race; + unsigned int Class; + unsigned int Deity; + unsigned int AllocationIndex; + unsigned int Zone; +}; + // Restore structure packing to default #pragma pack(pop) diff --git a/common/links.cpp b/common/links.cpp index 4afac085d..2e87a62e9 100644 --- a/common/links.cpp +++ b/common/links.cpp @@ -4,18 +4,48 @@ #include "links.h" -#include "spdat.h" - -void Links::FormatItemLink(char* Buffer, size_t BufferSize, const EQ::ItemInstance* item) +void Links::FormatItemLink(char* Buffer, size_t BufferSize, const std::string& text, const EQ::SayLinkBody_Struct item) { - // TODO: Reverse 0x14064B220 to get definition of this function + // interspersed between augs and after ornament_icon are assumed zero + // This is specifically the TOB style item link (ref: 0x14064B220), this could be in patches + snprintf(Buffer, BufferSize, + "%c%d" + "%05X" + "%05X%05X" // aug1, luck + "%05X%05X" // aug2, luck + "%05X%05X" // aug3, luck + "%05X%05X" // aug4, luck + "%05X%05X" // aug5, luck + "%05X%05X" // aug6, luck + "%1d" // is_evolving + "%04X" // evolve_group + "%02X" // evolve_level + "%05X" // ornament_icon + "%05X" // luck + "%08X" // hash + "%s%c", + ITEM_TAG_CHAR, ETAG_ITEM, + item.item_id, + item.augment_1, 0u, // Set Luck to 0 for all augs until server supports it + item.augment_2, 0u, // Set Luck to 0 for all augs until server supports it + item.augment_3, 0u, // Set Luck to 0 for all augs until server supports it + item.augment_4, 0u, // Set Luck to 0 for all augs until server supports it + item.augment_5, 0u, // Set Luck to 0 for all augs until server supports it + item.augment_6, 0u, // Set Luck to 0 for all augs until server supports it + item.is_evolving, + item.evolve_group, + item.evolve_level, + item.ornament_icon, + 0u, // Set Luck to 0 until server supports it + item.hash, + text.c_str(), ITEM_TAG_CHAR + ); } -void Links::FormatSpellLink(char* Buffer, size_t BufferSize, int32_t SpellID, - const char* spellNameOverride) +void Links::FormatSpellLink(char* Buffer, size_t BufferSize, int32_t SpellID, const char* spellName) { snprintf(Buffer, BufferSize, "%c%d3^%d^0^'%s%c", ITEM_TAG_CHAR, ETAG_SPELL, SpellID, - spellNameOverride && spellNameOverride[0] ? spellNameOverride : GetSpellName(SpellID), ITEM_TAG_CHAR); + spellName, ITEM_TAG_CHAR); } void Links::FormatDialogLink(char* Buffer, size_t BufferSize, std::string_view keyword, std::string_view text) @@ -29,3 +59,15 @@ void Links::FormatDialogLink(char* Buffer, size_t BufferSize, std::string_view k static_cast(text.length()), text.data(), ITEM_TAG_CHAR); } } + +void Links::FormatDialogLinkContent(char* Buffer, size_t BufferSize, std::string_view keyword, std::string_view text) +{ + if (text.empty()) { + snprintf(Buffer, BufferSize, "%.*s", + static_cast(keyword.length()), keyword.data()); + } else { + snprintf(Buffer, BufferSize, "%.*s:%.*s", + static_cast(keyword.length()), keyword.data(), + static_cast(text.length()), text.data()); + } +} diff --git a/common/links.h b/common/links.h index fbf1c0fa0..5b0c4e0b4 100644 --- a/common/links.h +++ b/common/links.h @@ -3,9 +3,7 @@ // #pragma once -#include "item_instance.h" - -namespace EQ { class ItemInstance; } +#include "common/say_link.h" namespace Links { @@ -45,12 +43,11 @@ enum ETagCodes // std::string_view playerName); // Create an item link from the given item. -void FormatItemLink(char* Buffer, size_t BufferSize, const EQ::ItemInstance* item); +void FormatItemLink(char* Buffer, size_t BufferSize, const std::string& text, EQ::SayLinkBody_Struct item); -// Create a spell link for the given spell, with optional spell name override. Spells on items often have +// Create a spell link for the given spell, including spell name. Spells on items often have // spell name overrides that changes the display name of the spell. -void FormatSpellLink(char* Buffer, size_t BufferSize, int32_t SpellID, - const char* spellNameOverride = nullptr); +void FormatSpellLink(char* Buffer, size_t BufferSize, int32_t SpellID, const char* spellName); // 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, @@ -58,4 +55,10 @@ void FormatSpellLink(char* Buffer, size_t BufferSize, int32_t SpellID, void FormatDialogLink(char* Buffer, size_t BufferSize, std::string_view keyword, std::string_view text = {}); +// 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 FormatDialogLinkContent(char* Buffer, size_t BufferSize, std::string_view keyword, + std::string_view text = {}); + } diff --git a/common/patches/tob.cpp b/common/patches/tob.cpp index 3a035b08b..e6b41e533 100644 --- a/common/patches/tob.cpp +++ b/common/patches/tob.cpp @@ -28,6 +28,8 @@ Copyright (C) 2001-2026 EQEmu Development Team #include "common/crc32.h" #include "common/eq_packet_structs.h" +#include "common/links.h" +#include "common/say_link.h" #include "common/misc_functions.h" #include "common/strings.h" #include "common/inventory_profile.h" @@ -37,8 +39,6 @@ Copyright (C) 2001-2026 EQEmu Development Team #include "common/packet_dump.h" #include "common/races.h" #include "common/raid.h" -#include "world/sof_char_create_data.h" -#include "zone/client.h" #include "zone/mob.h" #include "zone/string_ids.h" @@ -825,6 +825,8 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_ItemLinkResponse) { ENCODE_FORWARD(OP_ItemPacket); } + ENCODE(OP_ItemPacket) { EQApplicationPacket* in = *p; @@ -832,50 +834,79 @@ namespace TOB uchar* __emu_buffer = in->pBuffer; ItemPacket_Struct* old_item_pkt = (ItemPacket_Struct*)__emu_buffer; - auto type = ServerToTOBItemPacketType(old_item_pkt->PacketType); - if (type == item::ItemPacketType::ItemPacketInvalid) { - delete in; - return; - } - - switch (type) - { - case item::ItemPacketType::ItemPacketParcel: { - ParcelMessaging_Struct pms{}; - EQ::Util::MemoryStreamReader ss(reinterpret_cast(in->pBuffer), in->size); - cereal::BinaryInputArchive ar(ss); - ar(pms); - - auto* int_struct = (EQ::InternalSerializedItem_Struct*)pms.serialized_item.data(); - - SerializeBuffer buffer; - buffer.WriteInt32((int32_t)type); - SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, pms.slot_id, 0, old_item_pkt->PacketType); - - buffer.WriteUInt32(pms.sent_time); - buffer.WriteLengthString(pms.player_name); - buffer.WriteLengthString(pms.note); - - auto outapp = new EQApplicationPacket(OP_ItemPacket, buffer.size()); - outapp->WriteData(buffer.buffer(), buffer.size()); - dest->FastQueuePacket(&outapp, ack_req); - break; - } - default: { + // ItemPacketViewLink (0) is not in the TOB OP_ItemPacket type table. + // The client shows item windows from link clicks via OP_ItemLinkResponse (0x7805), + // whose handler sub_1402136D0 reads [int32 type][int32 position][uint32 display_mode] + // then serialized item, then calls CItemDisplayManager::ShowItem. + if (old_item_pkt->PacketType == ItemPacketViewLink) { EQ::InternalSerializedItem_Struct* int_struct = (EQ::InternalSerializedItem_Struct*)(&__emu_buffer[4]); SerializeBuffer buffer; - buffer.WriteInt32((int32_t)type); - SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, old_item_pkt->PacketType); - - auto outapp = new EQApplicationPacket(OP_ItemPacket, buffer.size()); + buffer.WriteInt32(0); // type: display mode (0 = default) + buffer.WriteInt32(0); // position: window slot (0 = default/new window) + buffer.WriteUInt32(0); // display_mode: ShowItem arg (0 = default) + SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, + old_item_pkt->PacketType); + auto outapp = new EQApplicationPacket(OP_ItemLinkResponse, buffer.size()); outapp->WriteData(buffer.buffer(), buffer.size()); dest->FastQueuePacket(&outapp, ack_req); - } + } else { + switch (const auto type = ServerToTOBItemPacketType(old_item_pkt->PacketType)) { + case item::ItemPacketType::ItemPacketInvalid: + break; + case item::ItemPacketType::ItemPacketParcel: { + ParcelMessaging_Struct pms{}; + EQ::Util::MemoryStreamReader ss(reinterpret_cast(in->pBuffer), in->size); + cereal::BinaryInputArchive ar(ss); + ar(pms); + + auto* int_struct = (EQ::InternalSerializedItem_Struct*)pms.serialized_item.data(); + + SerializeBuffer buffer; + buffer.WriteInt32((int32_t)type); + SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, pms.slot_id, 0, old_item_pkt->PacketType); + + buffer.WriteUInt32(pms.sent_time); + buffer.WriteLengthString(pms.player_name); + buffer.WriteLengthString(pms.note); + + auto outapp = new EQApplicationPacket(OP_ItemPacket, buffer.size()); + outapp->WriteData(buffer.buffer(), buffer.size()); + dest->FastQueuePacket(&outapp, ack_req); + break; + } + default: { + EQ::InternalSerializedItem_Struct* int_struct = (EQ::InternalSerializedItem_Struct*)(&__emu_buffer[4]); + SerializeBuffer buffer; + buffer.WriteInt32((int32_t)type); + SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, old_item_pkt->PacketType); + + auto outapp = new EQApplicationPacket(OP_ItemPacket, buffer.size()); + outapp->WriteData(buffer.buffer(), buffer.size()); + dest->FastQueuePacket(&outapp, ack_req); + break; + } + } } delete in; } + ENCODE(OP_ItemPreviewRequest) + { + EQApplicationPacket* in = *p; + *p = nullptr; + uchar* in_buf = in->pBuffer; + auto int_item = (EQ::InternalSerializedItem_Struct*) in_buf; + + SerializeBuffer buf; + SerializeItem(buf, (const EQ::ItemInstance*) int_item->inst, int_item->slot_id, 0, ItemPacketInvalid); + + auto outapp = new EQApplicationPacket(OP_ItemPreviewRequest, buf.size()); + outapp->WriteData(buf.buffer(), buf.size()); + dest->FastQueuePacket(&outapp, ack_req); + delete in; + } + ENCODE(OP_ItemRecastDelay) { SETUP_DIRECT_ENCODE(ItemRecastDelay_Struct, structs::ItemRecastDelay_Struct); @@ -893,6 +924,45 @@ namespace TOB FINISH_ENCODE(); } + DECODE(OP_ItemLinkClick) + { + // TOB sends 84 bytes (InspectItemSafe); first 52 are layout-compatible with ItemViewRequest_Struct. + // Extra 32 bytes (offsets 52-83) are uncharted — likely luck + DECODE_LENGTH_ATLEAST(ItemViewRequest_Struct); + SETUP_DIRECT_DECODE(ItemViewRequest_Struct, ItemViewRequest_Struct); + MEMSET_IN(ItemViewRequest_Struct); + + IN(item_id); + for (int r = EQ::invaug::SOCKET_BEGIN; r <= EQ::invaug::SOCKET_END; r++) { + IN(augments[r]); + } + IN(link_hash); + IN(icon); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_ItemLinkResponse) + { + // TOB sends 80 bytes; EQEMu LDONItemViewRequest_Struct is 72 bytes. + // First 72 bytes are layout-identical — extra 8 bytes (unknown072/076) are ignored. + DECODE_LENGTH_ATLEAST(LDONItemViewRequest_Struct); + SETUP_DIRECT_DECODE(LDONItemViewRequest_Struct, LDONItemViewRequest_Struct); + MEMSET_IN(LDONItemViewRequest_Struct); + IN(item_id); + memcpy(emu->unknown004, eq->unknown004, sizeof(emu->unknown004)); + strncpy(emu->item_name, eq->item_name, sizeof(emu->item_name) - 1); + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_ItemPreviewRequest) + { + DECODE_LENGTH_EXACT(structs::ItemPreviewRequest_Struct); + SETUP_DIRECT_DECODE(ItemPreview_Struct, structs::ItemPreviewRequest_Struct); + IN(itemid); + FINISH_DIRECT_DECODE(); + } + DECODE(OP_ItemVerifyRequest) { DECODE_LENGTH_EXACT(structs::ItemVerifyRequest_Struct); @@ -5665,64 +5735,48 @@ namespace TOB std::string item_id = segments[segment_iter].substr(index, 5); index += 5; - std::string aug1 = segments[segment_iter].substr(index, 5); - index += 5; + // Say links are encoded as item links with SAYLINK_ITEM_ID (0xFFFFF = "FFFFF"). + // TOB supports dialog links natively, so convert to a proper dialog link. + if (item_id == "FFFFF") { + // keyword = proxy text (display name), phrase = saylink text from cache + std::string keyword = segments[segment_iter].substr(EQ::constants::SAY_LINK_BODY_SIZE); + uint32 aug1_id = std::stoul(segments[segment_iter].substr(6, 5), nullptr, 16); + uint32 aug2_id = std::stoul(segments[segment_iter].substr(11, 5), nullptr, 16); + uint32 saylink_id = (aug1_id != 0) ? aug1_id : aug2_id; + std::string phrase = EQ::SayLinkEngine::GetSaylinkPhrase(saylink_id); - std::string aug2 = segments[segment_iter].substr(index, 5); - index += 5; + // The client's TagBracketedTextAsDialogueResponseLinks runs before ConvertItemTags + // and converts [text] -> [\x124text\x12]. Only send the content of the link in this case + char dialog_link[Links::MAX_LINK_SIZE]; + size_t next_seg = segment_iter + 1; + if (!message_out.empty() && message_out.back() == '[' && + next_seg < segments.size() && !segments[next_seg].empty() && segments[next_seg].front() == ']') { + // only do this if it is enclosed by square brackets + Links::FormatDialogLinkContent(dialog_link, sizeof(dialog_link), keyword, phrase); + } else { + Links::FormatDialogLink(dialog_link, sizeof(dialog_link), keyword, phrase); + } - std::string aug3 = segments[segment_iter].substr(index, 5); - index += 5; + message_out.append(dialog_link); + } else { + EQ::SayLinkBody_Struct body{}; + if (EQ::saylink::DeserializeLinkBody(body, segments[segment_iter].substr(0, EQ::constants::SAY_LINK_BODY_SIZE))) { + std::string text = segments[segment_iter].substr(EQ::constants::SAY_LINK_BODY_SIZE); + char item_link[Links::MAX_LINK_SIZE]; + Links::FormatItemLink(item_link, sizeof(item_link), text, body); - std::string aug4 = segments[segment_iter].substr(index, 5); - index += 5; + // the TOB client will replace anything in between [] with a dialog link, which explicitly + // breaks item links, so replace [] with () + if (!message_out.empty() && message_out.back() == '[') + message_out.back() = '('; - std::string aug5 = segments[segment_iter].substr(index, 5); - index += 5; + message_out.append(item_link); - 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'); + size_t next_seg = segment_iter + 1; + if (next_seg < segments.size() && !segments[next_seg].empty() && segments[next_seg].front() == ']') + segments[next_seg].front() = ')'; + } + } break; } @@ -5740,7 +5794,57 @@ namespace TOB } static void TOBToServerConvertLinks(std::string& message_out, const std::string& message_in) { - message_out = message_in; + if (message_in.find('\x12') == std::string::npos) { + message_out = message_in; + return; + } + + // TOB item link body layout (91 chars, positions within segment after \x12): + // [0] ETAG (1) + // [1-5] item_id (5) + // [6-65] [aug(5) + luck(5)] * 6 = 60 + // [66] is_evolving (1) + // [67-70] evolve_group (4) + // [71-72] evolve_level (2) + // [73-77] ornament_icon (5) + // [78-82] luck (5) + // [83-90] hash (8) + // [91+] text + constexpr size_t TOB_ITEM_BODY_SIZE = 91; + + std::vector segments = Strings::Split(message_in, '\x12'); + for (size_t i = 0; i < segments.size(); ++i) { + if (!(i & 1)) { + message_out.append(segments[i]); + continue; + } + + if (segments[i].empty() || segments[i][0] - '0' != Links::ETAG_ITEM + || segments[i].length() < TOB_ITEM_BODY_SIZE) { + message_out.push_back('\x12'); + message_out.append(segments[i]); + message_out.push_back('\x12'); + continue; + } + + // Strip luck fields to produce RoF2 body: + // action_id(1) item_id(5) aug1-6(30) is_evolving(1) evolve_group(4) + // evolve_level(2) ornament_icon(5) hash(8) text + message_out.push_back('\x12'); + message_out += segments[i][0]; // action_id + message_out += segments[i].substr(1, 5); // item_id + for (int aug = 0; aug < 6; ++aug) { + message_out += segments[i].substr(6 + aug * 10, 5); // aug, skip luck + } + message_out += segments[i].substr(66, 1); // is_evolving + message_out += segments[i].substr(67, 4); // evolve_group + message_out += segments[i].substr(71, 2); // evolve_level + message_out += segments[i].substr(73, 5); // ornament_icon + // skip luck [78-82] + message_out += segments[i].substr(83, 8); // hash + message_out += segments[i].substr(91); // text + message_out.push_back('\x12'); + } } static inline uint32 ServerToTOBSpawnAppearanceType(uint32 server_type) { diff --git a/common/patches/tob_ops.h b/common/patches/tob_ops.h index 848ee5ca6..ffbca0240 100644 --- a/common/patches/tob_ops.h +++ b/common/patches/tob_ops.h @@ -34,7 +34,9 @@ E(OP_GroupInvite) E(OP_HPUpdate) E(OP_Illusion) E(OP_IncreaseStats) +E(OP_ItemLinkResponse) E(OP_ItemPacket) +E(OP_ItemPreviewRequest) E(OP_ItemRecastDelay) E(OP_ItemVerifyReply) E(OP_LinkedReuse) @@ -110,6 +112,9 @@ D(OP_GMTraining) D(OP_GroupDisband) D(OP_GroupInvite) D(OP_GroupInvite2) +D(OP_ItemLinkClick) +D(OP_ItemLinkResponse) +D(OP_ItemPreviewRequest) D(OP_ItemVerifyRequest) D(OP_LootItem) D(OP_MemorizeSpell) diff --git a/common/patches/tob_structs.h b/common/patches/tob_structs.h index 69b4c61db..359e415c0 100644 --- a/common/patches/tob_structs.h +++ b/common/patches/tob_structs.h @@ -724,6 +724,20 @@ namespace TOB { /*0028*/ }; + struct ItemPreviewRequest_Struct + { + /*0x00*/ uint32 itemid; // GetItemRecordNum() + /*0x04*/ uint32 actor_tag1; // ItemBase::ActorTag1 [+0x5C] if mold applied, else 0 + /*0x08*/ uint32 actor_tag2; // ItemBase::ActorTag2 [+0xF8] if mold applied, else 0 + /*0x0C*/ uint32 mold_points; // GetTotalPointsByThemeEarned if mold, else 0xFFFFFFFF + /*0x10*/ uint32 mold_data1; // sub_14064F970(item) if mold, else 0 + /*0x14*/ uint32 mold_data2; // sub_1406501A0(item) if mold, else 0 + /*0x18*/ uint8 is_marketplace; // 1 when sent from CMarketplaceWnd, else 0 + /*0x19*/ uint8 is_mold_applied; // 1 when transformation mold applied (BYTE1 of word), else 0 + /*0x1A*/ uint16 unknown01a; // uninitialized stack — dword read at var_10 spills 2 extra bytes + /*0x1C*/ + }; + struct ItemRecastDelay_Struct { /*000*/ InventorySlot_Struct item_slot; // zeroed until server struct gains item slot fields diff --git a/common/say_link.cpp b/common/say_link.cpp index 6f0418b7f..06a0b64c0 100644 --- a/common/say_link.cpp +++ b/common/say_link.cpp @@ -17,18 +17,20 @@ */ #include "say_link.h" +#include "common/features.h" #include "common/emu_constants.h" #include "common/strings.h" #include "common/item_instance.h" #include "common/item_data.h" -#include "zone/zonedb.h" +#include "common/shareddb.h" -#include +// static database pointer (must be SharedDatabase since this module is in common) +static SharedDatabase* s_database = nullptr; // static bucket global std::vector g_cached_saylinks = {}; -bool EQ::saylink::DegenerateLinkBody(SayLinkBody_Struct &say_link_body_struct, const std::string &say_link_body) +bool EQ::saylink::DeserializeLinkBody(SayLinkBody_Struct &say_link_body_struct, const std::string &say_link_body) { memset(&say_link_body_struct, 0, sizeof(say_link_body_struct)); if (say_link_body.length() != EQ::constants::SAY_LINK_BODY_SIZE) { @@ -52,8 +54,9 @@ bool EQ::saylink::DegenerateLinkBody(SayLinkBody_Struct &say_link_body_struct, c return true; } -bool EQ::saylink::GenerateLinkBody(std::string &say_link_body, const SayLinkBody_Struct &say_link_body_struct) +bool EQ::saylink::SerializeLinkBody(std::string &say_link_body, const SayLinkBody_Struct &say_link_body_struct) { + // This is specifically the server struct for link bodies say_link_body = StringFormat( "%1X" "%05X" "%05X" "%05X" "%05X" "%05X" "%05X" "%05X" "%1X" "%04X" "%02X" "%05X" "%08X", (0x0F & say_link_body_struct.action_id), @@ -85,10 +88,16 @@ EQ::SayLinkEngine::SayLinkEngine() const std::string &EQ::SayLinkEngine::GenerateLink() { + // At this point, we can assume that we are generated an item link. Any client before TOB can only do item links + // so even dialog links will end up here. Other types of links aren't supported by the server yet, but they + // would require TOB+ without some other hacks like dialog links use now + m_Link.clear(); m_LinkBody.clear(); m_LinkText.clear(); + // These two functions translate multiple server-internal objects into the saylink-internal struct for rendering + // to the packet. Detect the client version after this generate_body(); generate_text(); @@ -162,8 +171,8 @@ void EQ::SayLinkEngine::generate_body() // TODO: add hash call break; case saylink::SayLinkLootItem: - if (m_LootData == nullptr) { break; } - item_data = database.GetItem(m_LootData->item_id); + if (m_LootData == nullptr || !s_database) { break; } + item_data = s_database->GetItem(m_LootData->item_id); if (item_data == nullptr) { break; } m_LinkBodyStruct.item_id = item_data->ID; m_LinkBodyStruct.augment_1 = m_LootData->aug_1; @@ -276,8 +285,8 @@ void EQ::SayLinkEngine::generate_text() m_LinkText = m_ItemData->Name; return; case saylink::SayLinkLootItem: - if (m_LootData == nullptr) { break; } - item_data = database.GetItem(m_LootData->item_id); + if (m_LootData == nullptr || !s_database) { break; } + item_data = s_database->GetItem(m_LootData->item_id); if (item_data == nullptr) { break; } m_LinkText = item_data->Name; return; @@ -373,11 +382,39 @@ std::string EQ::SayLinkEngine::InjectSaylinksIfNotExist(const char *message) return new_message; } +std::string EQ::SayLinkEngine::GetSaylinkPhrase(uint32_t saylink_id) +{ + auto cached_link = std::find_if(g_cached_saylinks.begin(), g_cached_saylinks.end(), + [&saylink_id](const SaylinkRepository::Saylink& saylink) { + return saylink.id == saylink_id; + }); + + if (cached_link != g_cached_saylinks.end()) { + return cached_link->phrase; + } + + if (s_database != nullptr) { + auto saylink = SaylinkRepository::FindOne(*s_database, saylink_id); + return saylink.phrase; // this will just be empty if it's not found, same as the default return case + } + + return {}; +} + +void EQ::SayLinkEngine::SetDatabase(SharedDatabase* db) +{ + s_database = db; +} + void EQ::SayLinkEngine::LoadCachedSaylinks() { - auto saylinks = SaylinkRepository::GetWhere(database, "phrase not REGEXP '[A-Z]' and phrase not REGEXP '[0-9]'"); - LogSaylink("Loaded [{}] saylinks into cache", saylinks.size()); - g_cached_saylinks = saylinks; + if (s_database != nullptr) { + auto saylinks = SaylinkRepository::GetWhere(*s_database, "phrase not REGEXP '[A-Z]' and phrase not REGEXP '[0-9]'"); + LogSaylink("Loaded [{}] saylinks into cache", saylinks.size()); + g_cached_saylinks = saylinks; + } else { + LogSaylink("Failed to load saylinks into cache: no database"); + } } SaylinkRepository::Saylink EQ::SayLinkEngine::GetOrSaveSaylink(std::string saylink_text) @@ -391,26 +428,28 @@ SaylinkRepository::Saylink EQ::SayLinkEngine::GetOrSaveSaylink(std::string sayli } } - auto saylinks = SaylinkRepository::GetWhere( - database, - fmt::format("phrase = '{}'", Strings::Escape(saylink_text)) - ); + if (s_database != nullptr) { + auto saylinks = SaylinkRepository::GetWhere( + *s_database, + fmt::format("phrase = '{}'", Strings::Escape(saylink_text)) + ); - // return if found from the database - if (!saylinks.empty()) { - g_cached_saylinks.emplace_back(saylinks[0]); - return saylinks[0]; - } + // return if found from the database + if (!saylinks.empty()) { + g_cached_saylinks.emplace_back(saylinks[0]); + return saylinks[0]; + } - // if not found in database - save - auto new_saylink = SaylinkRepository::NewEntity(); - new_saylink.phrase = saylink_text; + // if not found in database - save + auto new_saylink = SaylinkRepository::NewEntity(); + new_saylink.phrase = saylink_text; - // persist to database - auto link = SaylinkRepository::InsertOne(database, new_saylink); - if (link.id > 0) { - g_cached_saylinks.emplace_back(link); - return link; + // persist to database + auto link = SaylinkRepository::InsertOne(*s_database, new_saylink); + if (link.id > 0) { + g_cached_saylinks.emplace_back(link); + return link; + } } return {}; diff --git a/common/say_link.h b/common/say_link.h index 6977fb88c..8ccce7cbf 100644 --- a/common/say_link.h +++ b/common/say_link.h @@ -24,6 +24,7 @@ #include struct LootItem; +class SharedDatabase; namespace EQ { @@ -39,8 +40,8 @@ namespace EQ SayLinkItemInst }; - extern bool DegenerateLinkBody(SayLinkBody_Struct& say_Link_body_struct, const std::string& say_link_body); - extern bool GenerateLinkBody(std::string& say_link_body, const SayLinkBody_Struct& say_link_body_struct); + bool DeserializeLinkBody(SayLinkBody_Struct& say_Link_body_struct, const std::string& say_link_body); + bool SerializeLinkBody(std::string& say_link_body, const SayLinkBody_Struct& say_link_body_struct); } /*saylink*/ @@ -93,13 +94,13 @@ namespace EQ void SetTaskUse() { m_TaskUse = true; } const std::string& GenerateLink(); - bool LinkError() { return m_Error; } + bool LinkError() const { return m_Error; } - const std::string& Link() { return m_Link; } // contains full string format: '\x12' '' '' '\x12' - const std::string& LinkBody() { return m_LinkBody; } // contains string format: '' - const std::string& LinkText() { return m_LinkText; } // contains string format: '' + const std::string& Link() const { return m_Link; } // contains full string format: '\x12' '' '' '\x12' static std::string GenerateQuestSaylink(const std::string& saylink_text, bool silent, const std::string& link_name); + static std::string GetSaylinkPhrase(uint32_t saylink_id); + static void SetDatabase(SharedDatabase* db); void Reset(); diff --git a/tob/opcodes.md b/tob/opcodes.md index 38ad94325..b3870eb48 100644 --- a/tob/opcodes.md +++ b/tob/opcodes.md @@ -298,13 +298,13 @@ Below is a status list for the 450 opcodes we currently use on the server for th | `OP_InvokeNameChangeImmediate` | 🔴 Not-Set | | | | `OP_InvokeNameChangeLazy` | 🔴 Not-Set | | | | `OP_ItemAdvancedLoreText` | 🟠 Missing | | | -| `OP_ItemLinkClick` | 🟠 Missing | | | -| `OP_ItemLinkResponse` | 🟠 Missing | | | +| `OP_ItemLinkClick` | 🟢 Verified | 0x0350. C→S: CChatWindow::WndNotification@0x1403afbf0 (also CTaskWnd, CPetitionQWnd). ParseItemLink→InspectItemSafe 84B; DECODE reads first 52B (item_id+augs+hash); extra 32B uncharted. | | +| `OP_ItemLinkResponse` | 🟢 Verified | 0x7805. C→S: RequestInspectItem@0x140430e80 sends 80B; DECODE strips extra 8B. S→C: sub_1402136D0@0x1402136d0 calls ItemClient::CreateItemClient+ShowItem; ENCODE_FORWARD(OP_ItemPacket) correct. | | | `OP_ItemLinkText` | 🔴 Not-Set | | | | `OP_ItemName` | 🔴 Not-Set | | | | `OP_ItemPacket` | 🟢 Verified | Handler @ `0x1401f0bf3` (inline+CSB). Fixed: added `ItemPacketParcel` to `ServerToTOBItemPacketType` (srv 0x73→TOB 0x74) and corrected parcel `SerializeItem` to use `pms.slot_id`. Note: client discards note field. | | -| `OP_ItemPreview` | 🟠 Missing | | | -| `OP_ItemPreviewRequest` | 🔴 Not-Set | | | +| `OP_ItemPreview` | 🟢 Verified | S→C only (0x3867). Handler @ `0x1401f254d`; passes raw packet bytes as `chngForm&` directly to `CMarketplaceWnd::SetPreviewForm`. Passthrough — `Handle_OP_ItemPreview` hand-rolled bytes ARE the chngForm struct. Replaces RoF2 OP_ItemPreview (0x6B5C). | | +| `OP_ItemPreviewRequest` | 🟢 Verified | Bidirectional (0x2B8A). S→C: ENCODE added — SerializeItem → `ItemClient::CreateItemClient(CUnSerializeBuffer&)` + trailing bool @ `0x1401f2321`. C→S: 3 send sites (marketplace, ObjectPreviewWnd×2). Replaces RoF2 OP_ItemPreviewRequest (0x7F80). | | | `OP_ItemRecastDelay` | 🟢 Verified | Handler @ `0x1401E8638` (inline, Pattern B). Encoder added: InventorySlot_Struct (zeroed, TODO server struct) + recast_delay (+0x0C) + recast_type (+0x10). SetCoreItemRecastTimer fires; per-item timer needs server slot. | | | `OP_ItemVerifyReply` | 🟢 Verified | Handler `0x1401e652c` (inline Pattern B). Encoder added: expands 12→20 bytes; adds unknown0 (+0x0C exit gate) and recast_time (+0x10 timestamp, zeroed). Autobook-scribe (spell==0x407) needs server-side recast_time. | | | `OP_ItemVerifyRequest` | 🟢 Verified | C→S only. TOB sends 16-byte packet (InventorySlot_Struct 12 bytes + target uint32). DECODE added using TOBToServerSlot; tob_structs.h entry added. Matches RoF2 layout with int32 Type instead of int16. | | diff --git a/utils/patches/patch_TOB.conf b/utils/patches/patch_TOB.conf index cd48e294f..ca95b7106 100644 --- a/utils/patches/patch_TOB.conf +++ b/utils/patches/patch_TOB.conf @@ -253,9 +253,10 @@ OP_MoveMultipleItems=0x6bf7 OP_DeleteItem=0x29e6 OP_DeleteCharge=0x4bef OP_ItemPacket=0x0fb6 -OP_ItemLinkResponse=0x0000 -OP_ItemLinkClick=0x0000 -OP_ItemPreview=0x0000 +OP_ItemLinkResponse=0x7805 +OP_ItemLinkClick=0x0350 +OP_ItemPreview=0x3867 +OP_ItemPreviewRequest=0x2b8a OP_NewSpawn=0x053d OP_Track=0x24c2 OP_TrackTarget=0x0941 diff --git a/world/CMakeLists.txt b/world/CMakeLists.txt index bd51ea03e..ef304e2e6 100644 --- a/world/CMakeLists.txt +++ b/world/CMakeLists.txt @@ -71,7 +71,6 @@ set(world_headers queryserv.h shared_task_manager.h shared_task_world_messaging.h - sof_char_create_data.h ucs.h web_interface.h web_interface_eqw.h diff --git a/world/client.cpp b/world/client.cpp index 29b1e990f..0386f8b94 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -49,7 +49,6 @@ #include "world/clientlist.h" #include "world/login_server_list.h" #include "world/login_server.h" -#include "world/sof_char_create_data.h" #include "world/wguild_mgr.h" #include "world/world_config.h" #include "world/worlddb.h" diff --git a/world/sof_char_create_data.h b/world/sof_char_create_data.h deleted file mode 100644 index fb5eceaf9..000000000 --- a/world/sof_char_create_data.h +++ /dev/null @@ -1,47 +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 - -#pragma pack(push) -#pragma pack(1) - -struct RaceClassAllocation { - unsigned int Index; - unsigned int BaseStats[7]; - unsigned int DefaultPointAllocation[7]; -}; - -struct RaceClassCombos { - unsigned int ExpansionRequired; - unsigned int Race; - unsigned int Class; - unsigned int Deity; - unsigned int AllocationIndex; - unsigned int Zone; -}; - -/*struct SoFCCData { - unsigned char Unknown; - unsigned int RaceClassStatEntryCount; - SoFCCRaceClassData RCData[109]; - unsigned int Unknown2; - SoFCCStartZoneData StartZoneData[641]; -}; -*/ - -#pragma pack(pop) diff --git a/world/worlddb.cpp b/world/worlddb.cpp index 8fd102357..3801b128e 100644 --- a/world/worlddb.cpp +++ b/world/worlddb.cpp @@ -29,7 +29,6 @@ #include "common/rulesys.h" #include "common/strings.h" #include "common/zone_store.h" -#include "world/sof_char_create_data.h" #include #include diff --git a/zone/gm_commands/giveitem.cpp b/zone/gm_commands/giveitem.cpp index addd11275..8742e0677 100755 --- a/zone/gm_commands/giveitem.cpp +++ b/zone/gm_commands/giveitem.cpp @@ -39,7 +39,7 @@ void command_giveitem(Client *c, const Seperator *sep) if (link_open != link_close && (cmd_msg.length() - link_open) > EQ::constants::SAY_LINK_BODY_SIZE) { EQ::SayLinkBody_Struct link_body; - EQ::saylink::DegenerateLinkBody( + EQ::saylink::DeserializeLinkBody( link_body, cmd_msg.substr(link_open + 1, EQ::constants::SAY_LINK_BODY_SIZE) ); diff --git a/zone/gm_commands/summonitem.cpp b/zone/gm_commands/summonitem.cpp index 355a1e392..960f5ff4e 100755 --- a/zone/gm_commands/summonitem.cpp +++ b/zone/gm_commands/summonitem.cpp @@ -34,7 +34,7 @@ void command_summonitem(Client *c, const Seperator *sep) if (link_open != link_close && (cmd_msg.length() - link_open) > EQ::constants::SAY_LINK_BODY_SIZE) { EQ::SayLinkBody_Struct link_body; - EQ::saylink::DegenerateLinkBody(link_body, cmd_msg.substr(link_open + 1, EQ::constants::SAY_LINK_BODY_SIZE)); + EQ::saylink::DeserializeLinkBody(link_body, cmd_msg.substr(link_open + 1, EQ::constants::SAY_LINK_BODY_SIZE)); item_id = link_body.item_id; augment_one = link_body.augment_1; augment_two = link_body.augment_2; diff --git a/zone/main.cpp b/zone/main.cpp index e3ebb1959..e714ed661 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -396,6 +396,7 @@ int main(int argc, char **argv) ZoneEventScheduler::Instance()->SetDatabase(&database)->LoadScheduledEvents(); + EQ::SayLinkEngine::SetDatabase(&database); EQ::SayLinkEngine::LoadCachedSaylinks(); if (RuleB(Bots, Enabled) && database.DoesTableExist("bot_command_settings")) { diff --git a/zone/spells.cpp b/zone/spells.cpp index 0a6f2aff8..868800d24 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -336,7 +336,7 @@ bool Mob::DoCastSpell(int32 spell_id, uint16 target_id, CastingSlot slot, StopCasting(); char spell_link[Links::MAX_LINK_SIZE]; - Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id); + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id, GetSpellName(spell_id)); if (IsClient()) ClientPatch::SendMessageString(CastToClient(), Chat::SpellFailure, fizzle_msg, spell_link); @@ -1302,7 +1302,7 @@ void Mob::InterruptSpell(uint16 message, uint16 color, int32 spellid) { // the interrupt message char spell_link[Links::MAX_LINK_SIZE]; - Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid); + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid, GetSpellName(spellid)); ClientPatch::InterruptSpell(CastToClient(), message, GetID(), spell_link); SendSpellBarEnable(spellid); } @@ -1330,7 +1330,7 @@ void Mob::InterruptSpell(uint16 message, uint16 color, int32 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); + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spellid, GetSpellName(spellid)); ClientPatch::InterruptSpellOther(this, message_other, GetID(), GetCleanName(), spell_link); } @@ -7035,7 +7035,7 @@ void Mob::DoBardCastingFromItemClick(bool is_casting_bard_song, uint32 cast_time //For spells with cast times. Cancel song cast, stop pulsing and start item cast. if (cast_time != 0) { char spell_link[Links::MAX_LINK_SIZE]; - Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id); + Links::FormatSpellLink(spell_link, Links::MAX_LINK_SIZE, spell_id, GetSpellName(spell_id)); ClientPatch::InterruptSpell(CastToClient(), SONG_ENDS, GetID(), spell_link); ZeroCastingVars();