From 6fb919a16f19f7a25346cbafc575d2011e223e4e Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 3 Feb 2025 16:51:09 -0600 Subject: [PATCH] [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 --- common/database/database_update.cpp | 2 +- common/database/database_update_manifest.cpp | 13 +- common/eqemu_logsys.cpp | 2 + common/eqemu_logsys.h | 4 +- common/eqemu_logsys_log_aliases.h | 10 + .../events/player_event_discord_formatter.cpp | 24 + common/item_data.cpp | 28 + common/item_data.h | 1 + common/item_instance.cpp | 12 + common/item_instance.h | 1 + .../base/base_npc_types_repository.h | 20 +- common/ruletypes.h | 6 +- common/spdat.cpp | 64 +- common/spdat.h | 6 +- common/version.h | 2 +- utils/scripts/build/linux-build.sh | 31 +- utils/scripts/build/source-db-build.sh | 90 +++ zone/attack.cpp | 11 + zone/cli/npc_handins.cpp | 514 +++++++++++++++ zone/client.cpp | 585 +++++++++-------- zone/client.h | 21 +- zone/exp.cpp | 3 - zone/inventory.cpp | 13 +- zone/lua_iteminst.cpp | 7 + zone/lua_iteminst.h | 1 + zone/lua_npc.cpp | 99 +++ zone/lua_npc.h | 10 + zone/lua_parser_events.cpp | 9 + zone/main.cpp | 51 +- zone/mob.cpp | 31 +- zone/mob.h | 1 + zone/npc.cpp | 618 ++++++++++++++++++ zone/npc.h | 49 ++ zone/perl_npc.cpp | 83 +++ zone/questmgr.cpp | 45 +- zone/trading.cpp | 249 ++++--- zone/zone_cli.cpp | 7 + zone/zone_cli.h | 2 + zone/zonedb.cpp | 1 + zone/zonedump.h | 1 + 40 files changed, 2254 insertions(+), 473 deletions(-) create mode 100644 utils/scripts/build/source-db-build.sh create mode 100644 zone/cli/npc_handins.cpp diff --git a/common/database/database_update.cpp b/common/database/database_update.cpp index 30036fdd7..1ee07139c 100644 --- a/common/database/database_update.cpp +++ b/common/database/database_update.cpp @@ -192,7 +192,7 @@ bool DatabaseUpdate::UpdateManifest( LogInfo("{}", Strings::Repeat("-", BREAK_LENGTH)); } - if (force_interactive) { + if (force_interactive && !std::getenv("FORCE_INTERACTIVE")) { LogInfo("{}", Strings::Repeat("-", BREAK_LENGTH)); LogInfo("Some migrations require user input. Running interactively"); LogInfo("This is usually due to a major change that could cause data loss"); diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index a279ce90b..ae899a869 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -6491,8 +6491,19 @@ ALTER TABLE `merchantlist_temp` MODIFY COLUMN `slot` int UNSIGNED NOT NULL DEFAULT 0 AFTER `npcid`; )", .content_schema_update = false + }, + ManifestEntry{ + .version = 9300, + .description = "2024_10_15_npc_types_multiquest_enabled.sql", + .check = "SHOW COLUMNS FROM `npc_types` LIKE 'multiquest_enabled'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `npc_types` +ADD COLUMN `multiquest_enabled` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `is_parcel_merchant`; +)", + .content_schema_update = true } - // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/eqemu_logsys.cpp b/common/eqemu_logsys.cpp index 18ef3efcc..52ef1f18f 100644 --- a/common/eqemu_logsys.cpp +++ b/common/eqemu_logsys.cpp @@ -105,6 +105,8 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults() log_settings[Logs::QuestErrors].log_to_console = static_cast(Logs::General); log_settings[Logs::EqTime].log_to_console = static_cast(Logs::General); log_settings[Logs::EqTime].log_to_gmsay = static_cast(Logs::General); + log_settings[Logs::NpcHandin].log_to_console = static_cast(Logs::General); + log_settings[Logs::NpcHandin].log_to_gmsay = static_cast(Logs::General); /** * RFC 5424 diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index b03050a7b..6df671dba 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -148,6 +148,7 @@ namespace Logs { BotSettings, BotSpellChecks, BotSpellTypeChecks, + NpcHandin, MaxCategoryID /* Don't Remove this */ }; @@ -254,7 +255,8 @@ namespace Logs { "KSM", // Kernel Samepage Merging "Bot Settings", "Bot Spell Checks", - "Bot Spell Type Checks" + "Bot Spell Type Checks", + "NpcHandin" }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 331d22a6c..eaf66f8cc 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -904,6 +904,16 @@ OutF(LogSys, Logs::Detail, Logs::BotSpellTypeChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogNpcHandin(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::NpcHandin))\ + OutF(LogSys, Logs::General, Logs::NpcHandin, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogNpcHandinDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::NpcHandin))\ + OutF(LogSys, Logs::Detail, Logs::NpcHandin, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.IsLogEnabled(debug_level, log_category))\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/common/events/player_event_discord_formatter.cpp b/common/events/player_event_discord_formatter.cpp index 578ec672b..17f3704f5 100644 --- a/common/events/player_event_discord_formatter.cpp +++ b/common/events/player_event_discord_formatter.cpp @@ -714,6 +714,18 @@ std::string PlayerEventDiscordFormatter::FormatNPCHandinEvent( h.charges > 1 ? fmt::format(" Charges: {}", h.charges) : "", h.attuned ? " (Attuned)" : "" ); + + for (int i = 0; i < h.augment_ids.size(); i++) { + if (!Strings::EqualFold(h.augment_names[i], "None")) { + const uint8 slot_id = (i + 1); + handin_items_info += fmt::format( + "Augment {}: {} ({})\n", + slot_id, + h.augment_names[i], + h.augment_ids[i] + ); + } + } } } @@ -727,6 +739,18 @@ std::string PlayerEventDiscordFormatter::FormatNPCHandinEvent( r.charges > 1 ? fmt::format(" Charges: {}", r.charges) : "", r.attuned ? " (Attuned)" : "" ); + + for (int i = 0; i < r.augment_ids.size(); i++) { + if (!Strings::EqualFold(r.augment_names[i], "None")) { + const uint8 slot_id = (i + 1); + return_items_info += fmt::format( + "Augment {}: {} ({})\n", + slot_id, + r.augment_names[i], + r.augment_ids[i] + ); + } + } } } diff --git a/common/item_data.cpp b/common/item_data.cpp index 6dcb1f577..c31bad37c 100644 --- a/common/item_data.cpp +++ b/common/item_data.cpp @@ -220,6 +220,34 @@ bool EQ::ItemData::IsType1HWeapon() const return ((ItemType == item::ItemType1HBlunt) || (ItemType == item::ItemType1HSlash) || (ItemType == item::ItemType1HPiercing) || (ItemType == item::ItemTypeMartial)); } +bool EQ::ItemData::IsPetUsable() const +{ + if (ItemClass == item::ItemClassBag) { + return true; + } + + // if it's a misc item and has slots, it's wearable + // this item type is conflated with many other item types + if (ItemClass == item::ItemTypeMisc && Slots != 0) { + return true; + } + + switch (ItemType) { + case item::ItemType1HBlunt: + case item::ItemType1HSlash: + case item::ItemType1HPiercing: + case item::ItemType2HBlunt: + case item::ItemType2HSlash: + case item::ItemTypeMartial: + case item::ItemTypeShield: + case item::ItemTypeArmor: + case item::ItemTypeJewelry: + return true; + default: + return false; + } +} + bool EQ::ItemData::IsType2HWeapon() const { return ((ItemType == item::ItemType2HBlunt) || (ItemType == item::ItemType2HSlash) || (ItemType == item::ItemType2HPiercing)); diff --git a/common/item_data.h b/common/item_data.h index 449a86759..e4cad0e7a 100644 --- a/common/item_data.h +++ b/common/item_data.h @@ -550,6 +550,7 @@ namespace EQ bool IsType1HWeapon() const; bool IsType2HWeapon() const; bool IsTypeShield() const; + bool IsPetUsable() const; bool IsQuestItem() const; static bool CheckLoreConflict(const ItemData* l_item, const ItemData* r_item); diff --git a/common/item_instance.cpp b/common/item_instance.cpp index 1e1e550b5..823990539 100644 --- a/common/item_instance.cpp +++ b/common/item_instance.cpp @@ -1785,6 +1785,18 @@ std::vector EQ::ItemInstance::GetAugmentIDs() const return augments; } +std::vector EQ::ItemInstance::GetAugmentNames() const +{ + std::vector augment_names; + + for (uint8 slot_id = invaug::SOCKET_BEGIN; slot_id <= invaug::SOCKET_END; slot_id++) { + const auto augment = GetAugment(slot_id); + augment_names.push_back(augment ? augment->GetItem()->Name : "None"); + } + + return augment_names; +} + int EQ::ItemInstance::GetItemRegen(bool augments) const { int stat = 0; diff --git a/common/item_instance.h b/common/item_instance.h index 4005a70c2..805ac019d 100644 --- a/common/item_instance.h +++ b/common/item_instance.h @@ -305,6 +305,7 @@ namespace EQ int GetItemSkillsStat(EQ::skills::SkillType skill, bool augments = false) const; uint32 GetItemGuildFavor() const; std::vector GetAugmentIDs() const; + std::vector GetAugmentNames() const; static void AddGUIDToMap(uint64 existing_serial_number); static void ClearGUIDMap(); diff --git a/common/repositories/base/base_npc_types_repository.h b/common/repositories/base/base_npc_types_repository.h index 1eb12660b..e8ed23c8e 100644 --- a/common/repositories/base/base_npc_types_repository.h +++ b/common/repositories/base/base_npc_types_repository.h @@ -123,7 +123,7 @@ public: int8_t legtexture; int8_t feettexture; int8_t light; - int8_t walkspeed; + float walkspeed; int32_t peqid; int8_t unique_; int8_t fixed; @@ -148,6 +148,7 @@ public: int32_t faction_amount; uint8_t keeps_sold_items; uint8_t is_parcel_merchant; + uint8_t multiquest_enabled; }; static std::string PrimaryKey() @@ -287,6 +288,7 @@ public: "faction_amount", "keeps_sold_items", "is_parcel_merchant", + "multiquest_enabled", }; } @@ -422,6 +424,7 @@ public: "faction_amount", "keeps_sold_items", "is_parcel_merchant", + "multiquest_enabled", }; } @@ -591,6 +594,7 @@ public: e.faction_amount = 0; e.keeps_sold_items = 1; e.is_parcel_merchant = 0; + e.multiquest_enabled = 0; return e; } @@ -731,7 +735,7 @@ public: e.legtexture = row[101] ? static_cast(atoi(row[101])) : 0; e.feettexture = row[102] ? static_cast(atoi(row[102])) : 0; e.light = row[103] ? static_cast(atoi(row[103])) : 0; - e.walkspeed = row[104] ? static_cast(atoi(row[104])) : 0; + e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0; e.peqid = row[105] ? static_cast(atoi(row[105])) : 0; e.unique_ = row[106] ? static_cast(atoi(row[106])) : 0; e.fixed = row[107] ? static_cast(atoi(row[107])) : 0; @@ -756,6 +760,7 @@ public: e.faction_amount = row[126] ? static_cast(atoi(row[126])) : 0; e.keeps_sold_items = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 1; e.is_parcel_merchant = row[128] ? static_cast(strtoul(row[128], nullptr, 10)) : 0; + e.multiquest_enabled = row[129] ? static_cast(strtoul(row[129], nullptr, 10)) : 0; return e; } @@ -917,6 +922,7 @@ public: v.push_back(columns[126] + " = " + std::to_string(e.faction_amount)); v.push_back(columns[127] + " = " + std::to_string(e.keeps_sold_items)); v.push_back(columns[128] + " = " + std::to_string(e.is_parcel_merchant)); + v.push_back(columns[129] + " = " + std::to_string(e.multiquest_enabled)); auto results = db.QueryDatabase( fmt::format( @@ -1067,6 +1073,7 @@ public: v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); + v.push_back(std::to_string(e.multiquest_enabled)); auto results = db.QueryDatabase( fmt::format( @@ -1225,6 +1232,7 @@ public: v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); + v.push_back(std::to_string(e.multiquest_enabled)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -1362,7 +1370,7 @@ public: e.legtexture = row[101] ? static_cast(atoi(row[101])) : 0; e.feettexture = row[102] ? static_cast(atoi(row[102])) : 0; e.light = row[103] ? static_cast(atoi(row[103])) : 0; - e.walkspeed = row[104] ? static_cast(atoi(row[104])) : 0; + e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0; e.peqid = row[105] ? static_cast(atoi(row[105])) : 0; e.unique_ = row[106] ? static_cast(atoi(row[106])) : 0; e.fixed = row[107] ? static_cast(atoi(row[107])) : 0; @@ -1387,6 +1395,7 @@ public: e.faction_amount = row[126] ? static_cast(atoi(row[126])) : 0; e.keeps_sold_items = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 1; e.is_parcel_merchant = row[128] ? static_cast(strtoul(row[128], nullptr, 10)) : 0; + e.multiquest_enabled = row[129] ? static_cast(strtoul(row[129], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -1515,7 +1524,7 @@ public: e.legtexture = row[101] ? static_cast(atoi(row[101])) : 0; e.feettexture = row[102] ? static_cast(atoi(row[102])) : 0; e.light = row[103] ? static_cast(atoi(row[103])) : 0; - e.walkspeed = row[104] ? static_cast(atoi(row[104])) : 0; + e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0; e.peqid = row[105] ? static_cast(atoi(row[105])) : 0; e.unique_ = row[106] ? static_cast(atoi(row[106])) : 0; e.fixed = row[107] ? static_cast(atoi(row[107])) : 0; @@ -1540,6 +1549,7 @@ public: e.faction_amount = row[126] ? static_cast(atoi(row[126])) : 0; e.keeps_sold_items = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 1; e.is_parcel_merchant = row[128] ? static_cast(strtoul(row[128], nullptr, 10)) : 0; + e.multiquest_enabled = row[129] ? static_cast(strtoul(row[129], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -1743,6 +1753,7 @@ public: v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); + v.push_back(std::to_string(e.multiquest_enabled)); auto results = db.QueryDatabase( fmt::format( @@ -1894,6 +1905,7 @@ public: v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); + v.push_back(std::to_string(e.multiquest_enabled)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/ruletypes.h b/common/ruletypes.h index b80e16fa1..aed873e38 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -283,10 +283,9 @@ RULE_CATEGORY(Pets) RULE_REAL(Pets, AttackCommandRange, 150, "Range at which a pet will respond to attack commands") RULE_BOOL(Pets, UnTargetableSwarmPet, false, "Setting whether swarm pets should be targetable") RULE_REAL(Pets, PetPowerLevelCap, 10, "Maximum number of levels a player pet can go up with pet power") -RULE_BOOL(Pets, CanTakeNoDrop, false, "Setting whether anyone can give no-drop items to pets") -RULE_BOOL(Pets, CanTakeQuestItems, true, "Setting whether anyone can give quest items to pets") RULE_BOOL(Pets, LivelikeBreakCharmOnInvis, true, "Default: true will break charm on any type of invis (hide/ivu/iva/etc) false will only break if the pet can not see you (ex. you have an undead pet and cast IVU") RULE_BOOL(Pets, ClientPetsUseOwnerNameInLastName, true, "Disable this to keep client pet's last names from being owner_name's pet") +RULE_BOOL(Pets, CanTakeNoDrop, false, "Setting whether anyone can give no-drop items to pets") RULE_INT(Pets, PetTauntRange, 150, "Range at which a pet will taunt targets.") RULE_CATEGORY_END() @@ -666,8 +665,6 @@ RULE_BOOL(NPC, EnableNPCQuestJournal, false, "Setting whether the NPC Quest Jour RULE_INT(NPC, LastFightingDelayMovingMin, 10000, "Minimum time before mob goes home after all aggro loss (milliseconds)") RULE_INT(NPC, LastFightingDelayMovingMax, 20000, "Maximum time before mob goes home after all aggro loss (milliseconds)") RULE_BOOL(NPC, SmartLastFightingDelayMoving, true, "When true, mobs that started going home previously will do so again immediately if still on FD hate list") -RULE_BOOL(NPC, ReturnNonQuestNoDropItems, false, "Returns NO DROP items on NPC that don't have an EVENT_TRADE sub in their script") -RULE_BOOL(NPC, ReturnQuestItemsFromNonQuestNPCs, false, "Returns Quest items traded to NPCs that are not flagged as a Quest NPC") RULE_INT(NPC, StartEnrageValue, 9, " Percentage HP that an NPC will begin to enrage") RULE_BOOL(NPC, LiveLikeEnrage, false, "If set to true then only player controlled pets will enrage") RULE_BOOL(NPC, EnableMeritBasedFaction, false, "If set to true, faction will be given in the same way as experience (solo/group/raid)") @@ -1144,6 +1141,7 @@ RULE_BOOL(Items, DisablePotionBelt, false, "Enable this to disable Potion Belt I RULE_BOOL(Items, DisableSpellFocusEffects, false, "Enable this to disable Spell Focus Effects on Items") RULE_BOOL(Items, SummonItemAllowInvisibleAugments, false, "Enable this to allow augments to be put in invisible augment slots of items in Client::SummonItem") RULE_BOOL(Items, AugmentItemAllowInvisibleAugments, false, "Enable this to allow augments to be put in invisible augment slots by players") +RULE_BOOL(Items, AlwaysReturnHandins, true, "Enable this to always return handins to the player") RULE_CATEGORY_END() RULE_CATEGORY(Parcel) diff --git a/common/spdat.cpp b/common/spdat.cpp index 831c7f35e..2143c9682 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -233,8 +233,8 @@ bool IsDamageOverTimeSpell(uint16 spell_id) for (int i = 0; i < EFFECT_COUNT; i++) { const auto effect_id = spell.effect_id[i]; if ( - spell.base_value[i] < 0 && - effect_id == SE_CurrentHP && + spell.base_value[i] < 0 && + effect_id == SE_CurrentHP && spell.buff_duration > 1 ) { return true; @@ -629,7 +629,7 @@ bool IsPBAENukeSpell(uint16 spell_id) ) { return true; } - + return false; } @@ -670,7 +670,7 @@ bool IsAnyNukeOrStunSpell(uint16 spell_id) { ) { return true; } - + return false; } @@ -2693,7 +2693,7 @@ bool IsAegolismSpell(uint16 spell_id) { bool AegolismStackingIsSymbolSpell(uint16 spell_id) { - + /* This is hardcoded to be specific to the type of HP buffs that are removed if a mob has an Aegolism buff. */ @@ -2793,7 +2793,7 @@ bool IsValidSpellAndLoS(uint32 spell_id, bool has_los) { if (!IsValidSpell(spell_id)) { return false; } - + if (!has_los && IsTargetRequiredForSpell(spell_id)) { return false; } @@ -2949,3 +2949,55 @@ bool IsHateSpell(uint16 spell_id) { ) ); } + + +bool IsDisciplineTome(const EQ::ItemData* item) +{ + if (!item->IsClassCommon() || item->ItemType != EQ::item::ItemTypeSpell) { + return false; + } + + //Need a way to determine the difference between a spell and a tome + //so they cant turn in a spell and get it as a discipline + //this is kinda a hack: + + const std::string item_name = item->Name; + + if ( + !Strings::BeginsWith(item_name, "Tome of ") && + !Strings::BeginsWith(item_name, "Skill: ") + ) { + return false; + } + + //we know for sure none of the int casters get disciplines + uint32 class_bit = 0; + class_bit |= 1 << (Class::Wizard - 1); + class_bit |= 1 << (Class::Enchanter - 1); + class_bit |= 1 << (Class::Magician - 1); + class_bit |= 1 << (Class::Necromancer - 1); + if (item->Classes & class_bit) { + return false; + } + + const auto& spell_id = static_cast(item->Scroll.Effect); + if (!IsValidSpell(spell_id)) { + return false; + } + + if (!IsDiscipline(spell_id)) { + return false; + } + + const auto &spell = spells[spell_id]; + if ( + spell.classes[Class::Wizard - 1] != 255 && + spell.classes[Class::Enchanter - 1] != 255 && + spell.classes[Class::Magician - 1] != 255 && + spell.classes[Class::Necromancer - 1] != 255 + ) { + return false; + } + + return true; +} diff --git a/common/spdat.h b/common/spdat.h index 437910fa4..1d3bd521e 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -20,6 +20,7 @@ #include "classes.h" #include "skills.h" +#include "item_data.h" #define SPELL_UNKNOWN 0xFFFF #define POISON_PROC 0xFFFE @@ -651,8 +652,8 @@ enum SpellTypes : uint32 SpellType_PreCombatBuffSong = (1 << 21) }; -namespace BotSpellTypes -{ +namespace BotSpellTypes +{ constexpr uint16 Nuke = 0; constexpr uint16 RegularHeal = 1; constexpr uint16 Root = 2; @@ -1913,5 +1914,6 @@ bool IsResistanceOnlySpell(uint16 spell_id); bool IsDamageShieldOnlySpell(uint16 spell_id); bool IsDamageShieldAndResistSpell(uint16 spell_id); bool IsHateSpell(uint16 spell_id); +bool IsDisciplineTome(const EQ::ItemData* item); #endif diff --git a/common/version.h b/common/version.h index 6eb990da6..6798d42b3 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9299 +#define CURRENT_BINARY_DATABASE_VERSION 9300 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #endif diff --git a/utils/scripts/build/linux-build.sh b/utils/scripts/build/linux-build.sh index 6bb2ae8bb..6ee379689 100755 --- a/utils/scripts/build/linux-build.sh +++ b/utils/scripts/build/linux-build.sh @@ -5,6 +5,9 @@ set -ex sudo chown eqemu:eqemu /drone/src/ * -R sudo chown eqemu:eqemu /home/eqemu/.ccache/ * -R +chmod +x ./utils/scripts/build/source-db-build.sh +utils/scripts/build/source-db-build.sh & + git submodule init && git submodule update perl utils/scripts/build/tag-version.pl @@ -19,13 +22,39 @@ mkdir -p build && cd build && \ -DCMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING="-O1 -g -Wno-everything" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -G 'Unix Makefiles' \ - .. && make -j$((`nproc`-4)) + .. && make -j$((`nproc`-12)) curl https://raw.githubusercontent.com/Akkadius/eqemu-install-v2/master/eqemu_config.json --output eqemu_config.json ./bin/tests ldd ./bin/zone +echo "Waiting for MariaDB to be ready..." +while ! mysqladmin ping -uroot -peqemu -hlocalhost --silent; do + sleep 1 +done + +echo "# Cloning quests repository" +git -C ./quests pull 2> /dev/null || git clone https://github.com/ProjectEQ/projecteqquests.git quests + +# remove this eventually +cd ./quests && git checkout akkadius/item-handin-overhaul && cd .. + +mkdir maps +mkdir logs + +ln -s ./quests/lua_modules ./lua_modules +ln -s ./quests/plugins ./plugins + +echo "# Running world database updates" +FORCE_INTERACTIVE=1 ./bin/world database:updates --skip-backup --force + +echo "# Running shared_memory" +./bin/shared_memory + +echo "# Running NPC hand-in tests" +./bin/zone tests:npc-handins + # shellcheck disable=SC2164 cd /drone/src/ diff --git a/utils/scripts/build/source-db-build.sh b/utils/scripts/build/source-db-build.sh new file mode 100644 index 000000000..e62b5eb97 --- /dev/null +++ b/utils/scripts/build/source-db-build.sh @@ -0,0 +1,90 @@ +# Variables +ROOT_PASSWORD="eqemu" +MARIADB_CONFIG="/etc/mysql/mariadb.conf.d/50-server.cnf" + +# Update and install MariaDB +echo "Installing MariaDB..." +sudo apt update +sudo apt install -y mariadb-server mariadb-client + +# Ensure MariaDB is stopped before configuration +echo "Stopping MariaDB service..." +sudo systemctl stop mariadb + +# Initialize the data directory (in case it's not already initialized) +echo "Initializing MariaDB data directory..." +sudo mysqld --initialize --user=mysql --datadir=/var/lib/mysql + +# Start MariaDB in safe mode +echo "Starting MariaDB in safe mode..." +sudo mysqld_safe --skip-grant-tables --skip-networking & +sleep 5 + +# Reset root password and configure authentication +echo "Resetting root password and configuring authentication..." +mariadb <GetItem()->NoDrop != 0) { + auto lde = LootdropEntriesRepository::NewNpcEntity(); + lde.equip_item = 0; + lde.item_charges = i.item->GetCharges(); + AddLootDrop(i.item->GetItem(), lde, true); + } + } + } + if (killer_mob && killer_mob->IsOfClientBot() && IsValidSpell(spell) && damage > 0) { char val1[20] = { 0 }; diff --git a/zone/cli/npc_handins.cpp b/zone/cli/npc_handins.cpp new file mode 100644 index 000000000..36be4eb1f --- /dev/null +++ b/zone/cli/npc_handins.cpp @@ -0,0 +1,514 @@ +#include "../../common/http/httplib.h" +#include "../../common/eqemu_logsys.h" +#include "../../common/platform.h" +#include "../zone.h" +#include "../client.h" +#include "../../common/net/eqstream.h" + +extern Zone *zone; + +void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description) +{ + if (cmd[{"-h", "--help"}]) { + return; + } + + uint32 break_length = 50; + int failed_count = 0; + + RegisterExecutablePlatform(EQEmuExePlatform::ExePlatformZoneSidecar); + + LogInfo("{}", Strings::Repeat("-", break_length)); + LogInfo("Booting test zone for NPC handins"); + LogInfo("{}", Strings::Repeat("-", break_length)); + + Zone::Bootup(ZoneID("qrg"), 0, false); + zone->StopShutdownTimer(); + + entity_list.Process(); + entity_list.MobProcess(); + + LogInfo("{}", Strings::Repeat("-", break_length)); + LogInfo("> Done booting test zone"); + LogInfo("{}", Strings::Repeat("-", break_length)); + + Client *c = new Client(); + auto npc_type = content_db.LoadNPCTypesData(754008); + if (npc_type) { + auto npc = new NPC( + npc_type, + nullptr, + glm::vec4(0, 0, 0, 0), + GravityBehavior::Water + ); + + entity_list.AddNPC(npc); + + LogInfo("> Spawned NPC [{}]", npc->GetCleanName()); + LogInfo("> Spawned client [{}]", c->GetCleanName()); + + struct HandinEntry { + std::string item_id = "0"; + uint32 count = 0; + const EQ::ItemInstance *item = nullptr; + bool is_multiquest_item = false; // state + }; + + struct HandinMoney { + uint32 platinum = 0; + uint32 gold = 0; + uint32 silver = 0; + uint32 copper = 0; + }; + + struct Handin { + std::vector items = {}; // items can be removed from this set as successful handins are made + HandinMoney money = {}; // money can be removed from this set as successful handins are made + }; + + struct TestCase { + std::string description = ""; + Handin hand_in; + Handin required; + Handin returned; + bool handin_check_result; + }; + + std::vector test_cases = { + TestCase{ + .description = "Test basic cloth-cap hand-in", + .hand_in = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + }, + .required = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + }, + .returned = {}, + .handin_check_result = true, + }, + TestCase{ + .description = "Test basic cloth-cap hand-in failure", + .hand_in = { + .items = { + HandinEntry{.item_id = "9997", .count = 1}, + }, + }, + .required = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + }, + .returned = { + .items = { + HandinEntry{.item_id = "9997", .count = 1}, + }, + }, + .handin_check_result = false, + }, + TestCase{ + .description = "Test basic cloth-cap hand-in failure from handing in too many", + .hand_in = { + .items = { + HandinEntry{.item_id = "9997", .count = 1}, + HandinEntry{.item_id = "9997", .count = 1}, + }, + }, + .required = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + }, + .returned = { + .items = { + HandinEntry{.item_id = "9997", .count = 1}, + HandinEntry{.item_id = "9997", .count = 1}, + }, + }, + .handin_check_result = false, + }, + TestCase{ + .description = "Test handing in money", + .hand_in = { + .items = {}, + .money = {.platinum = 1}, + }, + .required = { + .items = {}, + .money = {.platinum = 1}, + }, + .returned = {}, + .handin_check_result = true, + }, + TestCase{ + .description = "Test handing in money, but not enough", + .hand_in = { + .items = {}, + .money = {.platinum = 1}, + }, + .required = { + .items = {}, + .money = {.platinum = 100}, + }, + .returned = {}, + .handin_check_result = false, + }, + TestCase{ + .description = "Test handing in money, but not enough of any type", + .hand_in = { + .items = {}, + .money = {.platinum = 1, .gold = 1, .silver = 1, .copper = 1}, + }, + .required = { + .items = {}, + .money = {.platinum = 100, .gold = 100, .silver = 100, .copper = 100}, + }, + .returned = {}, + .handin_check_result = false, + }, + TestCase{ + .description = "Test handing in money of all types", + .hand_in = { + .items = {}, + .money = {.platinum = 1, .gold = 1, .silver = 1, .copper = 1}, + }, + .required = { + .items = {}, + .money = {.platinum = 1, .gold = 1, .silver = 1, .copper = 1}, + }, + .returned = {}, + .handin_check_result = true, + }, + TestCase{ + .description = "Test handing in platinum with items with success", + .hand_in = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + .money = {.platinum = 1}, + }, + .required = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + .money = {.platinum = 1}, + }, + .returned = {}, + .handin_check_result = true, + }, + TestCase{ + .description = "Test handing in platinum with items with failure", + .hand_in = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + .money = {.platinum = 1}, + }, + .required = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + .money = {.platinum = 100}, + }, + .returned = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + }, + .handin_check_result = false, + }, + TestCase{ + .description = "Test returning money and items", + .hand_in = { + .items = { + HandinEntry{.item_id = "1007", .count = 1}, + }, + .money = { + .platinum = 1, + .gold = 666, + .silver = 234, + .copper = 444, + }, + }, + .required = { + .items = { + HandinEntry{.item_id = "1001", .count = 1}, + }, + .money = {.platinum = 100}, + }, + .returned = { + .items = { + HandinEntry{.item_id = "1007", .count = 1}, + }, + .money = { + .platinum = 1, + .gold = 666, + .silver = 234, + .copper = 444, + }, + }, + .handin_check_result = false, + }, + TestCase{ + .description = "Test returning money", + .hand_in = { + .items = {}, + .money = { + .platinum = 1, + .gold = 666, + .silver = 234, + .copper = 444, + }, + }, + .required = { + .items = {}, + .money = {.platinum = 100}, + }, + .returned = { + .items = { + }, + .money = { + .platinum = 1, + .gold = 666, + .silver = 234, + .copper = 444, + }, + }, + .handin_check_result = false, + }, + TestCase{ + .description = "Test handing in many items of the same required item", + .hand_in = { + .items = { + HandinEntry{.item_id = "1007", .count = 1}, + HandinEntry{.item_id = "1007", .count = 1}, + HandinEntry{.item_id = "1007", .count = 1}, + HandinEntry{.item_id = "1007", .count = 1}, + }, + .money = {}, + }, + .required = { + .items = { + HandinEntry{.item_id = "1007", .count = 1}, + }, + .money = {}, + }, + .returned = { + .items = { + HandinEntry{.item_id = "1007", .count = 1}, + HandinEntry{.item_id = "1007", .count = 1}, + HandinEntry{.item_id = "1007", .count = 1}, + }, + .money = { + .platinum = 1, + .gold = 666, + .silver = 234, + .copper = 444, + }, + }, + .handin_check_result = true, + }, + TestCase{ + .description = "Test handing in item of a stack", + .hand_in = { + .items = { + HandinEntry{.item_id = "13005", .count = 20}, + }, + .money = {}, + }, + .required = { + .items = { + HandinEntry{.item_id = "13005", .count = 20}, + }, + .money = {}, + }, + .returned = { + .items = {}, + .money = {}, + }, + .handin_check_result = true, + }, + TestCase{ + .description = "Test handing in item of a stack but not enough", + .hand_in = { + .items = { + HandinEntry{.item_id = "13005", .count = 10}, + }, + .money = {}, + }, + .required = { + .items = { + HandinEntry{.item_id = "13005", .count = 20}, + }, + .money = {}, + }, + .returned = { + .items = { + HandinEntry{.item_id = "13005", .count = 10}, + }, + .money = {}, + }, + .handin_check_result = false, + }, + TestCase{ + .description = "Test handing in 4 non-stacking helmets when 4 are required", + .hand_in = { + .items = { + HandinEntry{.item_id = "29062", .count = 1}, + HandinEntry{.item_id = "29062", .count = 1}, + HandinEntry{.item_id = "29062", .count = 1}, + HandinEntry{.item_id = "29062", .count = 1}, + }, + .money = {}, + }, + .required = { + .items = { + HandinEntry{.item_id = "29062", .count = 4}, + }, + .money = {}, + }, + .returned = { + .items = { + }, + .money = {}, + }, + .handin_check_result = true, + }, + TestCase{ + .description = "Test handing in Soulfire that has 5 charges and have it count as 1 item", + .hand_in = { + .items = { + HandinEntry{.item_id = "5504", .count = 1}, + }, + .money = {}, + }, + .required = { + .items = { + HandinEntry{.item_id = "5504", .count = 1}, + }, + .money = {}, + }, + .returned = { + .items = { + }, + .money = {}, + }, + .handin_check_result = true, + }, + }; + + std::map hand_ins; + std::map required; + std::vector items; + + // turn this on to see debugging output + LogSys.log_settings[Logs::NpcHandin].log_to_console = std::getenv("DEBUG") ? 3 : 0; + + LogInfo("{}", Strings::Repeat("-", break_length)); + + for (auto &test_case: test_cases) { + hand_ins.clear(); + required.clear(); + items.clear(); + + for (auto &hand_in: test_case.hand_in.items) { + auto item_id = Strings::ToInt(hand_in.item_id); + EQ::ItemInstance *inst = database.CreateItem(item_id); + if (inst->IsStackable()) { + inst->SetCharges(hand_in.count); + } + + if (inst->GetItem()->MaxCharges > 0) { + inst->SetCharges(inst->GetItem()->MaxCharges); + } + + hand_ins[hand_in.item_id] = inst->GetCharges(); + items.push_back(inst); + } + + // money + if (test_case.hand_in.money.platinum > 0) { + hand_ins["platinum"] = test_case.hand_in.money.platinum; + } + if (test_case.hand_in.money.gold > 0) { + hand_ins["gold"] = test_case.hand_in.money.gold; + } + if (test_case.hand_in.money.silver > 0) { + hand_ins["silver"] = test_case.hand_in.money.silver; + } + if (test_case.hand_in.money.copper > 0) { + hand_ins["copper"] = test_case.hand_in.money.copper; + } + + for (auto &req: test_case.required.items) { + required[req.item_id] = req.count; + } + + // money + if (test_case.required.money.platinum > 0) { + required["platinum"] = test_case.required.money.platinum; + } + if (test_case.required.money.gold > 0) { + required["gold"] = test_case.required.money.gold; + } + if (test_case.required.money.silver > 0) { + required["silver"] = test_case.required.money.silver; + } + if (test_case.required.money.copper > 0) { + required["copper"] = test_case.required.money.copper; + } + + auto result = npc->CheckHandin(c, hand_ins, required, items); + if (result != test_case.handin_check_result) { + failed_count++; + LogError("FAIL [{}]", test_case.description); + // print out the hand-ins + LogError("Hand-ins >"); + for (auto &hand_in: hand_ins) { + LogError(" > Item [{}] count [{}]", hand_in.first, hand_in.second); + } + LogError("Required >"); + for (auto &req: required) { + LogError(" > Item [{}] count [{}]", req.first, req.second); + } + LogError("Expected [{}] got [{}]", test_case.handin_check_result, result); + } + else { + LogInfo("PASS [{}]", test_case.description); + } + + auto returned = npc->ReturnHandinItems(c); + + // assert that returned items are expected + for (auto &item: test_case.returned.items) { + auto found = false; + for (auto &ret: returned.items) { + if (ret.item_id == item.item_id) { + found = true; + break; + } + } + if (!found) { + LogError("Returned item [{}] not expected", item.item_id); + } + } + + npc->ResetHandin(); + + if (LogSys.log_settings[Logs::NpcHandin].log_to_console > 0) { + std::cout << std::endl; + } + } + } + + if (failed_count > 0) { + LogError("Failed [{}] tests", failed_count); + std::exit(1); + } + else { + LogInfo("All tests passed"); + } +} diff --git a/zone/client.cpp b/zone/client.cpp index 71afd1ee1..8a1ac19e6 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -89,6 +89,314 @@ extern PetitionList petition_list; void UpdateWindowTitle(char* iNewTitle); +// client constructor purely for testing / mocking +Client::Client() : Mob( + "No name", // in_name + "", // in_lastname + 0, // in_cur_hp + 0, // in_max_hp + Gender::Male, // in_gender + Race::Doug, // in_race + Class::None, // in_class + BodyType::Humanoid, // in_bodytype + Deity::Unknown, // in_deity + 0, // in_level + 0, // in_npctype_id + 0.0f, // in_size + 0.7f, // in_runspeed + glm::vec4(), // position + 0, // in_light + 0xFF, // in_texture + 0xFF, // in_helmtexture + 0, // in_ac + 0, // in_atk + 0, // in_str + 0, // in_sta + 0, // in_dex + 0, // in_agi + 0, // in_int + 0, // in_wis + 0, // in_cha + 0, // in_haircolor + 0, // in_beardcolor + 0, // in_eyecolor1 + 0, // in_eyecolor2 + 0, // in_hairstyle + 0, // in_luclinface + 0, // in_beard + 0, // in_drakkin_heritage + 0, // in_drakkin_tattoo + 0, // in_drakkin_details + EQ::TintProfile(), // in_armor_tint + 0xff, // in_aa_title + 0, // in_see_invis + 0, // in_see_invis_undead + 0, // in_see_hide + 0, // in_see_improved_hide + 0, // in_hp_regen + 0, // in_mana_regen + 0, // in_qglobal + 0, // in_maxlevel + 0, // in_scalerate + 0, // in_armtexture + 0, // in_bracertexture + 0, // in_handtexture + 0, // in_legtexture + 0, // in_feettexture + 0, // in_usemodel + false, // in_always_aggros_foes + 0, // in_heroic_strikethrough + false // in_keeps_sold_items +), + hpupdate_timer(2000), + camp_timer(29000), + process_timer(100), + consume_food_timer(CONSUMPTION_TIMER), + zoneinpacket_timer(1000), + linkdead_timer(RuleI(Zone, ClientLinkdeadMS)), + dead_timer(2000), + global_channel_timer(1000), + fishing_timer(8000), + endupkeep_timer(1000), + autosave_timer(RuleI(Character, AutosaveIntervalS) * 1000), + m_client_npc_aggro_scan_timer(RuleI(Aggro, ClientAggroCheckIdleInterval)), + m_client_bulk_npc_pos_update_timer(60 * 1000), + tribute_timer(Tribute_duration), + proximity_timer(ClientProximity_interval), + TaskPeriodic_Timer(RuleI(TaskSystem, PeriodicCheckTimer) * 1000), + charm_update_timer(6000), + rest_timer(1), + pick_lock_timer(1000), + charm_class_attacks_timer(3000), + charm_cast_timer(3500), + qglobal_purge_timer(30000), + TrackingTimer(2000), + RespawnFromHoverTimer(0), + merc_timer(RuleI(Mercs, UpkeepIntervalMS)), + ItemQuestTimer(500), + anon_toggle_timer(250), + afk_toggle_timer(250), + helm_toggle_timer(250), + aggro_meter_timer(AGGRO_METER_UPDATE_MS), + m_Proximity(FLT_MAX, FLT_MAX, FLT_MAX), //arbitrary large number + m_ZoneSummonLocation(-2.0f, -2.0f, -2.0f, -2.0f), + m_AutoAttackPosition(0.0f, 0.0f, 0.0f, 0.0f), + m_AutoAttackTargetLocation(0.0f, 0.0f, 0.0f), + last_region_type(RegionTypeUnsupported), + m_dirtyautohaters(false), + m_position_update_timer(10000), + consent_throttle_timer(2000), + tmSitting(0), + parcel_timer(RuleI(Parcel, ParcelDeliveryDelay)), + lazy_load_bank_check_timer(1000), + bandolier_throttle_timer(0) +{ + eqs = nullptr; + for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) { + SetFilter(client_filter, FilterShow); + } + + cheat_manager.SetClient(this); + mMovementManager->AddClient(this); + character_id = 0; + conn_state = NoPacketsReceived; + client_data_loaded = false; + berserk = false; + dead = false; + client_state = CLIENT_CONNECTING; + SetTrader(false); + Haste = 0; + SetCustomerID(0); + SetTraderID(0); + TrackingID = 0; + WID = 0; + account_id = 0; + admin = AccountStatus::Player; + lsaccountid = 0; + guild_id = GUILD_NONE; + guildrank = 0; + guild_tribute_opt_in = 0; + SetGuildListDirty(false); + GuildBanker = false; + memset(lskey, 0, sizeof(lskey)); + strcpy(account_name, ""); + tellsoff = false; + last_reported_mana = 0; + last_reported_endurance = 0; + last_reported_endurance_percent = 0; + last_reported_mana_percent = 0; + gm_hide_me = false; + AFK = false; + LFG = false; + LFGFromLevel = 0; + LFGToLevel = 0; + LFGMatchFilter = false; + LFGComments[0] = '\0'; + LFP = false; + gmspeed = 0; + gminvul = false; + playeraction = 0; + SetTarget(0); + auto_attack = false; + auto_fire = false; + runmode = false; + linkdead_timer.Disable(); + zonesummon_id = 0; + zonesummon_ignorerestrictions = 0; + bZoning = false; + m_lock_save_position = false; + zone_mode = ZoneUnsolicited; + casting_spell_id = 0; + npcflag = false; + npclevel = 0; + fishing_timer.Disable(); + dead_timer.Disable(); + camp_timer.Disable(); + autosave_timer.Disable(); + GetMercTimer()->Disable(); + instalog = false; + m_pp.autosplit = false; + // initialise haste variable + m_tradeskill_object = nullptr; + delaytimer = false; + PendingRezzXP = -1; + PendingRezzDBID = 0; + PendingRezzSpellID = 0; + numclients++; + // emuerror; + UpdateWindowTitle(nullptr); + horseId = 0; + tgb = false; + tribute_master_id = 0xFFFFFFFF; + tribute_timer.Disable(); + task_state = nullptr; + TotalSecondsPlayed = 0; + keyring.clear(); + bind_sight_target = nullptr; + p_raid_instance = nullptr; + mercid = 0; + mercSlot = 0; + InitializeMercInfo(); + SetMerc(0); + if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) SetPVP(true, false); + dynamiczone_removal_timer.Disable(); + + //for good measure: + memset(&m_pp, 0, sizeof(m_pp)); + memset(&m_epp, 0, sizeof(m_epp)); + PendingTranslocate = false; + PendingSacrifice = false; + sacrifice_caster_id = 0; + controlling_boat_id = 0; + controlled_mob_id = 0; + qGlobals = nullptr; + + if (!RuleB(Character, PerCharacterQglobalMaxLevel) && !RuleB(Character, PerCharacterBucketMaxLevel)) { + SetClientMaxLevel(0); + } else if (RuleB(Character, PerCharacterQglobalMaxLevel)) { + SetClientMaxLevel(GetCharMaxLevelFromQGlobal()); + } else if (RuleB(Character, PerCharacterBucketMaxLevel)) { + SetClientMaxLevel(GetCharMaxLevelFromBucket()); + } + + KarmaUpdateTimer = new Timer(RuleI(Chat, KarmaUpdateIntervalMS)); + GlobalChatLimiterTimer = new Timer(RuleI(Chat, IntervalDurationMS)); + AttemptedMessages = 0; + TotalKarma = 0; + m_ClientVersion = EQ::versions::ClientVersion::Unknown; + m_ClientVersionBit = 0; + AggroCount = 0; + ooc_regen = false; + AreaHPRegen = 1.0f; + AreaManaRegen = 1.0f; + AreaEndRegen = 1.0f; + XPRate = 100; + current_endurance = 0; + + CanUseReport = true; + aa_los_them_mob = nullptr; + los_status = false; + los_status_facing = false; + HideCorpseMode = HideCorpseNone; + PendingGuildInvitation = false; + + InitializeBuffSlots(); + + adventure_request_timer = nullptr; + adventure_create_timer = nullptr; + adventure_leave_timer = nullptr; + adventure_door_timer = nullptr; + adv_requested_data = nullptr; + adventure_stats_timer = nullptr; + adventure_leaderboard_timer = nullptr; + adv_data = nullptr; + adv_requested_theme = LDoNTheme::Unused; + adv_requested_id = 0; + adv_requested_member_count = 0; + + for(int i = 0; i < XTARGET_HARDCAP; ++i) + { + XTargets[i].Type = Auto; + XTargets[i].ID = 0; + XTargets[i].Name[0] = 0; + XTargets[i].dirty = false; + } + MaxXTargets = 5; + XTargetAutoAddHaters = true; + m_autohatermgr.SetOwner(this, nullptr, nullptr); + m_activeautohatermgr = &m_autohatermgr; + + initial_respawn_selection = 0; + alternate_currency_loaded = false; + + interrogateinv_flag = false; + + trapid = 0; + + for (int i = 0; i < InnateSkillMax; ++i) + m_pp.InnateSkills[i] = InnateDisabled; + + temp_pvp = false; + + moving = false; + + environment_damage_modifier = 0; + invulnerable_environment_damage = false; + + // rate limiter + m_list_task_timers_rate_limit.Start(1000); + + // gm + SetDisplayMobInfoWindow(true); + SetDevToolsEnabled(true); + + bot_owner_options[booDeathMarquee] = false; + bot_owner_options[booStatsUpdate] = false; + bot_owner_options[booSpawnMessageSay] = false; + bot_owner_options[booSpawnMessageTell] = true; + bot_owner_options[booSpawnMessageClassSpecific] = true; + bot_owner_options[booAutoDefend] = RuleB(Bots, AllowOwnerOptionAutoDefend); + bot_owner_options[booBuffCounter] = false; + bot_owner_options[booMonkWuMessage] = false; + + m_parcel_platinum = 0; + m_parcel_gold = 0; + m_parcel_silver = 0; + m_parcel_copper = 0; + m_parcel_count = 0; + m_parcel_enabled = true; + m_parcel_merchant_engaged = false; + m_parcels.clear(); + + m_buyer_id = 0; + + SetBotPulling(false); + SetBotPrecombat(false); + + AI_Init(); + +} + Client::Client(EQStreamInterface *ieqs) : Mob( "No name", // in_name "", // in_lastname @@ -504,9 +812,11 @@ Client::~Client() { zone->RemoveAuth(GetName(), lskey); //let the stream factory know were done with this stream - eqs->Close(); - eqs->ReleaseFromUse(); - safe_delete(eqs); + if (eqs) { + eqs->Close(); + eqs->ReleaseFromUse(); + safe_delete(eqs); + } UninitializeBuffSlots(); } @@ -2488,6 +2798,7 @@ void Client::AddMoneyToPP(uint64 copper, bool update_client){ /* Add Amount of Platinum */ temporary_copper_two = temporary_copper / 1000; + m_external_handin_money_returned.platinum = temporary_copper_two; int32 new_value = m_pp.platinum + temporary_copper_two; if (new_value < 0) { @@ -2501,6 +2812,7 @@ void Client::AddMoneyToPP(uint64 copper, bool update_client){ /* Add Amount of Gold */ temporary_copper_two = temporary_copper / 100; new_value = m_pp.gold + temporary_copper_two; + m_external_handin_money_returned.gold = temporary_copper_two; if (new_value < 0) { m_pp.gold = 0; @@ -2513,6 +2825,7 @@ void Client::AddMoneyToPP(uint64 copper, bool update_client){ /* Add Amount of Silver */ temporary_copper_two = temporary_copper / 10; new_value = m_pp.silver + temporary_copper_two; + m_external_handin_money_returned.silver = temporary_copper_two; if (new_value < 0) { m_pp.silver = 0; @@ -2525,6 +2838,7 @@ void Client::AddMoneyToPP(uint64 copper, bool update_client){ /* Add Amount of Copper */ temporary_copper_two = temporary_copper; new_value = m_pp.copper + temporary_copper_two; + m_external_handin_money_returned.copper = temporary_copper_two; if (new_value < 0) { m_pp.copper = 0; @@ -2541,23 +2855,12 @@ void Client::AddMoneyToPP(uint64 copper, bool update_client){ SaveCurrency(); + m_external_handin_money_returned.return_source = "AddMoneyToPP"; + LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); } -void Client::EVENT_ITEM_ScriptStopReturn(){ - /* Set a timestamp in an entity variable for plugin check_handin.pl in return_items - This will stopgap players from items being returned if global_npc.pl has a catch all return_items - */ - struct timeval read_time; - char buffer[50]; - gettimeofday(&read_time, 0); - sprintf(buffer, "%li.%li \n", read_time.tv_sec, read_time.tv_usec); - SetEntityVariable("Stop_Return", buffer); -} - void Client::AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 platinum, bool update_client){ - EVENT_ITEM_ScriptStopReturn(); - int32 new_value = m_pp.platinum + platinum; if (new_value >= 0 && new_value > m_pp.platinum) { m_pp.platinum += platinum; @@ -2585,6 +2888,14 @@ void Client::AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 plat RecalcWeight(); SaveCurrency(); + m_external_handin_money_returned = ExternalHandinMoneyReturned{ + .copper = copper, + .silver = silver, + .gold = gold, + .platinum = platinum, + .return_source = "AddMoneyToPP" + }; + #if (EQDEBUG>=5) LogDebug("Client::AddMoneyToPP() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", GetName(), m_pp.platinum, m_pp.gold, m_pp.silver, m_pp.copper); @@ -12373,248 +12684,6 @@ void Client::PlayerTradeEventLog(Trade *t, Trade *t2) RecordPlayerEventLogWithClient(trader2, PlayerEvent::TRADE, e); } -void Client::NPCHandinEventLog(Trade* t, NPC* n) -{ - Client* c = t->GetOwner()->CastToClient(); - - std::vector hi = {}; - std::vector ri = {}; - PlayerEvent::HandinMoney hm{}; - PlayerEvent::HandinMoney rm{}; - - if ( - c->EntityVariableExists("HANDIN_ITEMS") && - c->EntityVariableExists("HANDIN_MONEY") && - c->EntityVariableExists("RETURN_ITEMS") && - c->EntityVariableExists("RETURN_MONEY") - ) { - const std::string& handin_items = c->GetEntityVariable("HANDIN_ITEMS"); - const std::string& return_items = c->GetEntityVariable("RETURN_ITEMS"); - const std::string& handin_money = c->GetEntityVariable("HANDIN_MONEY"); - const std::string& return_money = c->GetEntityVariable("RETURN_MONEY"); - - // Handin Items - if (!handin_items.empty()) { - if (Strings::Contains(handin_items, ",")) { - const auto handin_data = Strings::Split(handin_items, ","); - for (const auto& h : handin_data) { - const auto item_data = Strings::Split(h, "|"); - if ( - item_data.size() == 3 && - Strings::IsNumber(item_data[0]) && - Strings::IsNumber(item_data[1]) && - Strings::IsNumber(item_data[2]) - ) { - const uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - if (item_id != 0) { - const auto* item = database.GetItem(item_id); - - if (item) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - } else if (Strings::Contains(handin_items, "|")) { - const auto item_data = Strings::Split(handin_items, "|"); - if ( - item_data.size() == 3 && - Strings::IsNumber(item_data[0]) && - Strings::IsNumber(item_data[1]) && - Strings::IsNumber(item_data[2]) - ) { - const uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - - // Handin Money - if (!handin_money.empty()) { - const auto hms = Strings::Split(handin_money, "|"); - - hm.copper = Strings::ToUnsignedInt(hms[0]); - hm.silver = Strings::ToUnsignedInt(hms[1]); - hm.gold = Strings::ToUnsignedInt(hms[2]); - hm.platinum = Strings::ToUnsignedInt(hms[3]); - } - - // Return Items - if (!return_items.empty()) { - if (Strings::Contains(return_items, ",")) { - const auto return_data = Strings::Split(return_items, ","); - for (const auto& r : return_data) { - const auto item_data = Strings::Split(r, "|"); - if ( - item_data.size() == 3 && - Strings::IsNumber(item_data[0]) && - Strings::IsNumber(item_data[1]) && - Strings::IsNumber(item_data[2]) - ) { - const uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - ri.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } else if (Strings::Contains(return_items, "|")) { - const auto item_data = Strings::Split(return_items, "|"); - if ( - item_data.size() == 3 && - Strings::IsNumber(item_data[0]) && - Strings::IsNumber(item_data[1]) && - Strings::IsNumber(item_data[2]) - ) { - const uint32 item_id = Strings::ToUnsignedInt(item_data[0]); - const auto* item = database.GetItem(item_id); - - if (item) { - ri.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = item_id, - .item_name = item->Name, - .charges = static_cast(Strings::ToUnsignedInt(item_data[1])), - .attuned = Strings::ToInt(item_data[2]) ? true : false - } - ); - } - } - } - } - - // Return Money - if (!return_money.empty()) { - const auto rms = Strings::Split(return_money, "|"); - rm.copper = static_cast(Strings::ToUnsignedInt(rms[0])); - rm.silver = static_cast(Strings::ToUnsignedInt(rms[1])); - rm.gold = static_cast(Strings::ToUnsignedInt(rms[2])); - rm.platinum = static_cast(Strings::ToUnsignedInt(rms[3])); - } - - c->DeleteEntityVariable("HANDIN_ITEMS"); - c->DeleteEntityVariable("HANDIN_MONEY"); - c->DeleteEntityVariable("RETURN_ITEMS"); - c->DeleteEntityVariable("RETURN_MONEY"); - - const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; - - const bool event_has_data_to_record = ( - !hi.empty() || handed_in_money - ); - - if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { - auto e = PlayerEvent::HandinEvent{ - .npc_id = n->GetNPCTypeID(), - .npc_name = n->GetCleanName(), - .handin_items = hi, - .handin_money = hm, - .return_items = ri, - .return_money = rm, - .is_quest_handin = true - }; - - RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); - } - - return; - } - - uint8 item_count = 0; - - hm.platinum = t->pp; - hm.gold = t->gp; - hm.silver = t->sp; - hm.copper = t->cp; - - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { - if (c->GetInv().GetItem(i)) { - item_count++; - } - } - - hi.reserve(item_count); - - if (item_count > 0) { - for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) { - const EQ::ItemInstance* inst = c->GetInv().GetItem(i); - if (inst) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = inst->GetItem()->ID, - .item_name = inst->GetItem()->Name, - .charges = static_cast(inst->GetCharges()), - .attuned = inst->IsAttuned() - } - ); - - if (inst->IsClassBag()) { - for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) { - inst = c->GetInv().GetItem(i, j); - if (inst) { - hi.emplace_back( - PlayerEvent::HandinEntry{ - .item_id = inst->GetItem()->ID, - .item_name = inst->GetItem()->Name, - .charges = static_cast(inst->GetCharges()), - .attuned = inst->IsAttuned() - } - ); - } - } - } - } - } - } - - const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0; - - ri = hi; - rm = hm; - - const bool event_has_data_to_record = !hi.empty() || handed_in_money; - - if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { - auto e = PlayerEvent::HandinEvent{ - .npc_id = n->GetNPCTypeID(), - .npc_name = n->GetCleanName(), - .handin_items = hi, - .handin_money = hm, - .return_items = ri, - .return_money = rm, - .is_quest_handin = false - }; - - RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); - } -} - void Client::ShowSpells(Client* c, ShowSpellType show_spell_type) { std::string spell_string; diff --git a/zone/client.h b/zone/client.h index cd3f5f644..cc710c855 100644 --- a/zone/client.h +++ b/zone/client.h @@ -255,6 +255,7 @@ public: #include "client_packet.h" Client(EQStreamInterface * ieqs); + Client(); // mocking / testing ~Client(); void ReconnectUCS(); @@ -1101,7 +1102,6 @@ public: // Item methods void UseAugmentContainer(int container_slot); - void EVENT_ITEM_ScriptStopReturn(); uint32 NukeItem(uint32 itemnum, uint8 where_to_check = (invWhereWorn | invWherePersonal | invWhereBank | invWhereSharedBank | invWhereTrading | invWhereCursor)); void SetTint(int16 slot_id, uint32 color); @@ -1868,6 +1868,24 @@ public: uint32 GetBandolierItemID(uint8 bandolier_slot, uint8 slot_id); std::string GetBandolierItemName(uint8 bandolier_slot, uint8 slot_id); + // External handin tracking + // this is used to prevent things like quest::givecash and AddMoneyToPP + // from double giving money back to players in scripts when return_items + // also gives money back to players + struct ExternalHandinMoneyReturned { + uint64 copper; + uint64 silver; + uint64 gold; + uint64 platinum; + std::string return_source; + }; +private: + ExternalHandinMoneyReturned m_external_handin_money_returned = {}; + std::vector m_external_handin_items_returned = {}; +public: + ExternalHandinMoneyReturned GetExternalHandinMoneyReturned() { return m_external_handin_money_returned; } + std::vector GetExternalHandinItemsReturned() { return m_external_handin_items_returned; } + protected: friend class Mob; void CalcEdibleBonuses(StatBonuses* newbon); @@ -2317,7 +2335,6 @@ private: bool CanTradeFVNoDropItem(); void SendMobPositions(); void PlayerTradeEventLog(Trade *t, Trade *t2); - void NPCHandinEventLog(Trade* t, NPC* n); // full and partial mail key cache std::string m_mail_key_full; diff --git a/zone/exp.cpp b/zone/exp.cpp index a1f8c8adf..a7ebee711 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -502,9 +502,6 @@ void Client::AddEXP(ExpSource exp_source, uint64 in_add_exp, uint8 conlevel, boo return; } - - EVENT_ITEM_ScriptStopReturn(); - uint64 exp = 0; uint64 aaexp = 0; diff --git a/zone/inventory.cpp b/zone/inventory.cpp index 5d7bdf68f..129cda55e 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -181,10 +181,6 @@ bool Client::CheckLoreConflict(const EQ::ItemData* item) } bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, uint32 aug3, uint32 aug4, uint32 aug5, uint32 aug6, bool attuned, uint16 to_slot, uint32 ornament_icon, uint32 ornament_idfile, uint32 ornament_hero_model) { - EVENT_ITEM_ScriptStopReturn(); - - // TODO: update calling methods and script apis to handle a failure return - const EQ::ItemData* item = database.GetItem(item_id); // make sure the item exists @@ -658,6 +654,8 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, PutItemInInventory(to_slot, *inst, true); } + m_external_handin_items_returned.emplace_back(inst->GetItem()->ID); + safe_delete(inst); // discover item and any augments @@ -3181,8 +3179,13 @@ uint32 Client::GetEquipmentColor(uint8 material_slot) const // Send an item packet (including all subitems of the item) void Client::SendItemPacket(int16 slot_id, const EQ::ItemInstance* inst, ItemPacketType packet_type) { - if (!inst) + if (!inst) { return; + } + + if (!eqs) { + return; + } if (packet_type != ItemPacketMerchant) { if (slot_id <= EQ::invslot::POSSESSIONS_END && slot_id >= EQ::invslot::POSSESSIONS_BEGIN) { diff --git a/zone/lua_iteminst.cpp b/zone/lua_iteminst.cpp index f81cfc88b..a911fcee0 100644 --- a/zone/lua_iteminst.cpp +++ b/zone/lua_iteminst.cpp @@ -433,6 +433,12 @@ void Lua_ItemInst::SetEvolveProgression(float amount) self->SetEvolveProgression(amount); } +int Lua_ItemInst::GetSerialNumber() +{ + Lua_Safe_Call_Int(); + return self->GetSerialNumber(); +} + luabind::scope lua_register_iteminst() { return luabind::class_("ItemInst") .def(luabind::constructor<>()) @@ -475,6 +481,7 @@ luabind::scope lua_register_iteminst() { .def("GetItemScriptID", (uint32(Lua_ItemInst::*)(void))&Lua_ItemInst::GetItemScriptID) .def("GetMaxEvolveLvl", (int(Lua_ItemInst::*)(void))&Lua_ItemInst::GetMaxEvolveLvl) .def("GetName", (std::string(Lua_ItemInst::*)(void))&Lua_ItemInst::GetName) + .def("GetSerialNumber", (int(Lua_ItemInst::*)(void))&Lua_ItemInst::GetSerialNumber) .def("GetPrice", (uint32(Lua_ItemInst::*)(void))&Lua_ItemInst::GetPrice) .def("GetTaskDeliveredCount", &Lua_ItemInst::GetTaskDeliveredCount) .def("GetTotalItemCount", (uint8(Lua_ItemInst::*)(void))&Lua_ItemInst::GetTotalItemCount) diff --git a/zone/lua_iteminst.h b/zone/lua_iteminst.h index a4c7529e4..486378058 100644 --- a/zone/lua_iteminst.h +++ b/zone/lua_iteminst.h @@ -86,6 +86,7 @@ public: int GetTaskDeliveredCount(); int RemoveTaskDeliveredItems(); std::string GetName(); + int GetSerialNumber(); void ItemSay(const char* text); void ItemSay(const char* text, uint8 language_id); luabind::object GetAugmentIDs(lua_State* L); diff --git a/zone/lua_npc.cpp b/zone/lua_npc.cpp index aa0973292..eb41e7ac7 100644 --- a/zone/lua_npc.cpp +++ b/zone/lua_npc.cpp @@ -7,6 +7,8 @@ #include "npc.h" #include "lua_npc.h" #include "lua_client.h" +#include "lua_item.h" +#include "lua_iteminst.h" struct Lua_NPC_Loot_List { std::vector entries; @@ -837,6 +839,99 @@ void Lua_NPC::DescribeSpecialAbilities(Lua_Client c) self->DescribeSpecialAbilities(c); } +bool Lua_NPC::IsMultiQuestEnabled() +{ + Lua_Safe_Call_Bool(); + return self->IsMultiQuestEnabled(); +} + +void Lua_NPC::MultiQuestEnable() +{ + Lua_Safe_Call_Void(); + self->MultiQuestEnable(); +} + +bool Lua_NPC::LuaCheckHandin( + Lua_Client c, + luabind::adl::object handin_table, + luabind::adl::object required_table, + luabind::adl::object items_table +) +{ + Lua_Safe_Call_Bool(); + + if ( + luabind::type(handin_table) != LUA_TTABLE || + luabind::type(required_table) != LUA_TTABLE || + luabind::type(items_table) != LUA_TTABLE + ) { + return false; + } + + std::map handin_map; + std::map required_map; + std::vector items; + + for (luabind::iterator i(handin_table), end; i != end; i++) { + std::string key; + if (luabind::type(i.key()) == LUA_TSTRING) { + key = luabind::object_cast(i.key()); + } + else if (luabind::type(i.key()) == LUA_TNUMBER) { + key = fmt::format("{}", luabind::object_cast(i.key())); + } + else { + LogError("Handin key type [{}] not supported", luabind::type(i.key())); + } + + if (!key.empty()) { + handin_map[key] = luabind::object_cast(handin_table[i.key()]); + LogNpcHandinDetail("Handin key [{}] value [{}]", key, handin_map[key]); + } + } + + for (luabind::iterator i(required_table), end; i != end; i++) { + std::string key; + if (luabind::type(i.key()) == LUA_TSTRING) { + key = luabind::object_cast(i.key()); + } + else if (luabind::type(i.key()) == LUA_TNUMBER) { + key = fmt::format("{}", luabind::object_cast(i.key())); + } + else { + LogError("Required key type [{}] not supported", luabind::type(i.key())); + } + + if (!key.empty()) { + required_map[key] = luabind::object_cast(required_table[i.key()]); + LogNpcHandinDetail("Required key [{}] value [{}]", key, required_map[key]); + } + } + + for (luabind::iterator i(items_table), end; i != end; i++) { + auto item = luabind::object_cast(items_table[i.key()]); + + if (item && item.GetItem()) { + LogNpcHandinDetail( + "Item instance [{}] ({}) UUID ({}) added to handin list", + item.GetName(), + item.GetID(), + item.GetSerialNumber() + ); + + items.emplace_back(item); + } + } + + return self->CheckHandin(c, handin_map, required_map, items); +} + +void Lua_NPC::ReturnHandinItems(Lua_Client c) +{ + Lua_Safe_Call_Void(); + self->ReturnHandinItems(c); +} + luabind::scope lua_register_npc() { return luabind::class_("NPC") .def(luabind::constructor<>()) @@ -859,6 +954,7 @@ luabind::scope lua_register_npc() { .def("AssignWaypoints", (void(Lua_NPC::*)(int))&Lua_NPC::AssignWaypoints) .def("CalculateNewWaypoint", (void(Lua_NPC::*)(void))&Lua_NPC::CalculateNewWaypoint) .def("ChangeLastName", (void(Lua_NPC::*)(std::string))&Lua_NPC::ChangeLastName) + .def("CheckHandin", (bool(Lua_NPC::*)(Lua_Client,luabind::adl::object,luabind::adl::object,luabind::adl::object))&Lua_NPC::LuaCheckHandin) .def("CheckNPCFactionAlly", (int(Lua_NPC::*)(int))&Lua_NPC::CheckNPCFactionAlly) .def("ClearItemList", (void(Lua_NPC::*)(void))&Lua_NPC::ClearLootItems) .def("ClearLastName", (void(Lua_NPC::*)(void))&Lua_NPC::ClearLastName) @@ -932,6 +1028,7 @@ luabind::scope lua_register_npc() { .def("IsLDoNLocked", (bool(Lua_NPC::*)(void))&Lua_NPC::IsLDoNLocked) .def("IsLDoNTrapped", (bool(Lua_NPC::*)(void))&Lua_NPC::IsLDoNTrapped) .def("IsLDoNTrapDetected", (bool(Lua_NPC::*)(void))&Lua_NPC::IsLDoNTrapDetected) + .def("IsMultiQuestEnabled", (bool(Lua_NPC::*)(void))&Lua_NPC::IsMultiQuestEnabled) .def("IsOnHatelist", (bool(Lua_NPC::*)(Lua_Mob))&Lua_NPC::IsOnHatelist) .def("IsRaidTarget", (bool(Lua_NPC::*)(void))&Lua_NPC::IsRaidTarget) .def("IsRareSpawn", (bool(Lua_NPC::*)(void))&Lua_NPC::IsRareSpawn) @@ -941,6 +1038,7 @@ luabind::scope lua_register_npc() { .def("MerchantOpenShop", (void(Lua_NPC::*)(void))&Lua_NPC::MerchantOpenShop) .def("ModifyNPCStat", (void(Lua_NPC::*)(std::string,std::string))&Lua_NPC::ModifyNPCStat) .def("MoveTo", (void(Lua_NPC::*)(float,float,float,float,bool))&Lua_NPC::MoveTo) + .def("MultiQuestEnable", &Lua_NPC::MultiQuestEnable) .def("NextGuardPosition", (void(Lua_NPC::*)(void))&Lua_NPC::NextGuardPosition) .def("PauseWandering", (void(Lua_NPC::*)(int))&Lua_NPC::PauseWandering) .def("PickPocket", (void(Lua_NPC::*)(Lua_Client))&Lua_NPC::PickPocket) @@ -953,6 +1051,7 @@ luabind::scope lua_register_npc() { .def("RemoveItem", (void(Lua_NPC::*)(int,int))&Lua_NPC::RemoveItem) .def("RemoveItem", (void(Lua_NPC::*)(int,int,int))&Lua_NPC::RemoveItem) .def("ResumeWandering", (void(Lua_NPC::*)(void))&Lua_NPC::ResumeWandering) + .def("ReturnHandinItems", (void(Lua_NPC::*)(Lua_Client))&Lua_NPC::ReturnHandinItems) .def("SaveGuardSpot", (void(Lua_NPC::*)(void))&Lua_NPC::SaveGuardSpot) .def("SaveGuardSpot", (void(Lua_NPC::*)(bool))&Lua_NPC::SaveGuardSpot) .def("SaveGuardSpot", (void(Lua_NPC::*)(float,float,float,float))&Lua_NPC::SaveGuardSpot) diff --git a/zone/lua_npc.h b/zone/lua_npc.h index 180bddad0..8ec971d65 100644 --- a/zone/lua_npc.h +++ b/zone/lua_npc.h @@ -9,6 +9,7 @@ class Lua_Mob; class Lua_NPC; class Lua_Client; struct Lua_NPC_Loot_List; +class Lua_Inventory; namespace luabind { struct scope; @@ -186,6 +187,15 @@ public: void SetNPCAggro(bool in_npc_aggro); uint32 GetNPCSpellsEffectsID(); void DescribeSpecialAbilities(Lua_Client c); + bool IsMultiQuestEnabled(); + void MultiQuestEnable(); + bool LuaCheckHandin( + Lua_Client c, + luabind::adl::object handin_table, + luabind::adl::object required_table, + luabind::adl::object items_table + ); + void ReturnHandinItems(Lua_Client c); }; #endif diff --git a/zone/lua_parser_events.cpp b/zone/lua_parser_events.cpp index fb6cb8708..d6419189a 100644 --- a/zone/lua_parser_events.cpp +++ b/zone/lua_parser_events.cpp @@ -56,6 +56,11 @@ void handle_npc_event_trade( uint32 extra_data, std::vector *extra_pointers ) { + Lua_NPC l_npc(reinterpret_cast(npc)); + luabind::adl::object l_npc_o = luabind::adl::object(L, l_npc); + l_npc_o.push(L); + lua_setfield(L, -2, "self"); + Lua_Client l_client(reinterpret_cast(init)); luabind::adl::object l_client_o = luabind::adl::object(L, l_client); l_client_o.push(L); @@ -102,6 +107,10 @@ void handle_npc_event_trade( lua_pushinteger(L, money_value); lua_setfield(L, -2, "copper"); + // set a reference to the NPC inside the trade object as well for plugins to process + l_npc_o.push(L); + lua_setfield(L, -2, "self"); + // set a reference to the client inside of the trade object as well for plugins to process l_client_o.push(L); lua_setfield(L, -2, "other"); diff --git a/zone/main.cpp b/zone/main.cpp index 5d463fa9b..007f9f22a 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -124,6 +124,7 @@ void CatchSignal(int sig_num); extern void MapOpcodes(); +bool CheckForCompatibleQuestPlugins(); int main(int argc, char **argv) { RegisterExecutablePlatform(ExePlatformZone); @@ -298,7 +299,7 @@ int main(int argc, char **argv) } // command handler - if (ZoneCLI::RanConsoleCommand(argc, argv) && !ZoneCLI::RanSidecarCommand(argc, argv)) { + if (ZoneCLI::RanConsoleCommand(argc, argv) && !(ZoneCLI::RanSidecarCommand(argc, argv) || ZoneCLI::RanTestCommand(argc, argv))) { LogSys.EnableConsoleLogging(); ZoneCLI::CommandHandler(argc, argv); } @@ -369,6 +370,11 @@ int main(int argc, char **argv) return 1; } + if (!CheckForCompatibleQuestPlugins()) { + LogError("Incompatible quest plugins detected, please update your plugins to the latest version"); + return 1; + } + // load these here for now until spells and items can be truly repointed to "content_db" database.SetSharedItemsCount(content_db.GetItemsCount()); database.SetSharedSpellsCount(content_db.GetSpellsCount()); @@ -481,7 +487,8 @@ int main(int argc, char **argv) worldserver.SetScheduler(&event_scheduler); // sidecar command handler - if (ZoneCLI::RanConsoleCommand(argc, argv) && ZoneCLI::RanSidecarCommand(argc, argv)) { + if (ZoneCLI::RanConsoleCommand(argc, argv) + && (ZoneCLI::RanSidecarCommand(argc, argv) || ZoneCLI::RanTestCommand(argc, argv))) { ZoneCLI::CommandHandler(argc, argv); } @@ -712,3 +719,43 @@ void UpdateWindowTitle(char *iNewTitle) SetConsoleTitle(tmp); #endif } + +bool CheckForCompatibleQuestPlugins() +{ + const std::vector& directories = { "lua_modules", "plugins" }; + + bool lua_found = false; + bool perl_found = false; + + for (const auto& directory : directories) { + for (const auto& file : fs::directory_iterator(path.GetServerPath() + "/" + directory)) { + if (file.is_regular_file()) { + auto f = file.path().string(); + if (File::Exists(f)) { + auto r = File::GetContents(std::filesystem::path{ f }.string()); + if (Strings::Contains(r.contents, "CheckHandin")) { + if (Strings::EqualFold(directory, "lua_modules")) { + lua_found = true; + } else if (Strings::EqualFold(directory, "plugins")) { + perl_found = true; + } + + if (lua_found && perl_found) { + return true; + } + } + } + } + } + } + + if (!lua_found) { + LogError("Failed to find CheckHandin in lua_modules"); + } + + if (!perl_found) { + LogError("Failed to find CheckHandin in plugins"); + } + + return lua_found && perl_found; +} diff --git a/zone/mob.cpp b/zone/mob.cpp index 9d3b3007c..ca554bcd2 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -8648,7 +8648,7 @@ bool Mob::IsInGroupOrRaid(Mob* other, bool same_raid_group) { auto other_raid_group = other_raid->GetGroup(other->GetCleanName()); if ( - raid_group == RAID_GROUPLESS || + raid_group == RAID_GROUPLESS || other_raid_group == RAID_GROUPLESS || (same_raid_group && raid_group != other_raid_group) ) { @@ -8712,7 +8712,7 @@ bool Mob::CheckLosCheat(Mob* other) { auto other_to_door = DistanceNoZ(other->GetPosition(), d->GetPosition()); auto who_to_other = DistanceNoZ(GetPosition(), other->GetPosition()); auto distance_difference = who_to_other - (who_to_door + other_to_door); - + if (distance_difference >= (-1 * RuleR(Maps, RangeCheckForLoSCheat)) && distance_difference <= RuleR(Maps, RangeCheckForLoSCheat)) { return false; } @@ -8724,7 +8724,8 @@ bool Mob::CheckLosCheat(Mob* other) { return true; } -bool Mob::CheckLosCheatExempt(Mob* other) { +bool Mob::CheckLosCheatExempt(Mob* other) +{ if (RuleB(Map, EnableLoSCheatExemptions)) { /* This is an exmaple of how to configure exemptions for LoS checks. glm::vec4 exempt_check_who; @@ -8747,3 +8748,27 @@ bool Mob::CheckLosCheatExempt(Mob* other) { return false; } + +bool Mob::IsGuildmaster() const { + switch (GetClass()) { + case Class::WarriorGM: + case Class::ClericGM: + case Class::PaladinGM: + case Class::RangerGM: + case Class::ShadowKnightGM: + case Class::DruidGM: + case Class::MonkGM: + case Class::BardGM: + case Class::RogueGM: + case Class::ShamanGM: + case Class::NecromancerGM: + case Class::WizardGM: + case Class::MagicianGM: + case Class::EnchanterGM: + case Class::BeastlordGM: + case Class::BerserkerGM: + return true; + default: + return false; + } +} diff --git a/zone/mob.h b/zone/mob.h index 6ac286b5e..8ee41d0c3 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1504,6 +1504,7 @@ public: void CheckScanCloseMobsMovingTimer(); void ClearDataBucketCache(); + bool IsGuildmaster() const; protected: void CommonDamage(Mob* other, int64 &damage, const uint16 spell_id, const EQ::skills::SkillType attack_skill, bool &avoidable, const int8 buffslot, const bool iBuffTic, eSpecialAttacks specal = eSpecialAttacks::None); diff --git a/zone/npc.cpp b/zone/npc.cpp index 0e3572858..35f9c201c 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -49,6 +49,7 @@ #include "bot.h" #include "../common/skill_caps.h" +#include "../common/events/player_event_logs.h" #include #include @@ -226,6 +227,7 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi ATK = npc_type_data->ATK; heroic_strikethrough = npc_type_data->heroic_strikethrough; keeps_sold_items = npc_type_data->keeps_sold_items; + m_multiquest_enabled = npc_type_data->multiquest_enabled; // used for when switch back to charm default_ac = npc_type_data->AC; @@ -4263,3 +4265,619 @@ bool NPC::FacesTarget() return std::find(v.begin(), v.end(), std::to_string(GetBaseRace())) == v.end(); } +bool NPC::CanPetTakeItem(const EQ::ItemInstance *inst) +{ + if (!inst) { + return false; + } + + if (!IsPetOwnerClient()) { + return false; + } + + const bool can_take_nodrop = RuleB(Pets, CanTakeNoDrop) || inst->GetItem()->NoDrop != 0; + const bool is_charmed_with_attuned = IsCharmed() && inst->IsAttuned(); + + auto o = GetOwner() && GetOwner()->IsClient() ? GetOwner()->CastToClient() : nullptr; + + struct Check { + bool condition; + std::string message; + }; + + const Check checks[] = { + {inst->IsAttuned(), "I cannot equip attuned items, master."}, + {!can_take_nodrop || is_charmed_with_attuned, "I cannot equip no-drop items, master."}, + {inst->GetItem()->IsQuestItem(), "I cannot equip quest items, master."}, + {!inst->GetItem()->IsPetUsable(), "I cannot equip that item, master."} + }; + + // Iterate over checks and return false if any condition is true + for (const auto &c : checks) { + if (c.condition) { + if (o) { + o->Message(Chat::PetResponse, c.message.c_str()); + } + return false; + } + } + + return true; +} + +bool NPC::IsGuildmasterForClient(Client *c) { + std::map guildmaster_map = { + { Class::Warrior, Class::WarriorGM }, + { Class::Cleric, Class::ClericGM }, + { Class::Paladin, Class::PaladinGM }, + { Class::Ranger, Class::RangerGM }, + { Class::ShadowKnight, Class::ShadowKnightGM }, + { Class::Druid, Class::DruidGM }, + { Class::Monk, Class::MonkGM }, + { Class::Bard, Class::BardGM }, + { Class::Rogue, Class::RogueGM }, + { Class::Shaman, Class::ShamanGM }, + { Class::Necromancer, Class::NecromancerGM }, + { Class::Wizard, Class::WizardGM }, + { Class::Magician, Class::MagicianGM }, + { Class::Enchanter, Class::EnchanterGM }, + { Class::Beastlord, Class::BeastlordGM }, + { Class::Berserker, Class::BerserkerGM }, + }; + + if (guildmaster_map.find(c->GetClass()) != guildmaster_map.end()) { + if (guildmaster_map[c->GetClass()] == GetClass()) { + return true; + } + } + + return false; +} + +bool NPC::CheckHandin( + Client *c, + std::map handin, + std::map required, + std::vector items +) +{ + auto h = Handin{}; + auto r = Handin{}; + + std::string log_handin_prefix = fmt::format("[{}] -> [{}]", c->GetCleanName(), GetCleanName()); + + // if the npc is a multi-quest npc, we want to re-use our previously set hand-in bucket + if (!m_handin_started && IsMultiQuestEnabled()) { + h = m_hand_in; + } + + std::vector&, Handin&>> datasets = {}; + + // if we've already started the hand-in process, we don't want to re-process the hand-in data + // we continue to use the originally set hand-in bucket and decrement from it with each successive hand-in + if (m_handin_started) { + h = m_hand_in; + } else { + datasets.emplace_back(handin, h); + } + datasets.emplace_back(required, r); + + const std::string set_hand_in = "Hand-in"; + const std::string set_required = "Required"; + for (const auto &[data_map, current_handin]: datasets) { + std::string current_dataset = ¤t_handin == &h ? set_hand_in : set_required; + for (const auto &[key, value]: data_map) { + LogNpcHandinDetail("Processing [{}] key [{}] value [{}]", current_dataset, key, value); + + // Handle items + if (Strings::IsNumber(key)) { + if (const auto *exists = database.GetItem(Strings::ToUnsignedInt(key)); + exists && current_dataset == set_required) { + current_handin.items.emplace_back(HandinEntry{.item_id = key, .count = value}); + } + continue; + } + + // Handle money and any other key-value pairs + if (key == "platinum") { current_handin.money.platinum = value; } + else if (key == "gold") { current_handin.money.gold = value; } + else if (key == "silver") { current_handin.money.silver = value; } + else if (key == "copper") { current_handin.money.copper = value; } + } + } + + // pull hand-in items from the item instances + if (!m_handin_started) { + for (const auto &i: items) { + if (!i) { + continue; + } + + h.items.emplace_back( + HandinEntry{ + .item_id = std::to_string(i->GetItem()->ID), + .count = std::max(static_cast(i->IsStackable() ? i->GetCharges() : 1), static_cast(1)), + .item = i->Clone(), + .is_multiquest_item = false + } + ); + } + } + + // compare hand-in to required, the item_id can be in any slot + bool requirement_met = true; + + // money + bool money_met = h.money.platinum == r.money.platinum + && h.money.gold == r.money.gold + && h.money.silver == r.money.silver + && h.money.copper == r.money.copper; + + // if we started the hand-in process, we want to use the hand-in items from the member variable hand-in bucket + auto &handin_items = !m_handin_started ? h.items : m_hand_in.items; + + for (auto &h_item: h.items) { + LogNpcHandinDetail( + "{} Hand-in item [{}] ({}) count [{}] is_multiquest_item [{}]", + log_handin_prefix, + h_item.item->GetItem()->Name, + h_item.item_id, + h_item.count, + h_item.is_multiquest_item + ); + } + + // remove items from the hand-in bucket that were used to fulfill the requirement + std::vector items_to_remove; + + // check if the hand-in items fulfill the requirement + bool items_met = true; + if (!handin_items.empty() && !r.items.empty()) { + std::vector before_handin_state = handin_items; + for (const auto &r_item : r.items) { + uint32 remaining_requirement = r_item.count; + bool fulfilled = false; + + // Process the hand-in items using a standard for loop + for (size_t i = 0; i < handin_items.size() && remaining_requirement > 0; ++i) { + auto &h_item = handin_items[i]; + + // Check if the item IDs match (normalize if necessary) + bool id_match = (h_item.item_id == r_item.item_id); + + if (id_match) { + uint32 used_count = std::min(remaining_requirement, h_item.count); + h_item.count -= used_count; + remaining_requirement -= used_count; + + LogNpcHandinDetail( + "{} >>>> Using item [{}] ({}) count [{}] to fulfill [{}], remaining requirement [{}]", + log_handin_prefix, + h_item.item->GetItem()->Name, + h_item.item_id, + used_count, + r_item.item_id, + remaining_requirement + ); + + // If the item is fully consumed, mark it for removal + if (h_item.count == 0) { + items_to_remove.push_back(h_item); + } + } + } + + // If we cannot fulfill the requirement, mark as not met + if (remaining_requirement > 0) { + LogNpcHandinDetail( + "{} >>>> Failed to fulfill requirement for [{}], remaining [{}]", + log_handin_prefix, + r_item.item_id, + remaining_requirement + ); + items_met = false; + break; + } else { + fulfilled = true; + } + } + + // reset the hand-in items to the state prior to processing the hand-in + // if we failed to fulfill the requirement + if (!items_met) { + handin_items = before_handin_state; + items_to_remove.clear(); + } + } + else if (h.items.empty() && r.items.empty()) { // no items required, money only + items_met = true; + } + else { + items_met = false; + } + + requirement_met = money_met && items_met; + + // multi-quest + if (IsMultiQuestEnabled()) { + for (auto &h_item: h.items) { + for (const auto &r_item: r.items) { + if (h_item.item_id == r_item.item_id && h_item.count == r_item.count) { + h_item.is_multiquest_item = true; + } + } + } + } + + // in-case we trigger CheckHand-in multiple times, only set these once + if (!m_handin_started) { + m_handin_started = true; + m_hand_in = h; + // save original items for logging + m_hand_in.original_items = m_hand_in.items; + m_hand_in.original_money = m_hand_in.money; + } + + // check if npc is guildmaster + if (IsGuildmaster()) { + for (const auto &remove_item : items_to_remove) { + if (!remove_item.item) { + continue; + } + + if (!IsDisciplineTome(remove_item.item->GetItem())) { + continue; + } + + if (IsGuildmasterForClient(c)) { + c->TrainDiscipline(remove_item.item->GetID()); + m_hand_in.items.erase( + std::remove_if( + m_hand_in.items.begin(), + m_hand_in.items.end(), + [&](const HandinEntry &i) { + bool removed = i.item == remove_item.item; + if (removed) { + LogNpcHandin( + "{} Hand-in success, removing discipline tome [{}] from hand-in bucket", + log_handin_prefix, + i.item_id + ); + } + return removed; + } + ), + m_hand_in.items.end() + ); + } else { + Say("You are not a member of my guild. I will not train you!"); + requirement_met = false; + break; + } + } + } + + // print current hand-in bucket + LogNpcHandin( + "{} > Before processing hand-in | requirement_met [{}] item_count [{}] platinum [{}] gold [{}] silver [{}] copper [{}]", + log_handin_prefix, + requirement_met, + h.items.size(), + h.money.platinum, + h.money.gold, + h.money.silver, + h.money.copper + ); + + LogNpcHandin( + "{} >> Handed Items | Item(s) ({}) platinum [{}] gold [{}] silver [{}] copper [{}]", + log_handin_prefix, + h.items.size(), + h.money.platinum, + h.money.gold, + h.money.silver, + h.money.copper + ); + + int item_count = 1; + for (const auto &i: h.items) { + LogNpcHandin( + "{} >>> item{} [{}] ({}) count [{}]", + log_handin_prefix, + item_count, + i.item->GetItem()->Name, + i.item_id, + i.count + ); + item_count++; + } + + LogNpcHandin( + "{} >> Required Items | Item(s) ({}) platinum [{}] gold [{}] silver [{}] copper [{}]", + log_handin_prefix, + r.items.size(), + r.money.platinum, + r.money.gold, + r.money.silver, + r.money.copper + ); + + item_count = 1; + for (const auto &i: r.items) { + auto item = database.GetItem(Strings::ToUnsignedInt(i.item_id)); + + LogNpcHandin( + "{} >>> item{} [{}] ({}) count [{}]", + log_handin_prefix, + item_count, + item ? item->Name : "Unknown", + i.item_id, + i.count + ); + + item_count++; + } + + if (requirement_met) { + std::vector log_entries = {}; + for (const auto &remove_item: items_to_remove) { + m_hand_in.items.erase( + std::remove_if( + m_hand_in.items.begin(), + m_hand_in.items.end(), + [&](const HandinEntry &i) { + bool removed = (remove_item.item == i.item); + if (removed) { + log_entries.emplace_back( + fmt::format( + "{} >>> Hand-in success | Removing from hand-in bucket | item [{}] ({}) count [{}]", + log_handin_prefix, + i.item->GetItem()->Name, + i.item_id, + i.count + ) + ); + } + return removed; + } + ), + m_hand_in.items.end() + ); + } + + // log successful hand-in items + if (!log_entries.empty()) { + for (const auto& log : log_entries) { + LogNpcHandin("{}", log); + } + } + + // decrement successful hand-in money from current hand-in bucket + if (h.money.platinum > 0 || h.money.gold > 0 || h.money.silver > 0 || h.money.copper > 0) { + LogNpcHandin( + "{} Hand-in success, removing money p [{}] g [{}] s [{}] c [{}]", + log_handin_prefix, + h.money.platinum, + h.money.gold, + h.money.silver, + h.money.copper + ); + m_hand_in.money.platinum -= h.money.platinum; + m_hand_in.money.gold -= h.money.gold; + m_hand_in.money.silver -= h.money.silver; + m_hand_in.money.copper -= h.money.copper; + } + + LogNpcHandin( + "{} > End of hand-in | requirement_met [{}] item_count [{}] platinum [{}] gold [{}] silver [{}] copper [{}]", + log_handin_prefix, + requirement_met, + m_hand_in.items.size(), + m_hand_in.money.platinum, + m_hand_in.money.gold, + m_hand_in.money.silver, + m_hand_in.money.copper + ); + for (const auto &i: m_hand_in.items) { + LogNpcHandin( + "{} Hand-in success, item [{}] ({}) count [{}]", + log_handin_prefix, + i.item->GetItem()->Name, + i.item_id, + i.count + ); + } + } + + return requirement_met; +} + +NPC::Handin NPC::ReturnHandinItems(Client *c) +{ + // player event + std::vector handin_items; + PlayerEvent::HandinMoney handin_money{}; + std::vector return_items; + PlayerEvent::HandinMoney return_money{}; + for (const auto& i : m_hand_in.original_items) { + if (i.item && i.item->GetItem()) { + handin_items.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = i.item->GetID(), + .item_name = i.item->GetItem()->Name, + .augment_ids = i.item->GetAugmentIDs(), + .augment_names = i.item->GetAugmentNames(), + .charges = std::max(static_cast(i.item->GetCharges()), static_cast(1)) + } + ); + } + } + + auto returned = m_hand_in; + + // check if any money was handed in + if (m_hand_in.original_money.platinum > 0 || + m_hand_in.original_money.gold > 0 || + m_hand_in.original_money.silver > 0 || + m_hand_in.original_money.copper > 0 + ) { + handin_money.copper = m_hand_in.original_money.copper; + handin_money.silver = m_hand_in.original_money.silver; + handin_money.gold = m_hand_in.original_money.gold; + handin_money.platinum = m_hand_in.original_money.platinum; + } + + // if scripts have their own implementation of returning items instead of + // going through return_items, this guards against returning items twice (duplicate items) + bool external_returned_items = c->GetExternalHandinItemsReturned().size() > 0; + bool returned_items_already = false; + for (auto &handin_item: m_hand_in.items) { + for (auto &i: c->GetExternalHandinItemsReturned()) { + auto item = database.GetItem(i); + if (item && std::to_string(item->ID) == handin_item.item_id) { + LogNpcHandin(" -- External quest methods already returned item [{}] ({})", item->Name, item->ID); + returned_items_already = true; + } + } + } + + if (returned_items_already) { + LogNpcHandin("External quest methods returned items, not returning items to player via ReturnHandinItems"); + } + + bool returned_handin = false; + m_hand_in.items.erase( + std::remove_if( + m_hand_in.items.begin(), + m_hand_in.items.end(), + [&](HandinEntry &i) { + if (i.item && i.item->GetItem() && !i.is_multiquest_item && !returned_items_already) { + return_items.emplace_back( + PlayerEvent::HandinEntry{ + .item_id = i.item->GetID(), + .item_name = i.item->GetItem()->Name, + .augment_ids = i.item->GetAugmentIDs(), + .augment_names = i.item->GetAugmentNames(), + .charges = std::max(static_cast(i.item->GetCharges()), static_cast(1)) + } + ); + + // If the item is stackable and the new charges don't match the original count + // set the charges to the original count + if (i.item->IsStackable() && i.item->GetCharges() != i.count) { + i.item->SetCharges(i.count); + } + + c->PushItemOnCursor(*i.item, true); + LogNpcHandin("Hand-in failed, returning item [{}]", i.item->GetItem()->Name); + + returned_handin = true; + return true; // Mark this item for removal + } + return false; + } + ), + m_hand_in.items.end() + ); + + // check if any money was handed in via external quest methods + auto em = c->GetExternalHandinMoneyReturned(); + + bool money_returned_via_external_quest_methods = + em.copper > 0 || + em.silver > 0 || + em.gold > 0 || + em.platinum > 0; + + // check if any money was handed in + bool money_handed = m_hand_in.money.platinum > 0 || + m_hand_in.money.gold > 0 || + m_hand_in.money.silver > 0 || + m_hand_in.money.copper > 0; + if (money_handed && !money_returned_via_external_quest_methods) { + c->AddMoneyToPP( + m_hand_in.money.copper, + m_hand_in.money.silver, + m_hand_in.money.gold, + m_hand_in.money.platinum, + true + ); + returned_handin = true; + LogNpcHandin( + "Hand-in failed, returning money p [{}] g [{}] s [{}] c [{}]", + m_hand_in.money.platinum, + m_hand_in.money.gold, + m_hand_in.money.silver, + m_hand_in.money.copper + ); + + // player event + return_money.copper = m_hand_in.money.copper; + return_money.silver = m_hand_in.money.silver; + return_money.gold = m_hand_in.money.gold; + return_money.platinum = m_hand_in.money.platinum; + } + + if (money_returned_via_external_quest_methods) { + LogNpcHandin( + "Money handed in was returned via external quest methods, not returning money to player via ReturnHandinItems | handed-in p [{}] g [{}] s [{}] c [{}] returned-external p [{}] g [{}] s [{}] c [{}] source [{}]", + m_hand_in.money.platinum, + m_hand_in.money.gold, + m_hand_in.money.silver, + m_hand_in.money.copper, + em.platinum, + em.gold, + em.silver, + em.copper, + em.return_source + ); + } + + m_has_processed_handin_return = returned_handin; + + if (returned_handin) { + Say( + fmt::format( + "I have no need for this {}, you can have it back.", + c->GetCleanName() + ).c_str() + ); + } + + const bool handed_in_money = ( + handin_money.platinum > 0 || + handin_money.gold > 0 || + handin_money.silver > 0 || + handin_money.copper > 0 + ); + const bool event_has_data_to_record = !handin_items.empty() || handed_in_money; + + if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) { + auto e = PlayerEvent::HandinEvent{ + .npc_id = GetNPCTypeID(), + .npc_name = GetCleanName(), + .handin_items = handin_items, + .handin_money = handin_money, + .return_items = return_items, + .return_money = return_money, + .is_quest_handin = parse->HasQuestSub(GetNPCTypeID(), EVENT_TRADE) + }; + + RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e); + } + + return returned; +} + +void NPC::ResetHandin() +{ + m_has_processed_handin_return = false; + m_handin_started = false; + if (!IsMultiQuestEnabled()) { + for (auto &i: m_hand_in.original_items) { + safe_delete(i.item); + } + + m_hand_in = {}; + } +} diff --git a/zone/npc.h b/zone/npc.h index 9b17afaba..1b958dab9 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -559,6 +559,46 @@ public: bool CanPathTo(float x, float y, float z); void DoNpcToNpcAggroScan(); + + // hand-ins + bool CanPetTakeItem(const EQ::ItemInstance *inst); + + struct HandinEntry { + std::string item_id = "0"; + uint32 count = 0; + EQ::ItemInstance *item = nullptr; + bool is_multiquest_item = false; // state + }; + + struct HandinMoney { + uint32 platinum = 0; + uint32 gold = 0; + uint32 silver = 0; + uint32 copper = 0; + }; + + struct Handin { + std::vector original_items = {}; // this is what the player originally handed in, never modified + std::vector items = {}; // items can be removed from this set as successful handins are made + HandinMoney original_money = {}; // this is what the player originally handed in, never modified + HandinMoney money = {}; // money can be removed from this set as successful handins are made + }; + + // NPC Hand-in + bool IsMultiQuestEnabled() { return m_multiquest_enabled; } + void MultiQuestEnable() { m_multiquest_enabled = true; } + bool IsGuildmasterForClient(Client *c); + bool CheckHandin( + Client *c, + std::map handin, + std::map required, + std::vector items + ); + Handin ReturnHandinItems(Client *c); + void ResetHandin(); + bool HasProcessedHandinReturn() { return m_has_processed_handin_return; } + bool HandinStarted() { return m_handin_started; } + protected: void HandleRoambox(); @@ -700,6 +740,15 @@ protected: bool raid_target; bool ignore_despawn; //NPCs with this set to 1 will ignore the despawn value in spawngroup + // NPC Hand-in + bool m_multiquest_enabled = false; + bool m_handin_started = false; + bool m_has_processed_handin_return = false; + + // this is the working handin data from the player + // items can be decremented from this as each successful + // check is ran in scripts, the remainder is what is returned + Handin m_hand_in = {}; private: uint32 m_loottable_id; diff --git a/zone/perl_npc.cpp b/zone/perl_npc.cpp index ce3d8ba17..7ebe49eb2 100644 --- a/zone/perl_npc.cpp +++ b/zone/perl_npc.cpp @@ -796,6 +796,85 @@ void Perl_NPC_DescribeSpecialAbilities(NPC* self, Client* c) self->DescribeSpecialAbilities(c); } +bool Perl_NPC_IsMultiQuestEnabled(NPC* self) +{ + return self->IsMultiQuestEnabled(); +} + +void Perl_NPC_MultiQuestEnable(NPC* self) +{ + self->MultiQuestEnable(); +} + +bool Perl_NPC_CheckHandin( + NPC* self, + Client* c, + perl::reference handin_ref, + perl::reference required_ref, + perl::array items_ref +) +{ + perl::hash handin = handin_ref; + perl::hash required = required_ref; + + std::map handin_map; + std::map required_map; + std::vector items; + + for (auto e: handin) { + if (!e.first) { + continue; + } + + if (Strings::EqualFold(e.first, "0")) { + continue; + } + + LogNpcHandinDetail("Handin key [{}] value [{}]", e.first, handin.at(e.first).c_str()); + + const uint32 count = static_cast(handin.at(e.first)); + handin_map[e.first] = count; + } + + for (auto e: required) { + if (!e.first) { + continue; + } + + if (Strings::EqualFold(e.first, "0")) { + continue; + } + + LogNpcHandinDetail("Required key [{}] value [{}]", e.first, required.at(e.first).c_str()); + + const uint32 count = static_cast(required.at(e.first)); + required_map[e.first] = count; + } + + for (auto e : items_ref) { + EQ::ItemInstance* i = static_cast(e); + if (!i) { + continue; + } + + items.emplace_back(i); + + LogNpcHandinDetail( + "Item instance [{}] ({}) UUID ({}) added to handin list", + i->GetItem()->Name, + i->GetItem()->ID, + i->GetSerialNumber() + ); + } + + return self->CheckHandin(c, handin_map, required_map, items); +} + +void Perl_NPC_ReturnHandinItems(NPC *self, Client* c) +{ + self->ReturnHandinItems(c); +} + void perl_register_npc() { perl::interpreter perl(PERL_GET_THX); @@ -827,6 +906,7 @@ void perl_register_npc() package.add("CalculateNewWaypoint", &Perl_NPC_CalculateNewWaypoint); package.add("ChangeLastName", &Perl_NPC_ChangeLastName); package.add("CheckNPCFactionAlly", &Perl_NPC_CheckNPCFactionAlly); + package.add("CheckHandin", &Perl_NPC_CheckHandin); package.add("ClearItemList", &Perl_NPC_ClearLootItems); package.add("ClearLastName", &Perl_NPC_ClearLastName); package.add("CountItem", &Perl_NPC_CountItem); @@ -893,6 +973,7 @@ void perl_register_npc() package.add("IsLDoNLocked", &Perl_NPC_IsLDoNLocked); package.add("IsLDoNTrapped", &Perl_NPC_IsLDoNTrapped); package.add("IsLDoNTrapDetected", &Perl_NPC_IsLDoNTrapDetected); + package.add("IsMultiQuestEnabled", &Perl_NPC_IsMultiQuestEnabled); package.add("IsOnHatelist", &Perl_NPC_IsOnHatelist); package.add("IsRaidTarget", &Perl_NPC_IsRaidTarget); package.add("IsRareSpawn", &Perl_NPC_IsRareSpawn); @@ -904,6 +985,7 @@ void perl_register_npc() package.add("MoveTo", (void(*)(NPC*, float, float, float))&Perl_NPC_MoveTo); package.add("MoveTo", (void(*)(NPC*, float, float, float, float))&Perl_NPC_MoveTo); package.add("MoveTo", (void(*)(NPC*, float, float, float, float, bool))&Perl_NPC_MoveTo); + package.add("MultiQuestEnable", &Perl_NPC_MultiQuestEnable); package.add("NextGuardPosition", &Perl_NPC_NextGuardPosition); package.add("PauseWandering", &Perl_NPC_PauseWandering); package.add("PickPocket", &Perl_NPC_PickPocket); @@ -920,6 +1002,7 @@ void perl_register_npc() package.add("RemoveMeleeProc", &Perl_NPC_RemoveMeleeProc); package.add("RemoveRangedProc", &Perl_NPC_RemoveRangedProc); package.add("ResumeWandering", &Perl_NPC_ResumeWandering); + package.add("ReturnHandinItems", &Perl_NPC_ReturnHandinItems); package.add("SaveGuardSpot", (void(*)(NPC*))&Perl_NPC_SaveGuardSpot); package.add("SaveGuardSpot", (void(*)(NPC*, bool))&Perl_NPC_SaveGuardSpot); package.add("SaveGuardSpot", (void(*)(NPC*, float, float, float, float))&Perl_NPC_SaveGuardSpot); diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index 257f088e6..310347285 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -1223,50 +1223,7 @@ bool QuestManager::isdisctome(uint32 item_id) { return false; } - if (!item->IsClassCommon() || item->ItemType != EQ::item::ItemTypeSpell) { - return false; - } - - //Need a way to determine the difference between a spell and a tome - //so they cant turn in a spell and get it as a discipline - //this is kinda a hack: - - const std::string item_name = item->Name; - - if ( - !Strings::BeginsWith(item_name, "Tome of ") && - !Strings::BeginsWith(item_name, "Skill: ") - ) { - return false; - } - - //we know for sure none of the int casters get disciplines - uint32 class_bit = 0; - class_bit |= 1 << (Class::Wizard - 1); - class_bit |= 1 << (Class::Enchanter - 1); - class_bit |= 1 << (Class::Magician - 1); - class_bit |= 1 << (Class::Necromancer - 1); - if (item->Classes & class_bit) { - return false; - } - - const auto& spell_id = static_cast(item->Scroll.Effect); - if (!IsValidSpell(spell_id)) { - return false; - } - - //we know for sure none of the int casters get disciplines - const auto& spell = spells[spell_id]; - if( - spell.classes[Class::Wizard - 1] != 255 && - spell.classes[Class::Enchanter - 1] != 255 && - spell.classes[Class::Magician - 1] != 255 && - spell.classes[Class::Necromancer - 1] != 255 - ) { - return false; - } - - return true; + return IsDisciplineTome(item); } std::string QuestManager::getracename(uint16 race_id) { diff --git a/zone/trading.cpp b/zone/trading.cpp index 48d2d8b26..c2c04645b 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -320,7 +320,11 @@ void Client::ResetTrade() { } void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, std::list* 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(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(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(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(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 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 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 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); } } } diff --git a/zone/zone_cli.cpp b/zone/zone_cli.cpp index 5739aa6ca..15f1d91a2 100644 --- a/zone/zone_cli.cpp +++ b/zone/zone_cli.cpp @@ -12,6 +12,11 @@ bool ZoneCLI::RanSidecarCommand(int argc, char **argv) return argc > 1 && (strstr(argv[1], "sidecar:") != nullptr); } +bool ZoneCLI::RanTestCommand(int argc, char **argv) +{ + return argc > 1 && (strstr(argv[1], "tests:") != nullptr); +} + void ZoneCLI::CommandHandler(int argc, char **argv) { if (argc == 1) { return; } @@ -25,8 +30,10 @@ void ZoneCLI::CommandHandler(int argc, char **argv) // Register commands function_map["sidecar:serve-http"] = &ZoneCLI::SidecarServeHttp; + function_map["tests:npc-handins"] = &ZoneCLI::NpcHandins; EQEmuCommand::HandleMenu(function_map, cmd, argc, argv); } #include "cli/sidecar_serve_http.cpp" +#include "cli/npc_handins.cpp" diff --git a/zone/zone_cli.h b/zone/zone_cli.h index e9ed18367..9ef4c076d 100644 --- a/zone/zone_cli.h +++ b/zone/zone_cli.h @@ -9,6 +9,8 @@ public: static void SidecarServeHttp(int argc, char **argv, argh::parser &cmd, std::string &description); static bool RanConsoleCommand(int argc, char **argv); static bool RanSidecarCommand(int argc, char **argv); + static bool RanTestCommand(int argc, char **argv); + static void NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description); }; diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 4000397a3..455a45ed5 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -1905,6 +1905,7 @@ const NPCType *ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load t->heroic_strikethrough = n.heroic_strikethrough; t->faction_amount = n.faction_amount; t->keeps_sold_items = n.keeps_sold_items; + t->multiquest_enabled = n.multiquest_enabled != 0; // If NPC with duplicate NPC id already in table, // free item we attempted to add. diff --git a/zone/zonedump.h b/zone/zonedump.h index 52dbb5b3e..94adecf96 100644 --- a/zone/zonedump.h +++ b/zone/zonedump.h @@ -156,6 +156,7 @@ struct NPCType bool keeps_sold_items; bool is_parcel_merchant; uint8 greed; + bool multiquest_enabled; }; #pragma pack()