diff --git a/common/emu_constants.h b/common/emu_constants.h index 6494f61dc..80e7a0b05 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -431,4 +431,17 @@ enum ReloadWorld : uint8 { ForceRepop }; +enum MerchantBucketComparison : uint8 { + BucketEqualTo = 0, + BucketNotEqualTo, + BucketGreaterThanOrEqualTo, + BucketLesserThanOrEqualTo, + BucketGreaterThan, + BucketLesserThan, + BucketIsAny, + BucketIsNotAny, + BucketIsBetween, + BucketIsNotBetween +}; + #endif /*COMMON_EMU_CONSTANTS_H*/ diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 21681b262..76699c3d1 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -3621,14 +3621,17 @@ struct LevelAppearance_Struct { //Sends a little graphic on level up }; struct MerchantList { - uint32 id; - uint32 slot; - uint32 item; - int16 faction_required; - int8 level_required; - uint16 alt_currency_cost; - uint32 classes_required; - uint8 probability; + uint32 id; + uint32 slot; + uint32 item; + int16 faction_required; + int8 level_required; + uint16 alt_currency_cost; + uint32 classes_required; + uint8 probability; + std::string bucket_name; + std::string bucket_value; + uint8 bucket_comparison; }; struct TempMerchantList { diff --git a/common/version.h b/common/version.h index cd0b796fb..b9f3d43b3 100644 --- a/common/version.h +++ b/common/version.h @@ -34,7 +34,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9182 +#define CURRENT_BINARY_DATABASE_VERSION 9183 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9028 diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 6f816a947..8b17eba27 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -436,6 +436,7 @@ 9180|2022_05_01_character_peqzone_flags.sql|SHOW TABLES LIKE 'character_peqzone_flags'|empty| 9181|2022_05_03_task_activity_goal_match_list.sql|SHOW COLUMNS FROM `task_activities` LIKE 'goal_match_list'|empty| 9182|2022_05_02_npc_types_int64.sql|SHOW COLUMNS FROM `npc_types` LIKE 'hp'|missing|bigint +9183|2022_05_07_merchant_data_buckets.sql|SHOW COLUMNS FROM `merchantlist` LIKE 'bucket_comparison'|empty # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2022_05_07_merchant_data_buckets.sql b/utils/sql/git/required/2022_05_07_merchant_data_buckets.sql new file mode 100644 index 000000000..5b2ff8ec5 --- /dev/null +++ b/utils/sql/git/required/2022_05_07_merchant_data_buckets.sql @@ -0,0 +1,4 @@ +ALTER TABLE `merchantlist` +ADD COLUMN `bucket_name` varchar(100) NOT NULL DEFAULT '' AFTER `probability`, +ADD COLUMN `bucket_value` varchar(100) NOT NULL DEFAULT '' AFTER `bucket_name`, +ADD COLUMN `bucket_comparison` tinyint UNSIGNED NULL DEFAULT 0 AFTER `bucket_value`; \ No newline at end of file diff --git a/zone/client.cpp b/zone/client.cpp index 6ea03270d..8ff9b8f87 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -11048,3 +11048,167 @@ void Client::ReconnectUCS() QueuePacket(outapp); safe_delete(outapp); } + +bool Client::CheckMerchantDataBucket(uint8 bucket_comparison, std::string bucket_value, std::string player_value) +{ + std::vector bucket_checks; + bool found = false; + bool passes = false; + + switch (bucket_comparison) { + case MerchantBucketComparison::BucketEqualTo: + { + if (player_value != bucket_value) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketNotEqualTo: + { + if (player_value == bucket_value) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketGreaterThanOrEqualTo: + { + if (player_value < bucket_value) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketLesserThanOrEqualTo: + { + if (player_value > bucket_value) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketGreaterThan: + { + if (player_value <= bucket_value) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketLesserThan: + { + if (player_value >= bucket_value) { + break; + } + + passes = true; + + break; + } + case MerchantBucketComparison::BucketIsAny: + { + bucket_checks = split_string(bucket_value, "|"); + if (bucket_checks.empty()) { + break; + } + + for (const auto &bucket : bucket_checks) { + if (player_value == bucket) { + found = true; + break; + } + } + + if (!found) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketIsNotAny: + { + bucket_checks = split_string(bucket_value, "|"); + if (bucket_checks.empty()) { + break; + } + + for (const auto &bucket : bucket_checks) { + if (player_value == bucket) { + found = true; + break; + } + } + + if (found) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketIsBetween: + { + bucket_checks = split_string(bucket_value, "|"); + if (bucket_checks.empty()) { + break; + } + + if ( + std::stoll(player_value) < std::stoll(bucket_checks[0]) || + std::stoll(player_value) > std::stoll(bucket_checks[1]) + ) { + break; + } + + passes = true; + break; + } + case MerchantBucketComparison::BucketIsNotBetween: + { + bucket_checks = split_string(bucket_value, "|"); + if (bucket_checks.empty()) { + break; + } + + if ( + std::stoll(player_value) >= std::stoll(bucket_checks[0]) && + std::stoll(player_value) <= std::stoll(bucket_checks[1]) + ) { + break; + } + + passes = true; + break; + } + } + + return passes; +} + +std::map Client::GetMerchantDataBuckets() +{ + std::map merchant_data_buckets; + + auto query = fmt::format( + "SELECT `key`, `value` FROM data_buckets WHERE `key` LIKE '{}-%'", + EscapeString(GetBucketKey()) + ); + auto results = database.QueryDatabase(query); + + if (!results.Success() || !results.RowCount()) { + return merchant_data_buckets; + } + + for (auto row : results) { + merchant_data_buckets.insert(std::pair(row[0], row[1])); + } + + return merchant_data_buckets; +} diff --git a/zone/client.h b/zone/client.h index eaa814b96..8036e148b 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1652,6 +1652,9 @@ public: // rate limit Timer m_list_task_timers_rate_limit = {}; + std::map GetMerchantDataBuckets(); + bool CheckMerchantDataBucket(uint8 bucket_comparison, std::string bucket_value, std::string player_value); + protected: friend class Mob; void CalcItemBonuses(StatBonuses* newbon); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 46562f716..b5c56d2e6 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -813,152 +813,168 @@ void Client::BulkSendInventoryItems() } void Client::BulkSendMerchantInventory(int merchant_id, int npcid) { - const EQ::ItemData* handyitem = nullptr; - uint32 numItemSlots = 80; //The max number of items passed in the transaction. + const EQ::ItemData* handy_item = nullptr; + + uint32 merchant_slots = 80; //The max number of items passed in the transaction. if (m_ClientVersionBit & EQ::versions::maskRoFAndLater) { // RoF+ can send 200 items - numItemSlots = 200; + merchant_slots = 200; } + const EQ::ItemData *item = nullptr; - std::list merlist = zone->merchanttable[merchant_id]; - std::list::const_iterator itr; - Mob* merch = entity_list.GetMobByNpcTypeID(npcid); - if (merlist.size() == 0) { //Attempt to load the data, it might have been missed if someone spawned the merchant after the zone was loaded + auto merchant_list = zone->merchanttable[merchant_id]; + auto npc = entity_list.GetMobByNpcTypeID(npcid); + if (!merchant_list.size() == 0) { zone->LoadNewMerchantData(merchant_id); - merlist = zone->merchanttable[merchant_id]; - if (merlist.size() == 0) + merchant_list = zone->merchanttable[merchant_id]; + if (!merchant_list.size()) { return; + } } - std::list tmp_merlist = zone->tmpmerchanttable[npcid]; - std::list::iterator tmp_itr; - uint32 i = 1; - uint8 handychance = 0; - for (itr = merlist.begin(); itr != merlist.end() && i <= numItemSlots; ++itr) { - MerchantList ml = *itr; - if (ml.probability != 100 && zone->random.Int(1, 100) > ml.probability) - continue; + auto client_data_buckets = GetMerchantDataBuckets(); - if (GetLevel() < ml.level_required) - continue; - - if (!(ml.classes_required & (1 << (GetClass() - 1)))) - continue; - - int32 fac = merch ? merch->GetPrimaryFaction() : 0; - int32 cur_fac_level; - if (fac == 0 || sneaking) { - cur_fac_level = 0; - } - else { - cur_fac_level = GetModCharacterFactionLevel(fac); + auto temporary_merchant_list = zone->tmpmerchanttable[npcid]; + uint32 slot_id = 1; + uint8 handy_chance = 0; + for (auto ml : merchant_list) { + if (slot_id > merchant_slots) { + break; } - if (cur_fac_level < ml.faction_required) - continue; + auto bucket_name = ml.bucket_name; + auto bucket_value = ml.bucket_value; + if (!bucket_name.empty() && !bucket_value.empty()) { + auto full_name = fmt::format( + "{}-{}", + GetBucketKey(), + bucket_name + ); - handychance = zone->random.Int(0, merlist.size() + tmp_merlist.size() - 1); + auto player_value = client_data_buckets[full_name]; + if (player_value.empty()) { + continue; + } + + if (!CheckMerchantDataBucket(ml.bucket_comparison, bucket_value, player_value)) { + continue; + } + } + + if (ml.probability != 100 && zone->random.Int(1, 100) > ml.probability) { + continue; + } + + if (GetLevel() < ml.level_required) { + continue; + } + + if (!(ml.classes_required & (1 << (GetClass() - 1)))) { + continue; + } + + + int32 faction_id = npc ? npc->GetPrimaryFaction() : 0; + int32 faction_level = ( + (!faction_id || sneaking) ? + 0 : + GetModCharacterFactionLevel(faction_id) + ); + + if (faction_level < ml.faction_required) { + continue; + } + + handy_chance = zone->random.Int(0, merchant_list.size() + temporary_merchant_list.size() - 1); item = database.GetItem(ml.item); if (item) { - if (handychance == 0) - handyitem = item; - else - handychance--; - int charges = 1; - if (item->IsClassCommon()) - charges = item->MaxCharges; - EQ::ItemInstance* inst = database.CreateItem(item, charges); + if (!handy_chance) { + handy_item = item; + } else { + handy_chance--; + } + + int16 charges = item->IsClassCommon() ? item->MaxCharges : 1; + + auto inst = database.CreateItem(item, charges); if (inst) { + auto item_price = static_cast(item->Price * RuleR(Merchant, SellCostMod) * item->SellRate); + auto item_charges = charges ? charges : 1; + if (RuleB(Merchant, UsePriceMod)) { - inst->SetPrice((item->Price * (RuleR(Merchant, SellCostMod)) * item->SellRate * Client::CalcPriceMod(merch, false))); + item_price *= Client::CalcPriceMod(npc); } - else - inst->SetPrice((item->Price * (RuleR(Merchant, SellCostMod)) * item->SellRate)); + + inst->SetCharges(item_charges); + inst->SetMerchantCount(-1); inst->SetMerchantSlot(ml.slot); - inst->SetMerchantCount(-1); //unlimited - if (charges > 0) - inst->SetCharges(charges); - else - inst->SetCharges(1); + inst->SetPrice(item_price); SendItemPacket(ml.slot - 1, inst, ItemPacketMerchant); safe_delete(inst); } } + // Account for merchant lists with gaps. - if (ml.slot >= i) { - if (ml.slot > i) - LogDebug("(WARNING) Merchantlist contains gap at slot [{}]. Merchant: [{}], NPC: [{}]", i, merchant_id, npcid); - i = ml.slot + 1; + if (ml.slot >= slot_id) { + if (ml.slot > slot_id) { + LogDebug("(WARNING) Merchantlist contains gap at slot [{}]. Merchant: [{}], NPC: [{}]", slot_id, merchant_id, npcid); + } + + slot_id = ml.slot + 1; } } - std::list origtmp_merlist = zone->tmpmerchanttable[npcid]; - tmp_merlist.clear(); - for (tmp_itr = origtmp_merlist.begin(); tmp_itr != origtmp_merlist.end() && i <= numItemSlots; ++tmp_itr) { - TempMerchantList ml = *tmp_itr; + + auto temporary_merchant_list_two = zone->tmpmerchanttable[npcid]; + temporary_merchant_list.clear(); + for (auto ml : temporary_merchant_list_two) { + if (slot_id > merchant_slots) { + break; + } + item = database.GetItem(ml.item); - ml.slot = i; + ml.slot = slot_id; if (item) { - if (handychance == 0) - handyitem = item; - else - handychance--; - int charges = 1; - //if(item->ItemClass==ItemClassCommon && (int16)ml.charges <= item->MaxCharges) - // charges=ml.charges; - //else - charges = item->MaxCharges; - EQ::ItemInstance* inst = database.CreateItem(item, charges); + if (!handy_chance) { + handy_item = item; + } else { + handy_chance--; + } + + auto charges = item->MaxCharges; + auto inst = database.CreateItem(item, charges); if (inst) { + auto item_price = static_cast(item->Price * RuleR(Merchant, SellCostMod) * item->SellRate); + auto item_charges = charges ? charges : 1; + if (RuleB(Merchant, UsePriceMod)) { - inst->SetPrice((item->Price * (RuleR(Merchant, SellCostMod)) * item->SellRate * Client::CalcPriceMod(merch, false))); + item_price *= Client::CalcPriceMod(npc); } - else - inst->SetPrice((item->Price * (RuleR(Merchant, SellCostMod)) * item->SellRate)); - inst->SetMerchantSlot(ml.slot); + + inst->SetCharges(item_charges); inst->SetMerchantCount(ml.charges); - if(charges > 0) - inst->SetCharges(item->MaxCharges);//inst->SetCharges(charges); - else - inst->SetCharges(1); - SendItemPacket(ml.slot-1, inst, ItemPacketMerchant); + inst->SetMerchantSlot(ml.slot); + inst->SetPrice(item_price); + + SendItemPacket(ml.slot - 1, inst, ItemPacketMerchant); safe_delete(inst); } } - tmp_merlist.push_back(ml); - i++; + temporary_merchant_list.push_back(ml); + slot_id++; } + //this resets the slot - zone->tmpmerchanttable[npcid] = tmp_merlist; - if (merch != nullptr && handyitem) { - char handy_id[8] = { 0 }; - int greeting = zone->random.Int(0, 4); - int greet_id = 0; - switch (greeting) { - case 1: - greet_id = MERCHANT_GREETING; - break; - case 2: - greet_id = MERCHANT_HANDY_ITEM1; - break; - case 3: - greet_id = MERCHANT_HANDY_ITEM2; - break; - case 4: - greet_id = MERCHANT_HANDY_ITEM3; - break; - default: - greet_id = MERCHANT_HANDY_ITEM4; + zone->tmpmerchanttable[npcid] = temporary_merchant_list; + if (npc && handy_item) { + int greet_id = zone->random.Int(MERCHANT_GREETING, MERCHANT_HANDY_ITEM4); + auto handy_id = std::to_string(greet_id); + if (greet_id != MERCHANT_GREETING) { + MessageString(Chat::NPCQuestSay, GENERIC_STRINGID_SAY, npc->GetCleanName(), handy_id.c_str(), GetName(), handy_item->Name); + } else { + MessageString(Chat::NPCQuestSay, GENERIC_STRINGID_SAY, npc->GetCleanName(), handy_id.c_str(), GetName()); } - sprintf(handy_id, "%i", greet_id); - - if (greet_id != MERCHANT_GREETING) - MessageString(Chat::NPCQuestSay, GENERIC_STRINGID_SAY, merch->GetCleanName(), handy_id, GetName(), handyitem->Name); - else - MessageString(Chat::NPCQuestSay, GENERIC_STRINGID_SAY, merch->GetCleanName(), handy_id, GetName()); } - -// safe_delete_array(cpi); } uint8 Client::WithCustomer(uint16 NewCustomer){ diff --git a/zone/zone.cpp b/zone/zone.cpp index 5833f89f0..59c8c8dc7 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -605,26 +605,25 @@ void Zone::LoadTempMerchantData() void Zone::LoadNewMerchantData(uint32 merchantid) { - std::list merlist; + std::list merchant_list; - std::string query = fmt::format( + auto query = fmt::format( SQL( SELECT - item, - slot, - faction_required, - level_required, - alt_currency_cost, - classes_required, - probability - FROM - merchantlist - WHERE - merchantid = {} - {} - ORDER BY - slot - ), + item, + slot, + faction_required, + level_required, + alt_currency_cost, + classes_required, + probability, + bucket_name, + bucket_value, + bucket_comparison + FROM merchantlist + WHERE merchantid = {} {} + ORDER BY slot + ), merchantid, ContentFilterCriteria::apply() ); @@ -634,25 +633,28 @@ void Zone::LoadNewMerchantData(uint32 merchantid) { return; } - for (auto row = results.begin(); row != results.end(); ++row) { + for (auto row : results) { MerchantList ml; - ml.id = merchantid; - ml.item = atoul(row[0]); - ml.slot = atoul(row[1]); - ml.faction_required = atoul(row[2]); - ml.level_required = atoul(row[3]); - ml.alt_currency_cost = atoul(row[4]); - ml.classes_required = atoul(row[5]); - ml.probability = atoul(row[6]); - merlist.push_back(ml); + ml.id = merchantid; + ml.item = std::stoul(row[0]); + ml.slot = std::stoul(row[1]); + ml.faction_required = static_cast(std::stoi(row[2])); + ml.level_required = static_cast(std::stoul(row[3])); + ml.alt_currency_cost = static_cast(std::stoul(row[4])); + ml.classes_required = std::stoul(row[5]); + ml.probability = static_cast(std::stoul(row[6])); + ml.bucket_name = row[7]; + ml.bucket_value = row[8]; + ml.bucket_comparison = static_cast(std::stoul(row[9])); + merchant_list.push_back(ml); } - merchanttable[merchantid] = merlist; + merchanttable[merchantid] = merchant_list; } void Zone::GetMerchantDataForZoneLoad() { LogInfo("Loading Merchant Lists"); - std::string query = fmt::format( + auto query = fmt::format( SQL ( SELECT DISTINCT merchantlist.merchantid, @@ -662,7 +664,10 @@ void Zone::GetMerchantDataForZoneLoad() { merchantlist.level_required, merchantlist.alt_currency_cost, merchantlist.classes_required, - merchantlist.probability + merchantlist.probability, + merchantlist.bucket_name, + merchantlist.bucket_value, + merchantlist.bucket_comparison FROM merchantlist, npc_types, @@ -688,49 +693,50 @@ void Zone::GetMerchantDataForZoneLoad() { std::map >::iterator merchant_list; uint32 npc_id = 0; - if (results.RowCount() == 0) { + if (!results.Success() || !results.RowCount()) { LogDebug("No Merchant Data found for [{}]", GetShortName()); return; } - for (auto row = results.begin(); row != results.end(); ++row) { - MerchantList merchant_list_entry{}; - merchant_list_entry.id = atoul(row[0]); - if (npc_id != merchant_list_entry.id) { - merchant_list = merchanttable.find(merchant_list_entry.id); + + for (auto row : results) { + MerchantList mle{}; + mle.id = atoul(row[0]); + if (npc_id != mle.id) { + merchant_list = merchanttable.find(mle.id); if (merchant_list == merchanttable.end()) { std::list empty; - merchanttable[merchant_list_entry.id] = empty; - merchant_list = merchanttable.find(merchant_list_entry.id); + merchanttable[mle.id] = empty; + merchant_list = merchanttable.find(mle.id); } - npc_id = merchant_list_entry.id; + npc_id = mle.id; } - auto iter = merchant_list->second.begin(); bool found = false; - while (iter != merchant_list->second.end()) { - if ((*iter).item == merchant_list_entry.id) { + for (const auto &m : merchant_list->second) { + if (m.item == mle.id) { found = true; break; } - ++iter; } if (found) { continue; } + + mle.slot = std::stoul(row[0]); + mle.item = std::stoul(row[1]); + mle.faction_required = static_cast(std::stoi(row[2])); + mle.level_required = static_cast(std::stoul(row[3])); + mle.alt_currency_cost = static_cast(std::stoul(row[4])); + mle.classes_required = std::stoul(row[5]); + mle.probability = static_cast(std::stoul(row[6])); + mle.bucket_name = row[7]; + mle.bucket_value = row[8]; + mle.bucket_comparison = static_cast(std::stoul(row[9])); - merchant_list_entry.slot = atoul(row[1]); - merchant_list_entry.item = atoul(row[2]); - merchant_list_entry.faction_required = atoul(row[3]); - merchant_list_entry.level_required = atoul(row[4]); - merchant_list_entry.alt_currency_cost = atoul(row[5]); - merchant_list_entry.classes_required = atoul(row[6]); - merchant_list_entry.probability = atoul(row[7]); - - merchant_list->second.push_back(merchant_list_entry); + merchant_list->second.push_back(mle); } - } void Zone::LoadMercTemplates(){