eqemu-server/zone/loot.cpp
2025-03-03 00:16:51 -06:00

944 lines
22 KiB
C++

#include "../common/global_define.h"
#include "../common/data_verification.h"
#include "../common/loot.h"
#include "client.h"
#include "entity.h"
#include "mob.h"
#include "npc.h"
#include "zonedb.h"
#include "global_loot_manager.h"
#include "../common/repositories/criteria/content_filter_criteria.h"
#include "../common/repositories/global_loot_repository.h"
#include "quest_parser_collection.h"
#ifdef _WINDOWS
#define snprintf _snprintf
#endif
void NPC::AddLootTable(uint32 loottable_id, bool is_global)
{
// check if it's a GM spawn
if (!npctype_id) {
return;
}
if (!is_global) {
m_loot_copper = 0;
m_loot_silver = 0;
m_loot_gold = 0;
m_loot_platinum = 0;
}
zone->LoadLootTable(loottable_id);
const auto *l = zone->GetLootTable(loottable_id);
if (!l) {
return;
}
LogLootDetail(
"Attempting to load loot [{}] loottable [{}] ({}) is_global [{}]",
GetCleanName(),
loottable_id,
l->name,
is_global
);
auto content_flags = ContentFlags{
.min_expansion = l->min_expansion,
.max_expansion = l->max_expansion,
.content_flags = l->content_flags,
.content_flags_disabled = l->content_flags_disabled
};
if (!content_service.DoesPassContentFiltering(content_flags)) {
return;
}
uint32 min_cash = l->mincash;
uint32 max_cash = l->maxcash;
if (min_cash > max_cash) {
const uint32 t = min_cash;
min_cash = max_cash;
max_cash = t;
}
uint32 cash = 0;
if (!is_global) {
if (max_cash > 0 && l->avgcoin > 0 && EQ::ValueWithin(l->avgcoin, min_cash, max_cash)) {
const float upper_chance = static_cast<float>(l->avgcoin - min_cash) /
static_cast<float>(max_cash - min_cash);
const float avg_cash_roll = static_cast<float>(zone->random.Real(0.0, 1.0));
if (avg_cash_roll < upper_chance) {
cash = zone->random.Int(l->avgcoin, max_cash);
}
else {
cash = zone->random.Int(min_cash, l->avgcoin);
}
}
else {
cash = zone->random.Int(min_cash, max_cash);
}
}
if (cash != 0) {
m_loot_platinum = cash / 1000;
cash -= m_loot_platinum * 1000;
m_loot_gold = cash / 100;
cash -= m_loot_gold * 100;
m_loot_silver = cash / 10;
cash -= m_loot_silver * 10;
m_loot_copper = cash;
}
const uint32 global_loot_multiplier = RuleI(Zone, GlobalLootMultiplier);
for (auto &lte: zone->GetLootTableEntries(loottable_id)) {
for (uint32 k = 1; k <= (lte.multiplier * global_loot_multiplier); k++) {
const uint8 drop_limit = lte.droplimit;
const uint8 minimum_drop = lte.mindrop;
const float probability = lte.probability;
float drop_chance = 0.0f;
if (EQ::ValueWithin(probability, 0.0f, 100.0f)) {
drop_chance = static_cast<float>(zone->random.Real(0.0, 100.0));
}
if (probability != 0.0 && (probability == 100.0 || drop_chance <= probability)) {
AddLootDropTable(lte.lootdrop_id, drop_limit, minimum_drop);
}
}
}
LogLootDetail(
"Loaded [{}] Loot Table [{}] is_global [{}]",
GetCleanName(),
loottable_id,
is_global
);
}
void NPC::AddLootDropTable(uint32 lootdrop_id, uint8 drop_limit, uint8 min_drop)
{
const auto l = zone->GetLootdrop(lootdrop_id);
const auto le = zone->GetLootdropEntries(lootdrop_id);
if (l.id == 0 || le.empty()) {
return;
}
// if this lootdrop is droplimit=0 and mindrop 0, scan list once and return
if (drop_limit == 0 && min_drop == 0) {
for (const auto &e: le) {
for (int j = 0; j < e.multiplier; ++j) {
if (zone->random.Real(0.0, 100.0) <= e.chance && MeetsLootDropLevelRequirements(e, true)) {
const EQ::ItemData *database_item = database.GetItem(e.item_id);
AddLootDrop(database_item, e);
LogLootDetail(
"---- NPC (Rolled) [{}] Lootdrop [{}] Item [{}] ({}) Chance [{}] Multiplier [{}]",
GetCleanName(),
lootdrop_id,
database_item->Name,
e.item_id,
e.chance,
e.multiplier
);
}
}
}
return;
}
if (le.size() > 100 && drop_limit == 0) {
drop_limit = 10;
}
if (drop_limit < min_drop) {
drop_limit = min_drop;
}
float roll_t = 0.0f;
float no_loot_prob = 1.0f;
bool roll_table_chance_bypass = false;
bool active_item_list = false;
for (const auto &e: le) {
const EQ::ItemData *db_item = database.GetItem(e.item_id);
if (db_item && MeetsLootDropLevelRequirements(e)) {
roll_t += e.chance;
if (e.chance >= 100) {
roll_table_chance_bypass = true;
}
else {
no_loot_prob *= (100 - e.chance) / 100.0f;
}
active_item_list = true;
}
}
if (!active_item_list) {
return;
}
// This will pick one item per iteration until mindrop.
// Don't let the compare against chance fool you.
// The roll isn't 0-100, its 0-total and it picks the item, we're just
// looping to find the lucky item, descremening otherwise. This is ok,
// items with chance 60 are 6 times more likely than items chance 10.
int drops = 0;
// translate above for loop using l and le
for (int i = 0; i < drop_limit; ++i) {
if (drops < min_drop || roll_table_chance_bypass || (float) zone->random.Real(0.0, 1.0) >= no_loot_prob) {
float roll = (float) zone->random.Real(0.0, roll_t);
for (const auto &e: le) {
const auto *db_item = database.GetItem(e.item_id);
if (db_item) {
// if it doesn't meet the requirements do nothing
if (!MeetsLootDropLevelRequirements(e)) {
continue;
}
if (roll < e.chance) {
AddLootDrop(db_item, e);
drops++;
uint8 charges = e.multiplier;
charges = EQ::ClampLower(charges, static_cast<uint8>(1));
for (int k = 1; k < charges; ++k) {
float c_roll = static_cast<float>(zone->random.Real(0.0, 100.0));
if (c_roll <= e.chance) {
AddLootDrop(db_item, e);
}
}
break;
}
else {
roll -= e.chance;
}
}
}
}
}
UpdateEquipmentLight();
}
bool NPC::MeetsLootDropLevelRequirements(LootdropEntriesRepository::LootdropEntries loot_drop, bool verbose)
{
if (loot_drop.npc_min_level > 0 && GetLevel() < loot_drop.npc_min_level) {
if (verbose) {
LogLootDetail(
"NPC [{}] does not meet loot_drop level requirements (min_level) level [{}] current [{}] for item [{}]",
GetCleanName(),
loot_drop.npc_min_level,
GetLevel(),
database.CreateItemLink(loot_drop.item_id)
);
}
return false;
}
if (loot_drop.npc_max_level > 0 && GetLevel() > loot_drop.npc_max_level) {
if (verbose) {
LogLootDetail(
"NPC [{}] does not meet loot_drop level requirements (max_level) level [{}] current [{}] for item [{}]",
GetCleanName(),
loot_drop.npc_max_level,
GetLevel(),
database.CreateItemLink(loot_drop.item_id)
);
}
return false;
}
return true;
}
//if itemlist is null, just send wear changes
void NPC::AddLootDrop(
const EQ::ItemData *item2,
LootdropEntriesRepository::LootdropEntries loot_drop,
bool wear_change,
uint32 augment_one,
uint32 augment_two,
uint32 augment_three,
uint32 augment_four,
uint32 augment_five,
uint32 augment_six
)
{
if (m_resumed_from_zone_suspend) {
LogZoneState("NPC [{}] is resuming from zone suspend, skipping AddItem", GetCleanName());
return;
}
if (!item2) {
return;
}
auto item = new LootItem;
if (LogSys.log_settings[Logs::Loot].is_category_enabled == 1) {
EQ::SayLinkEngine linker;
linker.SetLinkType(EQ::saylink::SayLinkItemData);
linker.SetItemData(item2);
LogLoot(
"NPC [{}] Item ({}) [{}] charges [{}] chance [{}] trivial min/max [{}/{}] npc min/max [{}/{}]",
GetName(),
item2->ID,
linker.GenerateLink(),
loot_drop.item_charges,
loot_drop.chance,
loot_drop.trivial_min_level,
loot_drop.trivial_max_level,
loot_drop.npc_min_level,
loot_drop.npc_max_level
);
}
EQApplicationPacket *outapp = nullptr;
WearChange_Struct *p_wear_change_struct = nullptr;
if (wear_change) {
outapp = new EQApplicationPacket(OP_WearChange, sizeof(WearChange_Struct));
p_wear_change_struct = (WearChange_Struct *) outapp->pBuffer;
p_wear_change_struct->spawn_id = GetID();
p_wear_change_struct->material = 0;
}
item->item_id = item2->ID;
item->charges = loot_drop.item_charges;
item->aug_1 = augment_one;
item->aug_2 = augment_two;
item->aug_3 = augment_three;
item->aug_4 = augment_four;
item->aug_5 = augment_five;
item->aug_6 = augment_six;
item->attuned = false;
item->trivial_min_level = loot_drop.trivial_min_level;
item->trivial_max_level = loot_drop.trivial_max_level;
item->equip_slot = EQ::invslot::SLOT_INVALID;
// unsure if required to equip, YOLO for now
if (item2->ItemType == EQ::item::ItemTypeBow) {
SetBowEquipped(true);
}
if (item2->ItemType == EQ::item::ItemTypeArrow) {
SetArrowEquipped(true);
}
bool found = false; // track if we found an empty slot we fit into
int found_slot = INVALID_INDEX; // for multi-slot items
auto *inst = database.CreateItem(
item2->ID,
loot_drop.item_charges,
augment_one,
augment_two,
augment_three,
augment_four,
augment_five,
augment_six
);
if (!inst) {
return;
}
if (loot_drop.equip_item > 0) {
uint8 equipment_slot = UINT8_MAX;
const EQ::ItemData *compitem = nullptr;
// Equip rules are as follows:
// If the item has the NoPet flag set it will not be equipped.
// An empty slot takes priority. The first empty one that an item can
// fit into will be the one picked for the item.
// AC is the primary choice for which item gets picked for a slot.
// If AC is identical HP is considered next.
// If an item can fit into multiple slots we'll pick the last one where
// it is an improvement.
if (!item2->NoPet) {
for (int i = EQ::invslot::EQUIPMENT_BEGIN; !found && i <= EQ::invslot::EQUIPMENT_END; i++) {
const uint32 slots = (1 << i);
if (item2->Slots & slots) {
if (equipment[i]) {
compitem = database.GetItem(equipment[i]);
if (!compitem) {
continue;
}
if (item2->AC > compitem->AC || (item2->AC == compitem->AC && item2->HP > compitem->HP)) {
// item would be an upgrade
// check if we're multi-slot, if yes then we have to keep
// looking in case any of the other slots we can fit into are empty.
if (item2->Slots != slots) {
found_slot = i;
}
else {
// Unequip old item
auto *old_item = GetItem(i);
if (!old_item) {
continue;
}
old_item->equip_slot = EQ::invslot::SLOT_INVALID;
equipment[i] = item2->ID;
found_slot = i;
found = true;
}
}
}
else {
equipment[i] = item2->ID;
found_slot = i;
found = true;
}
}
}
}
// Possible slot was found but not selected. Pick it now.
if (!found && found_slot >= 0) {
equipment[found_slot] = item2->ID;
found = true;
}
uint32 equipment_material;
if (
item2->Material <= 0 ||
(
item2->Slots & (
(1 << EQ::invslot::slotPrimary) |
(1 << EQ::invslot::slotSecondary)
)
)
) {
equipment_material = Strings::ToUnsignedInt(&item2->IDFile[2]);
}
else {
equipment_material = item2->Material;
}
if (found_slot == EQ::invslot::slotPrimary) {
equipment_slot = EQ::textures::weaponPrimary;
if (item2->Damage > 0) {
SendAddPlayerState(PlayerState::PrimaryWeaponEquipped);
if (!RuleB(Combat, ClassicNPCBackstab)) {
SetFacestab(true);
}
}
if (item2->IsType2HWeapon()) {
SetTwoHanderEquipped(true);
}
}
else if (
found_slot == EQ::invslot::slotSecondary &&
(
GetOwner() ||
(CanThisClassDualWield() && zone->random.Roll(NPC_DW_CHANCE)) ||
item2->Damage == 0
) &&
(
item2->IsType1HWeapon() ||
item2->ItemType == EQ::item::ItemTypeShield ||
item2->ItemType == EQ::item::ItemTypeLight
)
) {
equipment_slot = EQ::textures::weaponSecondary;
if (item2->Damage > 0) {
SendAddPlayerState(PlayerState::SecondaryWeaponEquipped);
}
}
else if (found_slot == EQ::invslot::slotHead) {
equipment_slot = EQ::textures::armorHead;
}
else if (found_slot == EQ::invslot::slotChest) {
equipment_slot = EQ::textures::armorChest;
}
else if (found_slot == EQ::invslot::slotArms) {
equipment_slot = EQ::textures::armorArms;
}
else if (EQ::ValueWithin(found_slot, EQ::invslot::slotWrist1, EQ::invslot::slotWrist2)) {
equipment_slot = EQ::textures::armorWrist;
}
else if (found_slot == EQ::invslot::slotHands) {
equipment_slot = EQ::textures::armorHands;
}
else if (found_slot == EQ::invslot::slotLegs) {
equipment_slot = EQ::textures::armorLegs;
}
else if (found_slot == EQ::invslot::slotFeet) {
equipment_slot = EQ::textures::armorFeet;
}
if (equipment_slot != UINT8_MAX) {
if (wear_change) {
p_wear_change_struct->wear_slot_id = equipment_slot;
p_wear_change_struct->material = equipment_material;
}
}
if (found) {
item->equip_slot = found_slot;
}
}
if (found_slot != INVALID_INDEX) {
GetInv().PutItem(found_slot, *inst);
}
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_LOOT_ADDED)) {
std::vector<std::any> args = {inst};
parse->EventNPC(EVENT_LOOT_ADDED, this, nullptr, "", 0, &args);
}
item->lootdrop_id = loot_drop.lootdrop_id;
m_loot_items.push_back(item);
if (found) {
CalcBonuses();
}
if (IsRecordLootStats()) {
m_rolled_items.emplace_back(item->item_id);
}
if (wear_change && outapp) {
entity_list.QueueClients(this, outapp);
safe_delete(outapp);
}
UpdateEquipmentLight();
if (UpdateActiveLight()) {
SendAppearancePacket(AppearanceType::Light, GetActiveLightType());
}
safe_delete(inst);
}
void NPC::AddItem(const EQ::ItemData *item, uint16 charges, bool equip_item)
{
auto l = LootdropEntriesRepository::NewNpcEntity();
l.equip_item = static_cast<uint8>(equip_item ? 1 : 0);
l.item_charges = charges;
AddLootDrop(item, l, true);
}
void NPC::AddItem(
uint32 item_id,
uint16 charges,
bool equip_item,
uint32 augment_one,
uint32 augment_two,
uint32 augment_three,
uint32 augment_four,
uint32 augment_five,
uint32 augment_six
)
{
const auto *item = database.GetItem(item_id);
if (!item) {
return;
}
auto l = LootdropEntriesRepository::NewNpcEntity();
l.equip_item = static_cast<uint8>(equip_item ? 1 : 0);
l.item_charges = charges;
AddLootDrop(
item,
l,
true,
augment_one,
augment_two,
augment_three,
augment_four,
augment_five,
augment_six
);
}
void NPC::AddLootTable()
{
AddLootTable(m_loottable_id);
}
void NPC::CheckGlobalLootTables()
{
const auto &l = zone->GetGlobalLootTables(this);
for (const auto &e: l) {
AddLootTable(e, true);
}
}
void ZoneDatabase::LoadGlobalLoot()
{
const auto &l = GlobalLootRepository::GetWhere(
*this,
fmt::format(
"`enabled` = 1 {}",
ContentFilterCriteria::apply()
)
);
if (l.empty()) {
return;
}
LogInfo(
"Loaded [{}] Global Loot Entr{}.",
Strings::Commify(l.size()),
l.size() != 1 ? "ies" : "y"
);
const std::string &zone_id = std::to_string(zone->GetZoneID());
for (const auto &e: l) {
if (!e.zone.empty()) {
const auto &zones = Strings::Split(e.zone, "|");
if (!Strings::Contains(zones, zone_id)) {
continue;
}
}
GlobalLootEntry gle(e.id, e.loottable_id, e.description);
if (e.min_level) {
gle.AddRule(GlobalLoot::RuleTypes::LevelMin, e.min_level);
}
if (e.max_level) {
gle.AddRule(GlobalLoot::RuleTypes::LevelMax, e.max_level);
}
if (e.rare) {
gle.AddRule(GlobalLoot::RuleTypes::Rare, e.rare);
}
if (e.raid) {
gle.AddRule(GlobalLoot::RuleTypes::Raid, e.raid);
}
if (!e.race.empty()) {
const auto &races = Strings::Split(e.race, "|");
for (const auto &r: races) {
gle.AddRule(GlobalLoot::RuleTypes::Race, Strings::ToInt(r));
}
}
if (!e.class_.empty()) {
const auto &classes = Strings::Split(e.class_, "|");
for (const auto &c: classes) {
gle.AddRule(GlobalLoot::RuleTypes::Class, Strings::ToInt(c));
}
}
if (!e.bodytype.empty()) {
const auto &bodytypes = Strings::Split(e.bodytype, "|");
for (const auto &b: bodytypes) {
gle.AddRule(GlobalLoot::RuleTypes::BodyType, Strings::ToInt(b));
}
}
if (e.hot_zone) {
gle.AddRule(GlobalLoot::RuleTypes::HotZone, e.hot_zone);
}
zone->AddGlobalLootEntry(gle);
}
}
LootItem *NPC::GetItem(int slot_id)
{
LootItems::iterator cur, end;
cur = m_loot_items.begin();
end = m_loot_items.end();
for (; cur != end; ++cur) {
LootItem *item = *cur;
if (item && item->equip_slot == slot_id) {
return item;
}
}
return (nullptr);
}
void NPC::RemoveItem(uint32 item_id, uint16 quantity, uint16 slot)
{
LootItems::iterator cur, end;
cur = m_loot_items.begin();
end = m_loot_items.end();
for (; cur != end; ++cur) {
LootItem *item = *cur;
if (item->item_id == item_id && slot <= 0 && quantity <= 0) {
m_loot_items.erase(cur);
safe_delete(item);
UpdateEquipmentLight();
if (UpdateActiveLight()) { SendAppearancePacket(AppearanceType::Light, GetActiveLightType()); }
return;
}
else if (item->item_id == item_id && item->equip_slot == slot && quantity >= 1) {
if (item->charges <= quantity) {
m_loot_items.erase(cur);
UpdateEquipmentLight();
if (UpdateActiveLight()) {
SendAppearancePacket(AppearanceType::Light, GetActiveLightType());
}
safe_delete(item);
}
else {
item->charges -= quantity;
}
return;
}
}
}
void NPC::CheckTrivialMinMaxLevelDrop(Mob *killer)
{
if (killer == nullptr || !killer->IsClient()) {
return;
}
uint16 killer_level = killer->GetLevel();
uint8 material;
auto cur = m_loot_items.begin();
while (cur != m_loot_items.end()) {
if (!(*cur)) {
return;
}
uint16 trivial_min_level = (*cur)->trivial_min_level;
uint16 trivial_max_level = (*cur)->trivial_max_level;
bool fits_trivial_criteria = (
(trivial_min_level > 0 && killer_level < trivial_min_level) ||
(trivial_max_level > 0 && killer_level > trivial_max_level)
);
if (fits_trivial_criteria) {
material = EQ::InventoryProfile::CalcMaterialFromSlot((*cur)->equip_slot);
if (material != EQ::textures::materialInvalid) {
SendWearChange(material);
}
cur = m_loot_items.erase(cur);
continue;
}
++cur;
}
UpdateEquipmentLight();
if (UpdateActiveLight()) {
SendAppearancePacket(AppearanceType::Light, GetActiveLightType());
}
}
void NPC::ClearLootItems()
{
LootItems::iterator cur, end;
cur = m_loot_items.begin();
end = m_loot_items.end();
for (; cur != end; ++cur) {
LootItem *item = *cur;
safe_delete(item);
}
m_loot_items.clear();
UpdateEquipmentLight();
if (UpdateActiveLight()) {
SendAppearancePacket(AppearanceType::Light, GetActiveLightType());
}
}
void NPC::QueryLoot(Client *to, bool is_pet_query)
{
if (!m_loot_items.empty()) {
if (!is_pet_query) {
to->Message(
Chat::White,
fmt::format(
"Loot | {} ({}) ID: {} Loottable ID: {}",
GetName(),
GetID(),
GetNPCTypeID(),
GetLoottableID()
).c_str()
);
}
int item_count = 0;
for (auto current_item: m_loot_items) {
int item_number = (item_count + 1);
if (!current_item) {
LogError("ItemList error, null item.");
continue;
}
if (!current_item->item_id || !database.GetItem(current_item->item_id)) {
LogError("Database error, invalid item.");
continue;
}
EQ::SayLinkEngine linker;
linker.SetLinkType(EQ::saylink::SayLinkLootItem);
linker.SetLootData(current_item);
to->Message(
Chat::White,
fmt::format(
"Item {} | {} ({}){}",
item_number,
linker.GenerateLink().c_str(),
current_item->item_id,
(
current_item->charges > 1 ?
fmt::format(
" Amount: {}",
current_item->charges
) :
""
)
).c_str()
);
item_count++;
}
}
if (!is_pet_query) {
if (m_loot_platinum || m_loot_gold || m_loot_silver || m_loot_copper) {
to->Message(
Chat::White,
fmt::format(
"Money | {}",
Strings::Money(
m_loot_platinum,
m_loot_gold,
m_loot_silver,
m_loot_copper
)
).c_str()
);
}
}
}
bool NPC::HasItem(uint32 item_id)
{
if (!database.GetItem(item_id)) {
return false;
}
for (auto loot_item: m_loot_items) {
if (!loot_item) {
LogError("NPC::HasItem() - ItemList error, null item");
continue;
}
if (!loot_item->item_id || !database.GetItem(loot_item->item_id)) {
LogError("NPC::HasItem() - Database error, invalid item");
continue;
}
if (loot_item->item_id == item_id) {
return true;
}
}
return false;
}
uint32 NPC::CountItem(uint32 item_id)
{
uint32 item_count = 0;
if (!database.GetItem(item_id)) {
return item_count;
}
for (auto loot_item: m_loot_items) {
if (!loot_item) {
LogError("NPC::CountItem() - ItemList error, null item");
continue;
}
if (!loot_item->item_id || !database.GetItem(loot_item->item_id)) {
LogError("NPC::CountItem() - Database error, invalid item");
continue;
}
if (loot_item->item_id == item_id) {
item_count += loot_item->charges > 0 ? loot_item->charges : 1;
}
}
return item_count;
}
uint32 NPC::GetLootItemIDBySlot(uint16 loot_slot)
{
for (auto loot_item: m_loot_items) {
if (loot_item->lootslot == loot_slot) {
return loot_item->item_id;
}
}
return 0;
}
uint16 NPC::GetFirstLootSlotByItemID(uint32 item_id)
{
for (auto loot_item: m_loot_items) {
if (loot_item->item_id == item_id) {
return loot_item->lootslot;
}
}
return 0;
}
void NPC::AddLootCash(
uint32 in_copper,
uint32 in_silver,
uint32 in_gold,
uint32 in_platinum
)
{
m_loot_copper = in_copper >= 0 ? in_copper : 0;
m_loot_silver = in_silver >= 0 ? in_silver : 0;
m_loot_gold = in_gold >= 0 ? in_gold : 0;
m_loot_platinum = in_platinum >= 0 ? in_platinum : 0;
}
void NPC::RemoveLootCash()
{
m_loot_copper = 0;
m_loot_silver = 0;
m_loot_gold = 0;
m_loot_platinum = 0;
}