[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:
Chris Miles 2025-02-03 16:51:09 -06:00 committed by GitHub
parent d1d6db3a09
commit 6fb919a16f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 2254 additions and 473 deletions

View File

@ -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");

View File

@ -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,

View File

@ -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

View File

@ -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"
};
}

View File

@ -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__);\

View File

@ -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]
);
}
}
}
}

View File

@ -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));

View File

@ -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);

View File

@ -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;

View File

@ -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();

View File

@ -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) + ")");
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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/

View 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!"

View File

@ -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
View 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");
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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)

View File

@ -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);

View File

@ -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)

View File

@ -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

View File

@ -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");

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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 = &current_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 = {};
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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"

View File

@ -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);
};

View File

@ -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.

View File

@ -156,6 +156,7 @@ struct NPCType
bool keeps_sold_items;
bool is_parcel_merchant;
uint8 greed;
bool multiquest_enabled;
};
#pragma pack()