mirror of
https://github.com/EQEmu/Server.git
synced 2026-05-17 03:08:26 +00:00
[Items] Overhaul Item Hand-in System (#4593)
* [Items] Overhaul Item Hand-in System * Edge case lua fix * Merge fix * I'm going to be amazed if this works first try * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Add protections against scripts that hand back items themselves * Remove EVENT_ITEM_ScriptStopReturn * test * Update npc_handins.cpp * Add Items:AlwaysReturnHandins * Update spdat.cpp * Bypass update prompt on CI
This commit is contained in:
+114
-135
@@ -320,7 +320,11 @@ void Client::ResetTrade() {
|
||||
}
|
||||
|
||||
void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, std::list<void*>* event_details) {
|
||||
if(tradingWith && tradingWith->IsClient()) {
|
||||
if (!tradingWith) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tradingWith->IsClient()) {
|
||||
Client * other = tradingWith->CastToClient();
|
||||
PlayerLogTrade_Struct * qs_audit = nullptr;
|
||||
bool qs_log = false;
|
||||
@@ -366,7 +370,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
inst->GetItem()->NoDrop != 0 ||
|
||||
CanTradeFVNoDropItem() ||
|
||||
other == this
|
||||
) {
|
||||
) {
|
||||
int16 free_slot = other->GetInv().FindFreeSlotForTradeItem(inst);
|
||||
|
||||
if (free_slot != INVALID_INDEX) {
|
||||
@@ -481,8 +485,12 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
LogTrading("Transferring partial stack [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName());
|
||||
|
||||
if (other->PutItemInInventory(partial_slot, *partial_inst, true)) {
|
||||
LogTrading("Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot",
|
||||
inst->GetItem()->Name, inst->GetItem()->ID, (old_charges - inst->GetCharges()));
|
||||
LogTrading(
|
||||
"Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot",
|
||||
inst->GetItem()->Name,
|
||||
inst->GetItem()->ID,
|
||||
(old_charges - inst->GetCharges())
|
||||
);
|
||||
inst->TransferOwnership(database, other->CharacterID());
|
||||
if (qs_log) {
|
||||
auto detail = new PlayerLogTradeItemsEntry_Struct;
|
||||
@@ -509,7 +517,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
}
|
||||
else {
|
||||
LogTrading("Transfer of partial stack [{}] ([{}]) to [{}] failed, returning [{}] charges to trade slot",
|
||||
inst->GetItem()->Name, inst->GetItem()->ID, other->GetName(), (old_charges - inst->GetCharges()));
|
||||
inst->GetItem()->Name, inst->GetItem()->ID, other->GetName(), (old_charges - inst->GetCharges()));
|
||||
|
||||
inst->SetCharges(old_charges);
|
||||
partial_inst->SetCharges(partial_charges);
|
||||
@@ -666,8 +674,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
//Do not reset the trade here, done by the caller.
|
||||
}
|
||||
}
|
||||
else if(tradingWith && tradingWith->IsNPC()) {
|
||||
NPCHandinEventLog(trade, tradingWith->CastToNPC());
|
||||
else if(tradingWith->IsNPC()) {
|
||||
|
||||
QSPlayerLogHandin_Struct* qs_audit = nullptr;
|
||||
bool qs_log = false;
|
||||
@@ -744,7 +751,6 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
|
||||
bool quest_npc = false;
|
||||
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
|
||||
// This is a quest NPC
|
||||
quest_npc = true;
|
||||
}
|
||||
|
||||
@@ -760,34 +766,14 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
|
||||
if (RuleB(TaskSystem, EnableTaskSystem)) {
|
||||
if (UpdateTasksOnDeliver(items, *trade, tradingWith->CastToNPC())) {
|
||||
if (!tradingWith->IsMoving())
|
||||
if (!tradingWith->IsMoving()) {
|
||||
tradingWith->FaceTarget(this);
|
||||
|
||||
EVENT_ITEM_ScriptStopReturn();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of quest or non-quest NPC - No in combat trade completion
|
||||
// is allowed.
|
||||
if (tradingWith->CheckAggro(this))
|
||||
{
|
||||
for (EQ::ItemInstance* inst : items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
}
|
||||
|
||||
items.clear();
|
||||
}
|
||||
// Only enforce trade rules if the NPC doesn't have an EVENT_TRADE
|
||||
// subroutine. That overrides all.
|
||||
else if (!quest_npc)
|
||||
{
|
||||
for (EQ::ItemInstance* inst : items) {
|
||||
|
||||
if (!quest_npc) {
|
||||
for (auto &inst: items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
@@ -801,128 +787,121 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
}
|
||||
}
|
||||
|
||||
const EQ::ItemData* item = inst->GetItem();
|
||||
const bool is_pet = _CLIENTPET(tradingWith) && tradingWith->GetPetType()<=petOther;
|
||||
const bool is_quest_npc = tradingWith->CastToNPC()->IsQuestNPC();
|
||||
const bool restrict_quest_items_to_quest_npc = RuleB(NPC, ReturnQuestItemsFromNonQuestNPCs);
|
||||
const bool pets_can_take_quest_items = RuleB(Pets, CanTakeQuestItems);
|
||||
const bool is_pet_and_can_have_nodrop_items = (RuleB(Pets, CanTakeNoDrop) && is_pet);
|
||||
const bool is_pet_and_can_have_quest_items = (pets_can_take_quest_items && is_pet);
|
||||
// if it was not a NO DROP or Attuned item (or if a GM is trading), let the NPC have it
|
||||
if (GetGM() ||
|
||||
(!restrict_quest_items_to_quest_npc || (is_quest_npc && item->IsQuestItem()) || !item->IsQuestItem()) && // If rule is enabled, return any quest items given to non-quest NPCs
|
||||
(((item->NoDrop != 0 && !inst->IsAttuned()) || is_pet_and_can_have_nodrop_items) &&
|
||||
((!item->IsQuestItem() || is_pet_and_can_have_quest_items || !is_pet)))) {
|
||||
auto with = tradingWith->CastToNPC();
|
||||
const EQ::ItemData *item = inst->GetItem();
|
||||
|
||||
if (with->IsPetOwnerClient() && with->CanPetTakeItem(inst)) {
|
||||
// pets need to look inside bags and try to equip items found there
|
||||
if (item->IsClassBag() && item->BagSlots > 0) {
|
||||
for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) {
|
||||
// if an item inside the bag can't be given to the pet, keep the bag
|
||||
bool keep_bag = false;
|
||||
int item_count = 0;
|
||||
for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) {
|
||||
const EQ::ItemInstance *baginst = inst->GetItem(bslot);
|
||||
if (baginst) {
|
||||
const EQ::ItemData *bagitem = baginst->GetItem();
|
||||
if (bagitem && (GetGM() ||
|
||||
(!restrict_quest_items_to_quest_npc ||
|
||||
(is_quest_npc && bagitem->IsQuestItem()) || !bagitem->IsQuestItem()) &&
|
||||
// If rule is enabled, return any quest items given to non-quest NPCs (inside bags)
|
||||
(bagitem->NoDrop != 0 && !baginst->IsAttuned()) &&
|
||||
((is_pet && (!bagitem->IsQuestItem() || pets_can_take_quest_items) ||
|
||||
!is_pet)))) {
|
||||
|
||||
if (GetGM()) {
|
||||
const std::string& item_link = database.CreateItemLink(bagitem->ID);
|
||||
Message(
|
||||
Chat::White,
|
||||
fmt::format(
|
||||
"Your GM flag allows you to give {} to {}.",
|
||||
item_link,
|
||||
GetTargetDescription(tradingWith)
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(baginst->GetCharges());
|
||||
|
||||
tradingWith->CastToNPC()->AddLootDrop(
|
||||
bagitem,
|
||||
lde,
|
||||
true
|
||||
);
|
||||
// Return quest items being traded to non-quest NPC when the rule is true
|
||||
} else if (restrict_quest_items_to_quest_npc && (!is_quest_npc && bagitem->IsQuestItem())) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*baginst, true);
|
||||
Message(Chat::Red, "You can only trade quest items to quest NPCs.");
|
||||
// Return quest items being traded to player pet when not allowed
|
||||
} else if (is_pet && bagitem->IsQuestItem() && !pets_can_take_quest_items) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*baginst, true);
|
||||
Message(Chat::Red, "You cannot trade quest items with your pet.");
|
||||
} else if (RuleB(NPC, ReturnNonQuestNoDropItems)) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*baginst, true);
|
||||
}
|
||||
if (baginst && baginst->GetItem() && with->CanPetTakeItem(baginst)) {
|
||||
// add item to pet's inventory
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(baginst->GetCharges());
|
||||
with->AddLootDrop(baginst->GetItem(), lde, true);
|
||||
inst->DeleteItem(bslot);
|
||||
item_count++;
|
||||
}
|
||||
else {
|
||||
keep_bag = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// add item to pet's inventory
|
||||
if (!keep_bag || item_count == 0) {
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(inst->GetCharges());
|
||||
with->AddLootDrop(item, lde, true);
|
||||
inst = nullptr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// add item to pet's inventory
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(inst->GetCharges());
|
||||
|
||||
tradingWith->CastToNPC()->AddLootDrop(
|
||||
item,
|
||||
lde,
|
||||
true
|
||||
);
|
||||
with->AddLootDrop(item, lde, true);
|
||||
inst = nullptr;
|
||||
}
|
||||
}
|
||||
// Return quest items being traded to non-quest NPC when the rule is true
|
||||
else if (restrict_quest_items_to_quest_npc && (!is_quest_npc && item->IsQuestItem())) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
Message(Chat::Red, "You can only trade quest items to quest NPCs.");
|
||||
}
|
||||
// Return quest items being traded to player pet when not allowed
|
||||
else if (is_pet && item->IsQuestItem()) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
Message(Chat::Red, "You cannot trade quest items with your pet.");
|
||||
}
|
||||
// Return NO DROP and Attuned items being handed into a non-quest NPC if the rule is true
|
||||
else if (RuleB(NPC, ReturnNonQuestNoDropItems)) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char temp1[100] = { 0 };
|
||||
char temp2[100] = { 0 };
|
||||
snprintf(temp1, 100, "copper.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->cp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
snprintf(temp1, 100, "silver.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->sp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
snprintf(temp1, 100, "gold.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->gp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
snprintf(temp1, 100, "platinum.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->pp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
std::string currencies[] = {"copper", "silver", "gold", "platinum"};
|
||||
int32 amounts[] = {trade->cp, trade->sp, trade->gp, trade->pp};
|
||||
|
||||
if(tradingWith->GetAppearance() != eaDead) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
parse->AddVar(
|
||||
fmt::format("{}.{}", currencies[i], tradingWith->GetNPCTypeID()),
|
||||
fmt::format("{}", amounts[i])
|
||||
);
|
||||
}
|
||||
|
||||
if (tradingWith->GetAppearance() != eaDead) {
|
||||
tradingWith->FaceTarget(this);
|
||||
}
|
||||
|
||||
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
|
||||
std::vector<std::any> item_list(items.begin(), items.end());
|
||||
parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list);
|
||||
// we cast to any to pass through the quest event system
|
||||
std::vector<std::any> item_list(items.begin(), items.end());
|
||||
for (EQ::ItemInstance *inst: items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
item_list.emplace_back(inst);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 4; ++i) {
|
||||
if(insts[i]) {
|
||||
safe_delete(insts[i]);
|
||||
m_external_handin_money_returned = {};
|
||||
m_external_handin_items_returned = {};
|
||||
bool has_aggro = tradingWith->CheckAggro(this);
|
||||
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE) && !has_aggro) {
|
||||
parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list);
|
||||
LogNpcHandinDetail("EVENT_TRADE triggered for NPC [{}]", tradingWith->GetNPCTypeID());
|
||||
}
|
||||
|
||||
auto handin_npc = tradingWith->CastToNPC();
|
||||
|
||||
// this is a catch-all return for items that weren't consumed by the EVENT_TRADE subroutine
|
||||
// it's possible we have a quest NPC that doesn't have an EVENT_TRADE subroutine
|
||||
// we can't double fire the ReturnHandinItems() event, so we need to check if it's already been processed from EVENT_TRADE
|
||||
if (!handin_npc->HasProcessedHandinReturn()) {
|
||||
if (!handin_npc->HandinStarted()) {
|
||||
LogNpcHandinDetail("EVENT_TRADE did not process handin, calling ReturnHandinItems() for NPC [{}]", tradingWith->GetNPCTypeID());
|
||||
std::map<std::string, uint32> handin = {
|
||||
{"copper", trade->cp},
|
||||
{"silver", trade->sp},
|
||||
{"gold", trade->gp},
|
||||
{"platinum", trade->pp}
|
||||
};
|
||||
|
||||
for (EQ::ItemInstance *inst: items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string item_id = fmt::format("{}", inst->GetItem()->ID);
|
||||
handin[item_id] += inst->GetCharges();
|
||||
}
|
||||
|
||||
handin_npc->CheckHandin(this, handin, {}, items);
|
||||
}
|
||||
|
||||
if (RuleB(Items, AlwaysReturnHandins)) {
|
||||
handin_npc->ReturnHandinItems(this);
|
||||
LogNpcHandin("ReturnHandinItems called for NPC [{}]", handin_npc->GetNPCTypeID());
|
||||
}
|
||||
}
|
||||
|
||||
handin_npc->ResetHandin();
|
||||
|
||||
for (auto &inst: insts) {
|
||||
if (inst) {
|
||||
safe_delete(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user