/* 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.h"
#include "common/events/player_event_logs.h"
#include "common/evolving_items.h"
#include "zone/string_ids.h"
#include "zone/worldserver.h"
extern WorldServer worldserver;
extern QueryServ* QServ;
const std::string SUB_TYPE_DELIMITER = ".";
void Client::DoEvolveItemToggle(const EQApplicationPacket *app)
{
const auto in = reinterpret_cast(app->pBuffer);
auto item = CharacterEvolvingItemsRepository::FindOne(database, in->unique_id);
LogEvolveItemDetail(
"Character ID [{}] requested to set evolve item with unique id [{}] to status [{}]",
CharacterID(),
in->unique_id,
in->activated
);
if (!item.id) {
LogEvolveItemDetail(
"Character ID [{}] toggle evolve item unique id [{}] failed", CharacterID(), in->unique_id);
return;
}
item.activated = in->activated;
const auto inst = GetInv().GetItem(GetInv().HasItem(item.item_id));
inst->SetEvolveActivated(item.activated ? true : false);
CharacterEvolvingItemsRepository::ReplaceOne(database, item);
SendEvolvingPacket(EvolvingItems::Actions::UPDATE_ITEMS, item);
}
void Client::SendEvolvingPacket(const int8 action, const CharacterEvolvingItemsRepository::CharacterEvolvingItems &item)
{
auto out = std::make_unique(OP_EvolveItem, sizeof(EvolveItemToggle));
const auto data = reinterpret_cast(out->pBuffer);
LogEvolveItemDetail(
"Character ID [{}] requested info for evolving item with unique id [{}] status [{}] "
"percentage [{}]",
CharacterID(),
item.id,
item.activated,
item.progression
);
data->action = action;
data->unique_id = item.id;
data->percentage = item.progression;
data->activated = item.activated;
QueuePacket(out.get());
LogEvolveItem(
"Sent evolve item with unique id [{}] status [{}] percentage [{}] to Character ID "
"[{}]",
data->unique_id,
data->activated,
data->percentage,
CharacterID()
);
}
void Client::ProcessEvolvingItem(const uint64 exp, const Mob *mob)
{
std::vector queue{};
for (auto &[key, inst]: GetInv().GetWorn()) {
LogEvolveItemDetail(
"CharacterID [{}] found equipped item ID [{}]", CharacterID(), inst->GetID());
if (!inst->IsEvolving() || !inst->GetEvolveActivated()) {
LogEvolveItemDetail(
"CharacterID [{}], item ID [{}] not an evolving item.", CharacterID(), inst->GetID()
);
continue;
}
if (inst->GetTimers().contains("evolve") && !inst->GetTimers().at("evolve").Check(false)) {
LogEvolveItemDetail(
"CharacterID [{}], item ID [{}] timer not yet expired. [{}] secs remaining.",
CharacterID(),
inst->GetID(),
inst->GetTimers().at("evolve").GetRemainingTime() / 1000);
continue;
}
if (!EvolvingItemsManager::Instance()->GetEvolvingItemsCache().contains(inst->GetID())) {
LogEvolveItem(
"Character ID {} has an evolving item that is not found in the db. Please check your "
"items_evolving_details table for item id {}",
CharacterID(),
inst->GetID()
);
continue;
}
auto const type = EvolvingItemsManager::Instance()->GetEvolvingItemsCache().at(inst->GetID()).type;
auto const sub_type = EvolvingItemsManager::Instance()->GetEvolvingItemsCache().at(inst->GetID()).sub_type;
LogEvolveItemDetail(
"CharacterID [{}] item id [{}] type {} sub_type {} is Evolving. Continue processing...",
CharacterID(),
inst->GetID(),
type,
sub_type
);
auto sub_types = Strings::Split(sub_type, SUB_TYPE_DELIMITER);
auto has_sub_type = [&](uint32_t type) {
return Strings::Contains(sub_types, std::to_string(type));
};
switch (type) {
case EvolvingItems::Types::AMOUNT_OF_EXP: {
LogEvolveItemDetail("Type [{}] Processing sub_type", type);
// Determine the evolve amount based on sub_type conditions
int evolve_amount = 0;
if (has_sub_type(EvolvingItems::SubTypes::ALL_EXP) ||
(has_sub_type(EvolvingItems::SubTypes::GROUP_EXP) && IsGrouped())) {
evolve_amount = exp * RuleR(EvolvingItems, PercentOfGroupExperience) / 100;
}
else if (has_sub_type(EvolvingItems::SubTypes::ALL_EXP) ||
(has_sub_type(EvolvingItems::SubTypes::RAID_EXP) && IsRaidGrouped())) {
evolve_amount = exp * RuleR(EvolvingItems, PercentOfRaidExperience) / 100;
}
else if (has_sub_type(EvolvingItems::SubTypes::ALL_EXP) ||
has_sub_type(EvolvingItems::SubTypes::SOLO_EXP)) {
evolve_amount = exp * RuleR(EvolvingItems, PercentOfSoloExperience) / 100;
}
inst->SetEvolveAddToCurrentAmount(evolve_amount);
inst->CalculateEvolveProgression();
auto e = CharacterEvolvingItemsRepository::SetCurrentAmountAndProgression(
database, inst->GetEvolveUniqueID(), inst->GetEvolveCurrentAmount(), inst->GetEvolveProgression()
);
if (!e.id) {
break;
}
SendEvolvingPacket(EvolvingItems::Actions::UPDATE_ITEMS, e);
LogEvolveItem(
"Processing Complete for item id [{1}] Type 1 Amount of EXP - SubType [{0}] - "
"Assigned [{2}] of exp to [{1}]",
sub_type,
inst->GetID(),
exp * 0.001
);
if (inst->GetEvolveProgression() >= 100) {
queue.push_back(inst);
}
break;
}
case EvolvingItems::Types::SPECIFIC_MOB_RACE: {
LogEvolveItemDetail("Type [{}] Processing sub type", type);
if (mob && has_sub_type(mob->GetRace())) {
LogEvolveItemDetail("Sub_Type [{}] Processing Item", sub_type);
inst->SetEvolveAddToCurrentAmount(1);
inst->CalculateEvolveProgression();
auto e = CharacterEvolvingItemsRepository::SetCurrentAmountAndProgression(
database,
inst->GetEvolveUniqueID(),
inst->GetEvolveCurrentAmount(),
inst->GetEvolveProgression()
);
if (!e.id) {
break;
}
SendEvolvingPacket(EvolvingItems::Actions::UPDATE_ITEMS, e);
LogEvolveItem(
"Processing Complete for item id [{1}] Type 3 Specific Mob Race - SubType "
"[{0}] "
"- Increased count by 1 for [{1}]",
sub_type,
inst->GetID()
);
}
if (inst->GetEvolveProgression() >= 100) {
queue.push_back(inst);
}
break;
}
case EvolvingItems::Types::SPECIFIC_ZONE_ID: {
LogEvolveItemDetail("Type [{}] Processing sub type", type);
if (mob && has_sub_type(mob->GetZoneID())) {
LogEvolveItemDetail("Sub_Type [{}] Processing Item", sub_type);
inst->SetEvolveAddToCurrentAmount(1);
inst->CalculateEvolveProgression();
auto e = CharacterEvolvingItemsRepository::SetCurrentAmountAndProgression(
database,
inst->GetEvolveUniqueID(),
inst->GetEvolveCurrentAmount(),
inst->GetEvolveProgression()
);
if (!e.id) {
break;
}
SendEvolvingPacket(EvolvingItems::Actions::UPDATE_ITEMS, e);
LogEvolveItem(
"Processing Complete for item id [{1}] Type 4 Specific Zone ID - SubType "
"[{0}] "
"- Increased count by 1 for [{1}]",
sub_type,
inst->GetID()
);
}
if (inst->GetEvolveProgression() >= 100) {
queue.push_back(inst);
}
break;
}
case EvolvingItems::Types::NUMBER_OF_KILLS: {
LogEvolveItemDetail("Type [{}] Processing sub type", type);
if (mob) {
if (mob->GetLevel() >= Strings::ToUnsignedInt(sub_types.front()) ||
Strings::ToUnsignedInt(sub_types.front()) == 0
) {
LogEvolveItemDetail("Sub_Type [{}] Processing Item", sub_type);
inst->SetEvolveAddToCurrentAmount(1);
inst->CalculateEvolveProgression();
auto e = CharacterEvolvingItemsRepository::SetCurrentAmountAndProgression(
database,
inst->GetEvolveUniqueID(),
inst->GetEvolveCurrentAmount(),
inst->GetEvolveProgression()
);
if (!e.id) {
break;
}
SendEvolvingPacket(EvolvingItems::Actions::UPDATE_ITEMS, e);
LogEvolveItem(
"Processing Complete for item id [{1}] Type 4 Specific Zone ID - SubType "
"[{0}] "
"- Increased count by 1 for [{1}]",
sub_type,
inst->GetID()
);
}
}
if (inst->GetEvolveProgression() >= 100) {
queue.push_back(inst);
}
break;
}
default: {
}
}
}
if (!queue.empty()) {
for (auto const &i: queue) {
DoEvolveCheckProgression(*i);
}
}
}
void Client::DoEvolveItemDisplayFinalResult(const EQApplicationPacket *app)
{
const auto in = reinterpret_cast(app->pBuffer);
const uint32 item_id = static_cast(in->unique_id & 0xFFFFFFFF);
if (item_id == 0) {
LogEvolveItem("Error - Item ID of final evolve item is blank.");
return;
}
std::unique_ptr const inst(database.CreateItem(item_id));
if (!inst) {
return;
}
LogEvolveItemDetail(
"Character ID [{}] requested to view final evolve item id [{}] for evolve item id [{}]",
CharacterID(),
item_id,
EvolvingItemsManager::Instance()->GetFirstItemInLoreGroupByItemID(item_id)
);
inst->SetEvolveProgression(100);
LogEvolveItemDetail(
"Sending final result for item id [{}] to Character ID [{}]", item_id, CharacterID()
);
SendItemPacket(0, inst.get(), ItemPacketViewLink);
}
bool Client::DoEvolveCheckProgression(EQ::ItemInstance &inst)
{
if (!inst) {
return false;
}
if (inst.GetEvolveProgression() < 100 || inst.GetEvolveLvl() == inst.GetMaxEvolveLvl()) {
return false;
}
const auto new_item_id = EvolvingItemsManager::Instance()->GetNextEvolveItemID(inst);
if (!new_item_id) {
return false;
}
std::unique_ptr const new_inst(database.CreateItem(new_item_id));
if (!new_inst) {
return false;
}
if (RuleB(EvolvingItems, EnableParcelMerchants) &&
!RuleB(EvolvingItems, DestroyAugmentsOnEvolve) &&
inst.IsAugmented()
) {
auto const augs = inst.GetAugmentIDs();
std::vector parcels;
int32 next_slot = FindNextFreeParcelSlotUsingMemory();
for (auto const &item_id: augs) {
if (!item_id) {
continue;
}
if (next_slot == INVALID_INDEX) {
break;
}
CharacterParcelsRepository::CharacterParcels p{};
p.char_id = CharacterID();
p.from_name = "Evolving Item Sub-System";
p.note = fmt::format(
"System automatically removed from {} which recently evolved.",
inst.GetItem()->Name
);
p.slot_id = next_slot;
p.sent_date = time(nullptr);
p.item_id = item_id;
p.quantity = 1;
if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
PlayerEvent::ParcelSend e{};
e.from_player_name = p.from_name;
e.to_player_name = GetCleanName();
e.item_id = p.item_id;
e.quantity = 1;
e.sent_date = p.sent_date;
RecordPlayerEventLog(PlayerEvent::PARCEL_SEND, e);
}
parcels.push_back(p);
m_parcels.emplace(p.slot_id, p);
next_slot = FindNextFreeParcelSlotUsingMemory();
}
CharacterParcelsRepository::InsertMany(database, parcels);
SendParcelStatus();
SendParcelIconStatus();
}
CheckItemDiscoverability(new_inst->GetID());
PlayerEvent::EvolveItem e{};
RemoveItemBySerialNumber(inst.GetSerialNumber());
EvolvingItemsManager::Instance()->LoadPlayerEvent(inst, e);
e.status = "Evolved Item due to obtaining progression - Old Evolve Item removed from inventory.";
RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e);
PushItemOnCursor(*new_inst, true);
EvolvingItemsManager::Instance()->LoadPlayerEvent(*new_inst, e);
e.status = "Evolved Item due to obtaining progression - New Evolve Item placed in inventory.";
RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e);
MessageString(Chat::Yellow, EVOLVE_ITEM_EVOLVED, inst.GetItem()->Name);
LogEvolveItem(
"Evolved item id [{}] into item id [{}] for Character ID [{}]",
inst.GetID(),
new_inst->GetID(),
CharacterID());
return true;
}
void Client::SendEvolveXPTransferWindow()
{
auto out = std::make_unique(OP_EvolveItem, sizeof(EvolveItemToggle));
const auto data = reinterpret_cast(out->pBuffer);
data->action = 1;
QueuePacket(out.get());
}
void Client::SendEvolveXPWindowDetails(const EQApplicationPacket *app)
{
const auto in = reinterpret_cast(app->pBuffer);
const auto item_1_slot =
GetInv().HasEvolvingItem(in->item1_unique_id, 1, invWherePersonal | invWhereWorn | invWhereCursor);
const auto item_2_slot =
GetInv().HasEvolvingItem(in->item2_unique_id, 1, invWherePersonal | invWhereWorn | invWhereCursor);
if (item_1_slot == INVALID_INDEX || item_2_slot == INVALID_INDEX) {
return;
}
const auto inst_from = GetInv().GetItem(item_1_slot);
const auto inst_to = GetInv().GetItem(item_2_slot);
if (!inst_from || !inst_to) {
return;
}
const auto results = EvolvingItemsManager::Instance()->DetermineTransferResults(*inst_from, *inst_to);
if (!results.item_from_id || !results.item_to_id) {
SendEvolveTransferResults(*inst_from, *inst_to, *inst_from, *inst_to, 0, 0);
return;
}
std::unique_ptr const inst_from_new(database.CreateItem(results.item_from_id));
std::unique_ptr const inst_to_new(database.CreateItem(results.item_to_id));
if (!inst_from_new || !inst_to_new) {
SendEvolveTransferResults(*inst_from, *inst_to, *inst_from, *inst_to, 0, 0);
return;
}
inst_from_new->SetEvolveCurrentAmount(results.item_from_current_amount);
inst_from_new->CalculateEvolveProgression();
inst_to_new->SetEvolveCurrentAmount(results.item_to_current_amount);
inst_to_new->CalculateEvolveProgression();
SendEvolveTransferResults(
*inst_from, *inst_to, *inst_from_new, *inst_to_new, results.compatibility, results.max_transfer_level);
}
void Client::DoEvolveTransferXP(const EQApplicationPacket *app)
{
const auto in = reinterpret_cast(app->pBuffer);
const auto item_1_slot =
GetInv().HasEvolvingItem(in->item1_unique_id, 1, invWherePersonal | invWhereWorn | invWhereCursor);
const auto item_2_slot =
GetInv().HasEvolvingItem(in->item2_unique_id, 1, invWherePersonal | invWhereWorn | invWhereCursor);
if (item_1_slot == INVALID_INDEX || item_2_slot == INVALID_INDEX) {
return;
}
const auto inst_from = GetInv().GetItem(item_1_slot);
const auto inst_to = GetInv().GetItem(item_2_slot);
if (!inst_from || !inst_to) {
Message(Chat::Red, "Transfer Failed. Incompatible Items.");
LogEvolveItem("Transfer Failed for Character ID [{}]", CharacterID());
return;
}
const auto results = EvolvingItemsManager::Instance()->DetermineTransferResults(*inst_from, *inst_to);
if (!results.item_from_id || !results.item_to_id) {
Message(Chat::Red, "Transfer Failed. Incompatible Items.");
LogEvolveItem("Transfer Failed for Character ID [{}]", CharacterID());
return;
}
std::unique_ptr const inst_from_new(database.CreateItem(results.item_from_id));
std::unique_ptr const inst_to_new(database.CreateItem(results.item_to_id));
if (!inst_from_new || !inst_to_new) {
Message(Chat::Red, "Transfer Failed. Incompatible Items.");
LogEvolveItem("Transfer Failed for Character ID [{}]", CharacterID());
return;
}
inst_from_new->SetEvolveCurrentAmount(results.item_from_current_amount);
inst_from_new->CalculateEvolveProgression();
inst_to_new->SetEvolveCurrentAmount(results.item_to_current_amount);
inst_to_new->CalculateEvolveProgression();
PlayerEvent::EvolveItem e{};
RemoveItemBySerialNumber(inst_from->GetSerialNumber());
EvolvingItemsManager::Instance()->LoadPlayerEvent(*inst_from, e);
e.status = "Transfer XP - Original FROM Evolve Item removed from inventory.";
RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e);
PushItemOnCursor(*inst_from_new, true);
EvolvingItemsManager::Instance()->LoadPlayerEvent(*inst_from_new, e);
e.status = "Transfer XP - Updated FROM item placed in inventory.";
RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e);
RemoveItemBySerialNumber(inst_to->GetSerialNumber());
EvolvingItemsManager::Instance()->LoadPlayerEvent(*inst_to, e);
e.status = "Transfer XP - Original TO Evolve Item removed from inventory.";
RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e);
PushItemOnCursor(*inst_to_new, true);
EvolvingItemsManager::Instance()->LoadPlayerEvent(*inst_to_new, e);
e.status = "Transfer XP - Updated TO Evolve item placed in inventory.";
RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e);
LogEvolveItem(
"Evolve Transfer XP resulted in evolved item id [{}] into item id [{}] for Character ID "
"[{}]",
inst_to->GetID(),
inst_to_new->GetID(),
CharacterID()
);
}
void Client::SendEvolveTransferResults(
const EQ::ItemInstance &inst_from,
const EQ::ItemInstance &inst_to,
const EQ::ItemInstance &inst_from_new,
const EQ::ItemInstance &inst_to_new,
const uint32 compatibility,
const uint32 max_transfer_level)
{
std::stringstream ss;
cereal::BinaryOutputArchive ar(ss);
EvolveXPWindowSend e{};
e.action = EvolvingItems::Actions::TRANSFER_WINDOW_DETAILS;
e.compatibility = compatibility;
e.item1_unique_id = inst_from.GetEvolveUniqueID();
e.item2_unique_id = inst_to.GetEvolveUniqueID();
e.max_transfer_level = max_transfer_level;
e.item1_present = 1;
e.item2_present = 1;
e.serialize_item_1 = inst_from_new.Serialize(0);
e.serialize_item_2 = inst_to_new.Serialize(0);
{
ar(e);
}
uint32 packet_size = sizeof(EvolveItemMessaging) + ss.str().length();
std::unique_ptr out(new EQApplicationPacket(OP_EvolveItem, packet_size));
const auto data = reinterpret_cast(out->pBuffer);
data->action = EvolvingItems::Actions::TRANSFER_WINDOW_DETAILS;
memcpy(data->serialized_data, ss.str().data(), ss.str().length());
QueuePacket(out.get());
ss.str("");
ss.clear();
}