mirror of
https://github.com/EQEmu/Server.git
synced 2026-07-09 20:17:16 +00:00
Integrate client-based say links for TOB+ clients (#5105)
This commit is contained in:
@@ -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)
|
||||
|
||||
+49
-7
@@ -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<int>(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<int>(keyword.length()), keyword.data());
|
||||
} else {
|
||||
snprintf(Buffer, BufferSize, "%.*s:%.*s",
|
||||
static_cast<int>(keyword.length()), keyword.data(),
|
||||
static_cast<int>(text.length()), text.data());
|
||||
}
|
||||
}
|
||||
|
||||
+10
-7
@@ -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 = {});
|
||||
|
||||
}
|
||||
|
||||
+195
-91
@@ -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<char*>(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<char*>(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<std::string> 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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+67
-28
@@ -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 <algorithm>
|
||||
// static database pointer (must be SharedDatabase since this module is in common)
|
||||
static SharedDatabase* s_database = nullptr;
|
||||
|
||||
// static bucket global
|
||||
std::vector<SaylinkRepository::Saylink> 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 {};
|
||||
|
||||
+7
-6
@@ -24,6 +24,7 @@
|
||||
#include <string>
|
||||
|
||||
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' '<LinkBody>' '<LinkText>' '\x12'
|
||||
const std::string& LinkBody() { return m_LinkBody; } // contains string format: '<LinkBody>'
|
||||
const std::string& LinkText() { return m_LinkText; } // contains string format: '<LinkText>'
|
||||
const std::string& Link() const { return m_Link; } // contains full string format: '\x12' '<LinkBody>' '<LinkText>' '\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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user