[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
40 changed files with 2254 additions and 473 deletions
+327 -258
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;