mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 12:41:30 +00:00
[Items] Overhaul Item Hand-in System (#4593)
* [Items] Overhaul Item Hand-in System * Edge case lua fix * Merge fix * I'm going to be amazed if this works first try * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Update linux-build.sh * Add protections against scripts that hand back items themselves * Remove EVENT_ITEM_ScriptStopReturn * test * Update npc_handins.cpp * Add Items:AlwaysReturnHandins * Update spdat.cpp * Bypass update prompt on CI
This commit is contained in:
parent
d1d6db3a09
commit
6fb919a16f
@ -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");
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -105,6 +105,8 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults()
|
||||
log_settings[Logs::QuestErrors].log_to_console = static_cast<uint8>(Logs::General);
|
||||
log_settings[Logs::EqTime].log_to_console = static_cast<uint8>(Logs::General);
|
||||
log_settings[Logs::EqTime].log_to_gmsay = static_cast<uint8>(Logs::General);
|
||||
log_settings[Logs::NpcHandin].log_to_console = static_cast<uint8>(Logs::General);
|
||||
log_settings[Logs::NpcHandin].log_to_gmsay = static_cast<uint8>(Logs::General);
|
||||
|
||||
/**
|
||||
* RFC 5424
|
||||
|
||||
@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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__);\
|
||||
|
||||
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1785,6 +1785,18 @@ std::vector<uint32> EQ::ItemInstance::GetAugmentIDs() const
|
||||
return augments;
|
||||
}
|
||||
|
||||
std::vector<std::string> EQ::ItemInstance::GetAugmentNames() const
|
||||
{
|
||||
std::vector<std::string> 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;
|
||||
|
||||
@ -305,6 +305,7 @@ namespace EQ
|
||||
int GetItemSkillsStat(EQ::skills::SkillType skill, bool augments = false) const;
|
||||
uint32 GetItemGuildFavor() const;
|
||||
std::vector<uint32> GetAugmentIDs() const;
|
||||
std::vector<std::string> GetAugmentNames() const;
|
||||
static void AddGUIDToMap(uint64 existing_serial_number);
|
||||
static void ClearGUIDMap();
|
||||
|
||||
|
||||
@ -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<int8_t>(atoi(row[101])) : 0;
|
||||
e.feettexture = row[102] ? static_cast<int8_t>(atoi(row[102])) : 0;
|
||||
e.light = row[103] ? static_cast<int8_t>(atoi(row[103])) : 0;
|
||||
e.walkspeed = row[104] ? static_cast<int8_t>(atoi(row[104])) : 0;
|
||||
e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0;
|
||||
e.peqid = row[105] ? static_cast<int32_t>(atoi(row[105])) : 0;
|
||||
e.unique_ = row[106] ? static_cast<int8_t>(atoi(row[106])) : 0;
|
||||
e.fixed = row[107] ? static_cast<int8_t>(atoi(row[107])) : 0;
|
||||
@ -756,6 +760,7 @@ public:
|
||||
e.faction_amount = row[126] ? static_cast<int32_t>(atoi(row[126])) : 0;
|
||||
e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1;
|
||||
e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0;
|
||||
e.multiquest_enabled = row[129] ? static_cast<uint8_t>(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<int8_t>(atoi(row[101])) : 0;
|
||||
e.feettexture = row[102] ? static_cast<int8_t>(atoi(row[102])) : 0;
|
||||
e.light = row[103] ? static_cast<int8_t>(atoi(row[103])) : 0;
|
||||
e.walkspeed = row[104] ? static_cast<int8_t>(atoi(row[104])) : 0;
|
||||
e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0;
|
||||
e.peqid = row[105] ? static_cast<int32_t>(atoi(row[105])) : 0;
|
||||
e.unique_ = row[106] ? static_cast<int8_t>(atoi(row[106])) : 0;
|
||||
e.fixed = row[107] ? static_cast<int8_t>(atoi(row[107])) : 0;
|
||||
@ -1387,6 +1395,7 @@ public:
|
||||
e.faction_amount = row[126] ? static_cast<int32_t>(atoi(row[126])) : 0;
|
||||
e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1;
|
||||
e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0;
|
||||
e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0;
|
||||
|
||||
all_entries.push_back(e);
|
||||
}
|
||||
@ -1515,7 +1524,7 @@ public:
|
||||
e.legtexture = row[101] ? static_cast<int8_t>(atoi(row[101])) : 0;
|
||||
e.feettexture = row[102] ? static_cast<int8_t>(atoi(row[102])) : 0;
|
||||
e.light = row[103] ? static_cast<int8_t>(atoi(row[103])) : 0;
|
||||
e.walkspeed = row[104] ? static_cast<int8_t>(atoi(row[104])) : 0;
|
||||
e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0;
|
||||
e.peqid = row[105] ? static_cast<int32_t>(atoi(row[105])) : 0;
|
||||
e.unique_ = row[106] ? static_cast<int8_t>(atoi(row[106])) : 0;
|
||||
e.fixed = row[107] ? static_cast<int8_t>(atoi(row[107])) : 0;
|
||||
@ -1540,6 +1549,7 @@ public:
|
||||
e.faction_amount = row[126] ? static_cast<int32_t>(atoi(row[126])) : 0;
|
||||
e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1;
|
||||
e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0;
|
||||
e.multiquest_enabled = row[129] ? static_cast<uint8_t>(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) + ")");
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<uint32>(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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/
|
||||
|
||||
|
||||
90
utils/scripts/build/source-db-build.sh
Normal file
90
utils/scripts/build/source-db-build.sh
Normal file
@ -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 <<EOF
|
||||
FLUSH PRIVILEGES;
|
||||
ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('$ROOT_PASSWORD');
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '$ROOT_PASSWORD' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
|
||||
# Stop MariaDB safe mode
|
||||
echo "Stopping MariaDB safe mode..."
|
||||
sudo killall mysqld
|
||||
|
||||
# Configure MariaDB to allow remote connections (optional)
|
||||
echo "Configuring MariaDB to allow remote connections..."
|
||||
sudo sed -i "s/^bind-address.*/bind-address = 0.0.0.0/" $MARIADB_CONFIG
|
||||
|
||||
# Restart MariaDB service
|
||||
echo "Restarting MariaDB service..."
|
||||
sudo systemctl restart mariadb
|
||||
|
||||
# Test connection
|
||||
echo "Testing MariaDB connection..."
|
||||
mysql -u root -p"$ROOT_PASSWORD" -e "SELECT VERSION();"
|
||||
|
||||
# Display completion message
|
||||
echo "MariaDB setup completed!"
|
||||
echo "Root password: $ROOT_PASSWORD"
|
||||
|
||||
# Set Database Credentials
|
||||
DB_USER="root"
|
||||
DB_PASS="eqemu"
|
||||
DB_HOST="localhost"
|
||||
DB_NAME="peq"
|
||||
SQL_DIR="/tmp/db/peq-dump"
|
||||
|
||||
# Download the latest database dump
|
||||
echo "Downloading the latest PEQ database dump..."
|
||||
curl -s http://db.projecteq.net/api/v1/dump/latest -o /tmp/db.zip
|
||||
|
||||
# Unzip the database dump
|
||||
echo "Extracting the database dump..."
|
||||
unzip -o /tmp/db.zip -d /tmp/db/
|
||||
|
||||
# Ensure MariaDB is running
|
||||
echo "Ensuring MariaDB is running..."
|
||||
sudo systemctl start mariadb
|
||||
|
||||
# Wait for MariaDB to be ready
|
||||
echo "Waiting for MariaDB to be ready..."
|
||||
while ! mysqladmin ping -u${DB_USER} -p${DB_PASS} -h${DB_HOST} --silent; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Create the peq database
|
||||
echo "Creating the '${DB_NAME}' database..."
|
||||
mysql -u${DB_USER} -p${DB_PASS} -h${DB_HOST} -e "DROP DATABASE IF EXISTS ${DB_NAME}; CREATE DATABASE ${DB_NAME};"
|
||||
|
||||
# Parallelize the import process
|
||||
echo "Importing tables in parallel..."
|
||||
ls /tmp/db/peq-dump/create_tables_*.sql | xargs -P 4 -I {} sh -c "mysql -u${DB_USER} -p${DB_PASS} -h${DB_HOST} ${DB_NAME} < {}"
|
||||
|
||||
# Clean up temporary files
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -rf /tmp/db/
|
||||
rm -rf ${COMBINED_DIR}
|
||||
|
||||
echo "Database import complete!"
|
||||
@ -2515,6 +2515,17 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsMultiQuestEnabled()) {
|
||||
for (auto &i: m_hand_in.items) {
|
||||
if (i.is_multiquest_item && i.item->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 };
|
||||
|
||||
|
||||
514
zone/cli/npc_handins.cpp
Normal file
514
zone/cli/npc_handins.cpp
Normal file
@ -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<HandinEntry> 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<TestCase> 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<std::string, uint32> hand_ins;
|
||||
std::map<std::string, uint32> required;
|
||||
std::vector<EQ::ItemInstance *> 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");
|
||||
}
|
||||
}
|
||||
585
zone/client.cpp
585
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<PlayerEvent::HandinEntry> hi = {};
|
||||
std::vector<PlayerEvent::HandinEntry> 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<uint16>(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<uint16>(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<uint16>(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<uint16>(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<uint32>(Strings::ToUnsignedInt(rms[0]));
|
||||
rm.silver = static_cast<uint32>(Strings::ToUnsignedInt(rms[1]));
|
||||
rm.gold = static_cast<uint32>(Strings::ToUnsignedInt(rms[2]));
|
||||
rm.platinum = static_cast<uint32>(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<uint16>(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<uint16>(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;
|
||||
|
||||
@ -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<uint32_t> m_external_handin_items_returned = {};
|
||||
public:
|
||||
ExternalHandinMoneyReturned GetExternalHandinMoneyReturned() { return m_external_handin_money_returned; }
|
||||
std::vector<uint32_t> 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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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_<Lua_ItemInst>("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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<uint32> 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<std::string, uint32> handin_map;
|
||||
std::map<std::string, uint32> required_map;
|
||||
std::vector<EQ::ItemInstance *> 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<std::string>(i.key());
|
||||
}
|
||||
else if (luabind::type(i.key()) == LUA_TNUMBER) {
|
||||
key = fmt::format("{}", luabind::object_cast<int>(i.key()));
|
||||
}
|
||||
else {
|
||||
LogError("Handin key type [{}] not supported", luabind::type(i.key()));
|
||||
}
|
||||
|
||||
if (!key.empty()) {
|
||||
handin_map[key] = luabind::object_cast<uint32>(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<std::string>(i.key());
|
||||
}
|
||||
else if (luabind::type(i.key()) == LUA_TNUMBER) {
|
||||
key = fmt::format("{}", luabind::object_cast<int>(i.key()));
|
||||
}
|
||||
else {
|
||||
LogError("Required key type [{}] not supported", luabind::type(i.key()));
|
||||
}
|
||||
|
||||
if (!key.empty()) {
|
||||
required_map[key] = luabind::object_cast<uint32>(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<Lua_ItemInst>(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_<Lua_NPC, Lua_Mob>("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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -56,6 +56,11 @@ void handle_npc_event_trade(
|
||||
uint32 extra_data,
|
||||
std::vector<std::any> *extra_pointers
|
||||
) {
|
||||
Lua_NPC l_npc(reinterpret_cast<NPC*>(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<Client *>(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");
|
||||
|
||||
@ -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<std::string>& 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;
|
||||
}
|
||||
|
||||
31
zone/mob.cpp
31
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
618
zone/npc.cpp
618
zone/npc.cpp
@ -49,6 +49,7 @@
|
||||
|
||||
#include "bot.h"
|
||||
#include "../common/skill_caps.h"
|
||||
#include "../common/events/player_event_logs.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
@ -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<uint8, uint8> 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<std::string, uint32> handin,
|
||||
std::map<std::string, uint32> required,
|
||||
std::vector<EQ::ItemInstance *> 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<std::pair<const std::map<std::string, uint32>&, 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<uint16>(i->IsStackable() ? i->GetCharges() : 1), static_cast<uint16>(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<HandinEntry> 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<HandinEntry> 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<std::string> 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<PlayerEvent::HandinEntry> handin_items;
|
||||
PlayerEvent::HandinMoney handin_money{};
|
||||
std::vector<PlayerEvent::HandinEntry> 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<uint16>(i.item->GetCharges()), static_cast<uint16>(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<uint16>(i.item->GetCharges()), static_cast<uint16>(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 = {};
|
||||
}
|
||||
}
|
||||
|
||||
49
zone/npc.h
49
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<HandinEntry> original_items = {}; // this is what the player originally handed in, never modified
|
||||
std::vector<HandinEntry> 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<std::string, uint32> handin,
|
||||
std::map<std::string, uint32> required,
|
||||
std::vector<EQ::ItemInstance *> 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;
|
||||
|
||||
@ -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<std::string, uint32> handin_map;
|
||||
std::map<std::string, uint32> required_map;
|
||||
std::vector<EQ::ItemInstance *> 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<uint32>(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<uint32>(required.at(e.first));
|
||||
required_map[e.first] = count;
|
||||
}
|
||||
|
||||
for (auto e : items_ref) {
|
||||
EQ::ItemInstance* i = static_cast<EQ::ItemInstance*>(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);
|
||||
|
||||
@ -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<uint32>(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) {
|
||||
|
||||
249
zone/trading.cpp
249
zone/trading.cpp
@ -320,7 +320,11 @@ void Client::ResetTrade() {
|
||||
}
|
||||
|
||||
void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, std::list<void*>* event_details) {
|
||||
if(tradingWith && tradingWith->IsClient()) {
|
||||
if (!tradingWith) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tradingWith->IsClient()) {
|
||||
Client * other = tradingWith->CastToClient();
|
||||
PlayerLogTrade_Struct * qs_audit = nullptr;
|
||||
bool qs_log = false;
|
||||
@ -366,7 +370,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
inst->GetItem()->NoDrop != 0 ||
|
||||
CanTradeFVNoDropItem() ||
|
||||
other == this
|
||||
) {
|
||||
) {
|
||||
int16 free_slot = other->GetInv().FindFreeSlotForTradeItem(inst);
|
||||
|
||||
if (free_slot != INVALID_INDEX) {
|
||||
@ -481,8 +485,12 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
LogTrading("Transferring partial stack [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName());
|
||||
|
||||
if (other->PutItemInInventory(partial_slot, *partial_inst, true)) {
|
||||
LogTrading("Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot",
|
||||
inst->GetItem()->Name, inst->GetItem()->ID, (old_charges - inst->GetCharges()));
|
||||
LogTrading(
|
||||
"Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot",
|
||||
inst->GetItem()->Name,
|
||||
inst->GetItem()->ID,
|
||||
(old_charges - inst->GetCharges())
|
||||
);
|
||||
inst->TransferOwnership(database, other->CharacterID());
|
||||
if (qs_log) {
|
||||
auto detail = new PlayerLogTradeItemsEntry_Struct;
|
||||
@ -509,7 +517,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
}
|
||||
else {
|
||||
LogTrading("Transfer of partial stack [{}] ([{}]) to [{}] failed, returning [{}] charges to trade slot",
|
||||
inst->GetItem()->Name, inst->GetItem()->ID, other->GetName(), (old_charges - inst->GetCharges()));
|
||||
inst->GetItem()->Name, inst->GetItem()->ID, other->GetName(), (old_charges - inst->GetCharges()));
|
||||
|
||||
inst->SetCharges(old_charges);
|
||||
partial_inst->SetCharges(partial_charges);
|
||||
@ -666,8 +674,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
//Do not reset the trade here, done by the caller.
|
||||
}
|
||||
}
|
||||
else if(tradingWith && tradingWith->IsNPC()) {
|
||||
NPCHandinEventLog(trade, tradingWith->CastToNPC());
|
||||
else if(tradingWith->IsNPC()) {
|
||||
|
||||
QSPlayerLogHandin_Struct* qs_audit = nullptr;
|
||||
bool qs_log = false;
|
||||
@ -744,7 +751,6 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
|
||||
bool quest_npc = false;
|
||||
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
|
||||
// This is a quest NPC
|
||||
quest_npc = true;
|
||||
}
|
||||
|
||||
@ -760,34 +766,14 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
|
||||
if (RuleB(TaskSystem, EnableTaskSystem)) {
|
||||
if (UpdateTasksOnDeliver(items, *trade, tradingWith->CastToNPC())) {
|
||||
if (!tradingWith->IsMoving())
|
||||
if (!tradingWith->IsMoving()) {
|
||||
tradingWith->FaceTarget(this);
|
||||
|
||||
EVENT_ITEM_ScriptStopReturn();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of quest or non-quest NPC - No in combat trade completion
|
||||
// is allowed.
|
||||
if (tradingWith->CheckAggro(this))
|
||||
{
|
||||
for (EQ::ItemInstance* inst : items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
}
|
||||
|
||||
items.clear();
|
||||
}
|
||||
// Only enforce trade rules if the NPC doesn't have an EVENT_TRADE
|
||||
// subroutine. That overrides all.
|
||||
else if (!quest_npc)
|
||||
{
|
||||
for (EQ::ItemInstance* inst : items) {
|
||||
|
||||
if (!quest_npc) {
|
||||
for (auto &inst: items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
@ -801,128 +787,121 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
|
||||
}
|
||||
}
|
||||
|
||||
const EQ::ItemData* item = inst->GetItem();
|
||||
const bool is_pet = _CLIENTPET(tradingWith) && tradingWith->GetPetType()<=petOther;
|
||||
const bool is_quest_npc = tradingWith->CastToNPC()->IsQuestNPC();
|
||||
const bool restrict_quest_items_to_quest_npc = RuleB(NPC, ReturnQuestItemsFromNonQuestNPCs);
|
||||
const bool pets_can_take_quest_items = RuleB(Pets, CanTakeQuestItems);
|
||||
const bool is_pet_and_can_have_nodrop_items = (RuleB(Pets, CanTakeNoDrop) && is_pet);
|
||||
const bool is_pet_and_can_have_quest_items = (pets_can_take_quest_items && is_pet);
|
||||
// if it was not a NO DROP or Attuned item (or if a GM is trading), let the NPC have it
|
||||
if (GetGM() ||
|
||||
(!restrict_quest_items_to_quest_npc || (is_quest_npc && item->IsQuestItem()) || !item->IsQuestItem()) && // If rule is enabled, return any quest items given to non-quest NPCs
|
||||
(((item->NoDrop != 0 && !inst->IsAttuned()) || is_pet_and_can_have_nodrop_items) &&
|
||||
((!item->IsQuestItem() || is_pet_and_can_have_quest_items || !is_pet)))) {
|
||||
auto with = tradingWith->CastToNPC();
|
||||
const EQ::ItemData *item = inst->GetItem();
|
||||
|
||||
if (with->IsPetOwnerClient() && with->CanPetTakeItem(inst)) {
|
||||
// pets need to look inside bags and try to equip items found there
|
||||
if (item->IsClassBag() && item->BagSlots > 0) {
|
||||
for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) {
|
||||
// if an item inside the bag can't be given to the pet, keep the bag
|
||||
bool keep_bag = false;
|
||||
int item_count = 0;
|
||||
for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) {
|
||||
const EQ::ItemInstance *baginst = inst->GetItem(bslot);
|
||||
if (baginst) {
|
||||
const EQ::ItemData *bagitem = baginst->GetItem();
|
||||
if (bagitem && (GetGM() ||
|
||||
(!restrict_quest_items_to_quest_npc ||
|
||||
(is_quest_npc && bagitem->IsQuestItem()) || !bagitem->IsQuestItem()) &&
|
||||
// If rule is enabled, return any quest items given to non-quest NPCs (inside bags)
|
||||
(bagitem->NoDrop != 0 && !baginst->IsAttuned()) &&
|
||||
((is_pet && (!bagitem->IsQuestItem() || pets_can_take_quest_items) ||
|
||||
!is_pet)))) {
|
||||
|
||||
if (GetGM()) {
|
||||
const std::string& item_link = database.CreateItemLink(bagitem->ID);
|
||||
Message(
|
||||
Chat::White,
|
||||
fmt::format(
|
||||
"Your GM flag allows you to give {} to {}.",
|
||||
item_link,
|
||||
GetTargetDescription(tradingWith)
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(baginst->GetCharges());
|
||||
|
||||
tradingWith->CastToNPC()->AddLootDrop(
|
||||
bagitem,
|
||||
lde,
|
||||
true
|
||||
);
|
||||
// Return quest items being traded to non-quest NPC when the rule is true
|
||||
} else if (restrict_quest_items_to_quest_npc && (!is_quest_npc && bagitem->IsQuestItem())) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*baginst, true);
|
||||
Message(Chat::Red, "You can only trade quest items to quest NPCs.");
|
||||
// Return quest items being traded to player pet when not allowed
|
||||
} else if (is_pet && bagitem->IsQuestItem() && !pets_can_take_quest_items) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*baginst, true);
|
||||
Message(Chat::Red, "You cannot trade quest items with your pet.");
|
||||
} else if (RuleB(NPC, ReturnNonQuestNoDropItems)) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*baginst, true);
|
||||
}
|
||||
if (baginst && baginst->GetItem() && with->CanPetTakeItem(baginst)) {
|
||||
// add item to pet's inventory
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(baginst->GetCharges());
|
||||
with->AddLootDrop(baginst->GetItem(), lde, true);
|
||||
inst->DeleteItem(bslot);
|
||||
item_count++;
|
||||
}
|
||||
else {
|
||||
keep_bag = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// add item to pet's inventory
|
||||
if (!keep_bag || item_count == 0) {
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(inst->GetCharges());
|
||||
with->AddLootDrop(item, lde, true);
|
||||
inst = nullptr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// add item to pet's inventory
|
||||
auto lde = LootdropEntriesRepository::NewNpcEntity();
|
||||
lde.equip_item = 1;
|
||||
lde.item_charges = static_cast<int8>(inst->GetCharges());
|
||||
|
||||
tradingWith->CastToNPC()->AddLootDrop(
|
||||
item,
|
||||
lde,
|
||||
true
|
||||
);
|
||||
with->AddLootDrop(item, lde, true);
|
||||
inst = nullptr;
|
||||
}
|
||||
}
|
||||
// Return quest items being traded to non-quest NPC when the rule is true
|
||||
else if (restrict_quest_items_to_quest_npc && (!is_quest_npc && item->IsQuestItem())) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
Message(Chat::Red, "You can only trade quest items to quest NPCs.");
|
||||
}
|
||||
// Return quest items being traded to player pet when not allowed
|
||||
else if (is_pet && item->IsQuestItem()) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
Message(Chat::Red, "You cannot trade quest items with your pet.");
|
||||
}
|
||||
// Return NO DROP and Attuned items being handed into a non-quest NPC if the rule is true
|
||||
else if (RuleB(NPC, ReturnNonQuestNoDropItems)) {
|
||||
tradingWith->SayString(TRADE_BACK, GetCleanName());
|
||||
PushItemOnCursor(*inst, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char temp1[100] = { 0 };
|
||||
char temp2[100] = { 0 };
|
||||
snprintf(temp1, 100, "copper.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->cp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
snprintf(temp1, 100, "silver.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->sp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
snprintf(temp1, 100, "gold.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->gp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
snprintf(temp1, 100, "platinum.%d", tradingWith->GetNPCTypeID());
|
||||
snprintf(temp2, 100, "%u", trade->pp);
|
||||
parse->AddVar(temp1, temp2);
|
||||
std::string currencies[] = {"copper", "silver", "gold", "platinum"};
|
||||
int32 amounts[] = {trade->cp, trade->sp, trade->gp, trade->pp};
|
||||
|
||||
if(tradingWith->GetAppearance() != eaDead) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
parse->AddVar(
|
||||
fmt::format("{}.{}", currencies[i], tradingWith->GetNPCTypeID()),
|
||||
fmt::format("{}", amounts[i])
|
||||
);
|
||||
}
|
||||
|
||||
if (tradingWith->GetAppearance() != eaDead) {
|
||||
tradingWith->FaceTarget(this);
|
||||
}
|
||||
|
||||
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
|
||||
std::vector<std::any> item_list(items.begin(), items.end());
|
||||
parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list);
|
||||
// we cast to any to pass through the quest event system
|
||||
std::vector<std::any> item_list(items.begin(), items.end());
|
||||
for (EQ::ItemInstance *inst: items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
item_list.emplace_back(inst);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 4; ++i) {
|
||||
if(insts[i]) {
|
||||
safe_delete(insts[i]);
|
||||
m_external_handin_money_returned = {};
|
||||
m_external_handin_items_returned = {};
|
||||
bool has_aggro = tradingWith->CheckAggro(this);
|
||||
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE) && !has_aggro) {
|
||||
parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list);
|
||||
LogNpcHandinDetail("EVENT_TRADE triggered for NPC [{}]", tradingWith->GetNPCTypeID());
|
||||
}
|
||||
|
||||
auto handin_npc = tradingWith->CastToNPC();
|
||||
|
||||
// this is a catch-all return for items that weren't consumed by the EVENT_TRADE subroutine
|
||||
// it's possible we have a quest NPC that doesn't have an EVENT_TRADE subroutine
|
||||
// we can't double fire the ReturnHandinItems() event, so we need to check if it's already been processed from EVENT_TRADE
|
||||
if (!handin_npc->HasProcessedHandinReturn()) {
|
||||
if (!handin_npc->HandinStarted()) {
|
||||
LogNpcHandinDetail("EVENT_TRADE did not process handin, calling ReturnHandinItems() for NPC [{}]", tradingWith->GetNPCTypeID());
|
||||
std::map<std::string, uint32> handin = {
|
||||
{"copper", trade->cp},
|
||||
{"silver", trade->sp},
|
||||
{"gold", trade->gp},
|
||||
{"platinum", trade->pp}
|
||||
};
|
||||
|
||||
for (EQ::ItemInstance *inst: items) {
|
||||
if (!inst || !inst->GetItem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string item_id = fmt::format("{}", inst->GetItem()->ID);
|
||||
handin[item_id] += inst->GetCharges();
|
||||
}
|
||||
|
||||
handin_npc->CheckHandin(this, handin, {}, items);
|
||||
}
|
||||
|
||||
if (RuleB(Items, AlwaysReturnHandins)) {
|
||||
handin_npc->ReturnHandinItems(this);
|
||||
LogNpcHandin("ReturnHandinItems called for NPC [{}]", handin_npc->GetNPCTypeID());
|
||||
}
|
||||
}
|
||||
|
||||
handin_npc->ResetHandin();
|
||||
|
||||
for (auto &inst: insts) {
|
||||
if (inst) {
|
||||
safe_delete(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -156,6 +156,7 @@ struct NPCType
|
||||
bool keeps_sold_items;
|
||||
bool is_parcel_merchant;
|
||||
uint8 greed;
|
||||
bool multiquest_enabled;
|
||||
};
|
||||
|
||||
#pragma pack()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user