diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ea71c2f..10d49541e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,68 @@ +## [22.62.0] 1/26/2025 + +### Bazaar + +* Improve Bazaar Search Performance ([#4615](https://github.com/EQEmu/Server/pull/4615)) @neckkola 2025-01-27 + +### CLI + +* Add --skip-backup to world database:updates ([#4605](https://github.com/EQEmu/Server/pull/4605)) @Akkadius 2025-01-22 + +### Database + +* Change npc_types walkspeed to be of type float ([#4589](https://github.com/EQEmu/Server/pull/4589)) @Akkadius 2025-01-07 + +### Databuckets + +* Add Account Scoped Databuckets ([#4603](https://github.com/EQEmu/Server/pull/4603)) @Akkadius 2025-01-21 +* Implement Nested Databuckets ([#4604](https://github.com/EQEmu/Server/pull/4604)) @Akkadius 2025-01-27 + +### Feature + +* Add Alternate Bazaar Search Approach ([#4600](https://github.com/EQEmu/Server/pull/4600)) @neckkola 2025-01-20 +* Add Support for Item Previews ([#4599](https://github.com/EQEmu/Server/pull/4599)) @Kinglykrab 2025-01-20 +* Evolving Item Support for RoF2 ([#4496](https://github.com/EQEmu/Server/pull/4496)) @neckkola 2025-01-20 +* Implement Custom Pet Names ([#4594](https://github.com/EQEmu/Server/pull/4594)) @catapultam-habeo 2025-01-22 + +### Fixes + +* Add Bazaar BulkSendTrader Limit for RoF2 ([#4590](https://github.com/EQEmu/Server/pull/4590)) @neckkola 2025-01-08 +* CLI help menu from parsing correctly in World @Akkadius 2025-01-22 +* Delete later in RemoveItem second case @Akkadius 2025-01-25 +* Fix query error in character_evolving_items @Akkadius 2025-01-21 +* Repair a memory leak in #summonitem ([#4591](https://github.com/EQEmu/Server/pull/4591)) @neckkola 2025-01-08 +* Repair an incorrect safe_delete call memory leak. ([#4588](https://github.com/EQEmu/Server/pull/4588)) @neckkola 2025-01-07 +* Repair levers opening the Evolving XP Transfer Window ([#4607](https://github.com/EQEmu/Server/pull/4607)) @neckkola 2025-01-23 +* Update a few Bazaar RoF2 routines for memory leaks ([#4592](https://github.com/EQEmu/Server/pull/4592)) @neckkola 2025-01-08 +* Update database version to match manifest @Akkadius 2025-01-21 +* Update trader add/remove packets to limits for RoF2 ([#4595](https://github.com/EQEmu/Server/pull/4595)) @neckkola 2025-01-19 + +### Linux + +* Implement KSM Kernel Samepage Merging with Maps ([#4601](https://github.com/EQEmu/Server/pull/4601)) @Akkadius 2025-01-21 + +### Memory Leak + +* Change raw pointer to unique_ptr to avoid potential leak in dbg stream ([#4616](https://github.com/EQEmu/Server/pull/4616)) @KimLS 2025-01-27 +* Fix leak in BuyTraderItemOutsideBazaar ([#4609](https://github.com/EQEmu/Server/pull/4609)) @Akkadius 2025-01-24 +* Fix leak in Client::RemoveDuplicateLore ([#4614](https://github.com/EQEmu/Server/pull/4614)) @Akkadius 2025-01-24 +* Fix leak in NPC::RemoveItem ([#4611](https://github.com/EQEmu/Server/pull/4611)) @Akkadius 2025-01-24 +* Fix leak in QuestManager::varlink ([#4610](https://github.com/EQEmu/Server/pull/4610)) @Akkadius 2025-01-24 +* Fix leaks in Client::Handle_OP_AugmentItem ([#4612](https://github.com/EQEmu/Server/pull/4612)) @Akkadius 2025-01-24 +* Fix memory leak in Client::Handle_OP_MoveMultipleItems ([#4613](https://github.com/EQEmu/Server/pull/4613)) @Akkadius 2025-01-24 + +### Performance + +* Client / NPC Position Update Optimizations ([#4602](https://github.com/EQEmu/Server/pull/4602)) @Akkadius 2025-01-21 + +### Quest API + +* Add SetAAEXPPercentage to Perl/Lua ([#4597](https://github.com/EQEmu/Server/pull/4597)) @Kinglykrab 2025-01-19 + +### Zone + +* Implement zone player count sharding ([#4536](https://github.com/EQEmu/Server/pull/4536)) @Akkadius 2025-01-08 + ## [22.61.0] 1/6/2025 ### Bots diff --git a/common/bazaar.cpp b/common/bazaar.cpp index cce3fcdd3..c07869055 100644 --- a/common/bazaar.cpp +++ b/common/bazaar.cpp @@ -6,7 +6,8 @@ std::vector Bazaar::GetSearchResults( - SharedDatabase &db, + Database &db, + Database &content_db, BazaarSearchCriteria_Struct search, uint32 char_zone_id, int32 char_zone_instance_id @@ -31,8 +32,132 @@ Bazaar::GetSearchResults( char_zone_instance_id ); + static std::map item_slot_searches_new = { + {EQ::invslot::slotCharm, 1}, + {EQ::invslot::slotEar1, 2}, + {EQ::invslot::slotHead, 4}, + {EQ::invslot::slotFace, 8}, + {EQ::invslot::slotEar2, 16}, + {EQ::invslot::slotNeck, 32}, + {EQ::invslot::slotShoulders, 64}, + {EQ::invslot::slotArms, 128}, + {EQ::invslot::slotBack, 256}, + {EQ::invslot::slotWrist1, 512}, + {EQ::invslot::slotWrist2, 1024}, + {EQ::invslot::slotRange, 2048}, + {EQ::invslot::slotHands, 4096}, + {EQ::invslot::slotPrimary, 8192}, + {EQ::invslot::slotSecondary, 16384}, + {EQ::invslot::slotFinger1, 32768}, + {EQ::invslot::slotFinger2, 65536}, + {EQ::invslot::slotChest, 131072}, + {EQ::invslot::slotLegs, 262144}, + {EQ::invslot::slotFeet, 524288}, + {EQ::invslot::slotWaist, 1048576}, + {EQ::invslot::slotPowerSource, 2097152}, + {EQ::invslot::slotAmmo, 4194304}, + }; + + struct ItemSearchType { + EQ::item::ItemType type; + std::string condition; + }; + + std::vector item_search_types_new = { + {EQ::item::ItemType::ItemTypeBook, " AND (items.itemclass = 2 or items.itemclass = 31)"}, + {EQ::item::ItemType::ItemTypeContainer, " AND (items.itemclass = 1 or items.itemclass = 67)"}, + {EQ::item::ItemType::ItemTypeAllEffects, " AND (items.scrolleffect > 0 && items.scrolleffect < 65000)"}, + {EQ::item::ItemType::ItemTypeUnknown9, " AND items.worneffect = 998"}, + {EQ::item::ItemType::ItemTypeUnknown10, " AND (items.worneffect >= 1298 && items.worneffect <= 1307)"}, + {EQ::item::ItemType::ItemTypeFocusEffect, " AND items.focuseffect > 0"}, + {EQ::item::ItemType::ItemTypeArmor, " AND items.itemtype = 10"}, + {EQ::item::ItemType::ItemType1HBlunt, " AND items.itemtype = 3"}, + {EQ::item::ItemType::ItemType1HPiercing, " AND items.itemtype = 2"}, + {EQ::item::ItemType::ItemType1HSlash, " AND items.itemtype = 0"}, + {EQ::item::ItemType::ItemType2HBlunt, " AND items.itemtype = 4"}, + {EQ::item::ItemType::ItemType2HSlash, " AND items.itemtype = 1"}, + {EQ::item::ItemType::ItemTypeBow, " AND items.itemtype = 5"}, + {EQ::item::ItemType::ItemTypeShield, " AND items.itemtype = 8"}, + {EQ::item::ItemType::ItemTypeMisc, " AND items.itemtype = 11"}, + {EQ::item::ItemType::ItemTypeFood, " AND items.itemtype = 14"}, + {EQ::item::ItemType::ItemTypeDrink, " AND items.itemtype = 15"}, + {EQ::item::ItemType::ItemTypeLight, " AND items.itemtype = 16"}, + {EQ::item::ItemType::ItemTypeCombinable, " AND items.itemtype = 17"}, + {EQ::item::ItemType::ItemTypeBandage, " AND items.itemtype = 18"}, + {EQ::item::ItemType::ItemTypeSmallThrowing, " AND (items.itemtype = 19 OR items.itemtype = 7)"}, + {EQ::item::ItemType::ItemTypeSpell, " AND items.itemtype = 20"}, + {EQ::item::ItemType::ItemTypePotion, " AND items.itemtype = 21"}, + {EQ::item::ItemType::ItemTypeBrassInstrument, " AND items.itemtype = 25"}, + {EQ::item::ItemType::ItemTypeWindInstrument, " AND items.itemtype = 23"}, + {EQ::item::ItemType::ItemTypeStringedInstrument, " AND items.itemtype = 24"}, + {EQ::item::ItemType::ItemTypePercussionInstrument, " AND items.itemtype = 26"}, + {EQ::item::ItemType::ItemTypeArrow, " AND items.itemtype = 27"}, + {EQ::item::ItemType::ItemTypeJewelry, " AND items.itemtype = 29"}, + {EQ::item::ItemType::ItemTypeNote, " AND items.itemtype = 32"}, + {EQ::item::ItemType::ItemTypeKey, " AND items.itemtype = 33"}, + {EQ::item::ItemType::ItemType2HPiercing, " AND items.itemtype = 35"}, + {EQ::item::ItemType::ItemTypeAlcohol, " AND items.itemtype = 38"}, + {EQ::item::ItemType::ItemTypeMartial, " AND items.itemtype = 45"}, + {EQ::item::ItemType::ItemTypeAugmentation, " AND items.itemtype = 54"}, + {EQ::item::ItemType::ItemTypeAlternateAbility, " AND items.itemtype = 57"}, + {EQ::item::ItemType::ItemTypeCount, " AND items.itemtype = 65"}, + {EQ::item::ItemType::ItemTypeCollectible, " AND items.itemtype = 66"} + }; + + // item stat searches + struct ItemStatSearch { + std::string query_string; + EQ::skills::SkillType skill_type; + }; + + std::map item_stat_searches_new = { + {STAT_AC, {" items.ac" , static_cast(0)} }, + {STAT_AGI, {" items.aagi", static_cast(0)} }, + {STAT_CHA, {" items.acha", static_cast(0)} }, + {STAT_DEX, {" items.adex", static_cast(0)} }, + {STAT_INT, {" items.aint", static_cast(0)} }, + {STAT_STA, {" items.asta", static_cast(0)} }, + {STAT_STR, {" items.astr", static_cast(0)} }, + {STAT_WIS, {" items.awis", static_cast(0)} }, + {STAT_COLD, {" items.cr", static_cast(0)} }, + {STAT_DISEASE, {" items.dr", static_cast(0)} }, + {STAT_FIRE, {" items.fr", static_cast(0)} }, + {STAT_MAGIC, {" items.mr", static_cast(0)} }, + {STAT_POISON, {" items.pr", static_cast(0)} }, + {STAT_HP, {" items.hp", static_cast(0)} }, + {STAT_MANA, {" items.mana", static_cast(0)} }, + {STAT_ENDURANCE, {" items.endur", static_cast(0)} }, + {STAT_ATTACK, {" items.attack", static_cast(0)} }, + {STAT_HP_REGEN, {" items.regen", static_cast(0)} }, + {STAT_MANA_REGEN, {" items.manaregen", static_cast(0)} }, + {STAT_HASTE, {" items.haste", static_cast(0)} }, + {STAT_DAMAGE_SHIELD, {" items.damageshield", static_cast(0)} }, + {STAT_DS_MITIGATION, {" items.dsmitigation", static_cast(0)} }, + {STAT_HEAL_AMOUNT, {" items.healamt", static_cast(0)} }, + {STAT_SPELL_DAMAGE, {" items.spelldmg", static_cast(0)} }, + {STAT_CLAIRVOYANCE, {" items.clairvoyance", static_cast(0)} }, + {STAT_HEROIC_AGILITY, {" items.heroic_agi", static_cast(0)} }, + {STAT_HEROIC_CHARISMA, {" items.heroic_cha", static_cast(0)} }, + {STAT_HEROIC_DEXTERITY, {" items.heroic_dex", static_cast(0)} }, + {STAT_HEROIC_INTELLIGENCE, {" items.heroic_int", static_cast(0)} }, + {STAT_HEROIC_STAMINA, {" items.heroic_sta", static_cast(0)} }, + {STAT_HEROIC_STRENGTH, {" items.heroic_str", static_cast(0)} }, + {STAT_HEROIC_WISDOM, {" items.heroic_wis", static_cast(0)} }, + {STAT_BASH, {" items.skillmodvalue", EQ::skills::SkillBash} }, + {STAT_BACKSTAB, {" items.backstabdmg", EQ::skills::SkillBackstab} }, + {STAT_DRAGON_PUNCH, {" items.skillmodvalue", EQ::skills::SkillDragonPunch} }, + {STAT_EAGLE_STRIKE, {" items.skillmodvalue", EQ::skills::SkillEagleStrike} }, + {STAT_FLYING_KICK, {" items.skillmodvalue", EQ::skills::SkillFlyingKick} }, + {STAT_KICK, {" items.skillmodvalue", EQ::skills::SkillKick} }, + {STAT_ROUND_KICK, {" items.skillmodvalue", EQ::skills::SkillRoundKick} }, + {STAT_TIGER_CLAW, {" items.skillmodvalue", EQ::skills::SkillTigerClaw} }, + {STAT_FRENZY, {" items.skillmodvalue", EQ::skills::SkillFrenzy} }, + }; + bool convert = false; - std::string search_criteria_trader("TRUE "); + std::string search_criteria_trader("TRUE"); + std::string field_criteria_items("FALSE"); + std::string where_criteria_items(" TRUE "); if (search.search_scope == NonRoFBazaarSearchScope) { search_criteria_trader.append( @@ -77,301 +202,115 @@ Bazaar::GetSearchResults( search_criteria_trader.append(fmt::format(" AND trader.item_cost <= {}", (uint64) search.max_cost * 1000)); } - // not yet implemented - // if (search.prestige != 0) { - // 0xffffffff prestige only, 0xfffffffe non-prestige, 0 all - // search_criteria.append(fmt::format(" AND items.type = {} ", search.prestige)); - // } + if (search.slot != std::numeric_limits::max()) { + if (item_slot_searches_new.contains(search.slot)) { + where_criteria_items.append( + fmt::format(" AND items.slots & {0} = {0}", item_slot_searches_new[search.slot])); + } + } - std::string query = fmt::format( - "SELECT COUNT(item_id), trader.char_id, trader.item_id, trader.item_sn, trader.item_charges, trader.item_cost, " - "trader.slot_id, SUM(trader.item_charges), trader.char_zone_id, trader.char_entity_id, character_data.name, " - "aug_slot_1, aug_slot_2, aug_slot_3, aug_slot_4, aug_slot_5, aug_slot_6, trader.char_zone_instance_id " - "FROM trader, character_data " - "WHERE {} AND trader.char_id = character_data.id " - "GROUP BY trader.item_sn, trader.item_charges, trader.char_id", - search_criteria_trader.c_str() - ); + if (search.type != std::numeric_limits::max()) { + for (auto const &[type, condition]: item_search_types_new) { + if (type == search.type) { + where_criteria_items.append(condition); + break; + } + } + } + + if (search.race != std::numeric_limits::max()) { + where_criteria_items.append( + fmt::format(" AND items.races & {0} = {0}", GetPlayerRaceBit(GetRaceIDFromPlayerRaceValue(search.race)))); + } + + if (search._class != std::numeric_limits::max()) { + where_criteria_items.append(fmt::format(" AND items.classes & {0} = {0}", GetPlayerClassBit(search._class))); + } + + if (search.item_stat != std::numeric_limits::max()) { + if (item_stat_searches_new.contains(search.item_stat)) { + field_criteria_items = fmt::format("{}", item_stat_searches_new[search.item_stat].query_string); + if (item_stat_searches_new[search.item_stat].skill_type) { + where_criteria_items.append( + fmt::format(" AND items.skillmodtype = {} ", item_stat_searches_new[search.item_stat].skill_type)); + } + else { + where_criteria_items.append( + fmt::format(" AND {} > 0 ", item_stat_searches_new[search.item_stat].query_string)); + } + } + } + + if (search.augment) { + where_criteria_items.append(fmt::format( + " AND (items.augslot1type = {0} OR " + "items.augslot2type = {0} OR " + "items.augslot3type = {0} OR " + "items.augslot4type = {0} OR " + "items.augslot5type = {0} OR " + "items.augslot6type = {0})", + search.augment) + ); + } + + if (search.min_level != 1) { + where_criteria_items.append(fmt::format(" AND items.reclevel >= {}", search.min_level)); + } + + if (search.max_level != 100) { + where_criteria_items.append(fmt::format(" AND items.reclevel <= {}", search.max_level)); + } std::vector all_entries; + std::vector trader_items_ids{}; - auto results = db.QueryDatabase(query); - - if (!results.Success()) { + auto const trader_results = TraderRepository::GetBazaarTraderDetails(db, search_criteria_trader); + if (trader_results.empty()) { + LogTradingDetail("Bazaar - No traders found in bazaar search."); return all_entries; } - struct ItemSearchType { - EQ::item::ItemType type; - bool condition; - }; + for (auto const &i: trader_results) { + trader_items_ids.push_back(std::to_string(i.trader.item_id)); + } - struct AddititiveSearchCriteria { - bool should_check; - bool condition; - }; + auto const item_results = ItemsRepository::GetItemsForBazaarSearch( + content_db, + trader_items_ids, + std::string(search.item_name), + field_criteria_items, + where_criteria_items + ); + + if (item_results.empty()) { + LogError("Bazaar - No items found in bazaar search."); + return all_entries; + } + + all_entries.reserve(trader_results.size()); + + for (auto const& t:trader_results) { + if (!item_results.contains(t.trader.item_id)) { + continue; + } - for (auto row: results) { BazaarSearchResultsFromDB_Struct r{}; + r.count = 1; + r.trader_id = t.trader.char_id; + r.serial_number = t.trader.item_sn; + r.cost = t.trader.item_cost; + r.slot_id = t.trader.slot_id; + r.sum_charges = t.trader.item_charges; + r.stackable = item_results.at(t.trader.item_id).stackable; + r.icon_id = item_results.at(t.trader.item_id).icon; + r.trader_zone_id = t.trader.char_zone_id; + r.trader_zone_instance_id = t.trader.char_zone_instance_id; + r.trader_entity_id = t.trader.char_entity_id; + r.serial_number_RoF = fmt::format("{:016}\0", t.trader.item_sn); + r.item_name = fmt::format("{:.63}\0", item_results.at(t.trader.item_id).name); + r.trader_name = fmt::format("{:.63}\0", t.trader_name); + r.item_stat = item_results.at(t.trader.item_id).stats; - r.item_id = Strings::ToInt(row[2]); - r.charges = Strings::ToInt(row[4]); - - auto item = db.GetItem(r.item_id); - if (!item) { - continue; - } - - uint32 aug_slot_1 = Strings::ToUnsignedInt(row[11]); - uint32 aug_slot_2 = Strings::ToUnsignedInt(row[12]); - uint32 aug_slot_3 = Strings::ToUnsignedInt(row[13]); - uint32 aug_slot_4 = Strings::ToUnsignedInt(row[14]); - uint32 aug_slot_5 = Strings::ToUnsignedInt(row[15]); - uint32 aug_slot_6 = Strings::ToUnsignedInt(row[16]); - - std::unique_ptr inst( - db.CreateItem( - item, - r.charges, - aug_slot_1, - aug_slot_2, - aug_slot_3, - aug_slot_4, - aug_slot_5, - aug_slot_6 - ) - ); - - if (!inst->GetItem()) { - continue; - } - - r.count = Strings::ToInt(row[0]); - r.trader_id = Strings::ToInt(row[1]); - r.serial_number = Strings::ToInt(row[3]); - r.cost = Strings::ToInt(row[5]); - r.slot_id = Strings::ToInt(row[6]); - r.sum_charges = Strings::ToInt(row[7]); - r.stackable = item->Stackable; - r.icon_id = item->Icon; - r.trader_zone_id = Strings::ToInt(row[8]); - r.trader_zone_instance_id = Strings::ToInt(row[17]); - r.trader_entity_id = Strings::ToInt(row[9]); - r.serial_number_RoF = fmt::format("{:016}\0", Strings::ToInt(row[3])); - r.item_name = fmt::format("{:.63}\0", item->Name); - r.trader_name = fmt::format("{:.63}\0", std::string(row[10]).c_str()); - - LogTradingDetail( - "Searching against item [{}] ({}) for trader [{}]", - item->Name, - item->ID, - r.trader_name - ); - - // item stat searches - std::map item_stat_searches = { - - {STAT_AC, inst->GetItemArmorClass(true)}, - {STAT_AGI, static_cast(inst->GetItemAgi(true))}, - {STAT_CHA, static_cast(inst->GetItemCha(true))}, - {STAT_DEX, static_cast(inst->GetItemDex(true))}, - {STAT_INT, static_cast(inst->GetItemInt(true))}, - {STAT_STA, static_cast(inst->GetItemSta(true))}, - {STAT_STR, static_cast(inst->GetItemStr(true))}, - {STAT_WIS, static_cast(inst->GetItemWis(true))}, - {STAT_COLD, static_cast(inst->GetItemCR(true))}, - {STAT_DISEASE, static_cast(inst->GetItemDR(true))}, - {STAT_FIRE, static_cast(inst->GetItemFR(true))}, - {STAT_MAGIC, static_cast(inst->GetItemMR(true))}, - {STAT_POISON, static_cast(inst->GetItemPR(true))}, - {STAT_HP, static_cast(inst->GetItemHP(true))}, - {STAT_MANA, static_cast(inst->GetItemMana(true))}, - {STAT_ENDURANCE, static_cast(inst->GetItemEndur(true))}, - {STAT_ATTACK, static_cast(inst->GetItemAttack(true))}, - {STAT_HP_REGEN, static_cast(inst->GetItemRegen(true))}, - {STAT_MANA_REGEN, static_cast(inst->GetItemManaRegen(true))}, - {STAT_HASTE, static_cast(inst->GetItemHaste(true))}, - {STAT_DAMAGE_SHIELD, static_cast(inst->GetItemDamageShield(true))}, - {STAT_DS_MITIGATION, static_cast(inst->GetItemDSMitigation(true))}, - {STAT_HEAL_AMOUNT, static_cast(inst->GetItemHealAmt(true))}, - {STAT_SPELL_DAMAGE, static_cast(inst->GetItemSpellDamage(true))}, - {STAT_CLAIRVOYANCE, static_cast(inst->GetItemClairvoyance(true))}, - {STAT_HEROIC_AGILITY, static_cast(inst->GetItemHeroicAgi(true))}, - {STAT_HEROIC_CHARISMA, static_cast(inst->GetItemHeroicCha(true))}, - {STAT_HEROIC_DEXTERITY, static_cast(inst->GetItemHeroicDex(true))}, - {STAT_HEROIC_INTELLIGENCE, static_cast(inst->GetItemHeroicInt(true))}, - {STAT_HEROIC_STAMINA, static_cast(inst->GetItemHeroicSta(true))}, - {STAT_HEROIC_STRENGTH, static_cast(inst->GetItemHeroicStr(true))}, - {STAT_HEROIC_WISDOM, static_cast(inst->GetItemHeroicWis(true))}, - {STAT_BASH, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillBash, true))}, - {STAT_BACKSTAB, static_cast(inst->GetItemBackstabDamage(true))}, - {STAT_DRAGON_PUNCH, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillDragonPunch, true))}, - {STAT_EAGLE_STRIKE, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillEagleStrike, true))}, - {STAT_FLYING_KICK, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillFlyingKick, true))}, - {STAT_KICK, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillKick, true))}, - {STAT_ROUND_KICK, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillRoundKick, true))}, - {STAT_TIGER_CLAW, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillTigerClaw, true))}, - {STAT_FRENZY, static_cast(inst->GetItemSkillsStat(EQ::skills::SkillFrenzy, true))}, - }; - - r.item_stat = item_stat_searches.contains(search.item_stat) ? item_stat_searches[search.item_stat] : 0; - if (item_stat_searches.contains(search.item_stat) && item_stat_searches[search.item_stat] <= 0) { - continue; - } - - static std::map item_slot_searches = { - {EQ::invslot::slotCharm, 1}, - {EQ::invslot::slotEar1, 2}, - {EQ::invslot::slotHead, 4}, - {EQ::invslot::slotFace, 8}, - {EQ::invslot::slotEar2, 16}, - {EQ::invslot::slotNeck, 32}, - {EQ::invslot::slotShoulders, 64}, - {EQ::invslot::slotArms, 128}, - {EQ::invslot::slotBack, 256}, - {EQ::invslot::slotWrist1, 512}, - {EQ::invslot::slotWrist2, 1024}, - {EQ::invslot::slotRange, 2048}, - {EQ::invslot::slotHands, 4096}, - {EQ::invslot::slotPrimary, 8192}, - {EQ::invslot::slotSecondary, 16384}, - {EQ::invslot::slotFinger1, 32768}, - {EQ::invslot::slotFinger2, 65536}, - {EQ::invslot::slotChest, 131072}, - {EQ::invslot::slotLegs, 262144}, - {EQ::invslot::slotFeet, 524288}, - {EQ::invslot::slotWaist, 1048576}, - {EQ::invslot::slotPowerSource, 2097152}, - {EQ::invslot::slotAmmo, 4194304}, - }; - - auto GetEquipmentSlotBit = [&](uint32 slot) -> uint32 { - return item_slot_searches.contains(slot) ? item_slot_searches[slot] : 0; - }; - - auto FindItemAugSlot = [&]() -> bool { - for (auto const &s: inst->GetItem()->AugSlotType) { - return s == search.augment; - } - return false; - }; - - // item type searches - std::vector item_search_types = { - {EQ::item::ItemType::ItemTypeAll, true}, - {EQ::item::ItemType::ItemTypeBook, item->ItemClass == EQ::item::ItemType::ItemTypeBook}, - {EQ::item::ItemType::ItemTypeContainer, item->ItemClass == EQ::item::ItemType::ItemTypeContainer || - item->IsClassBag()}, - {EQ::item::ItemType::ItemTypeAllEffects, item->Scroll.Effect > 0 && item->Scroll.Effect < 65000}, - {EQ::item::ItemType::ItemTypeUnknown9, item->Worn.Effect == 998}, - {EQ::item::ItemType::ItemTypeUnknown10, item->Worn.Effect >= 1298 && item->Worn.Effect <= 1307}, - {EQ::item::ItemType::ItemTypeFocusEffect, item->Focus.Effect > 0}, - {EQ::item::ItemType::ItemTypeArmor, item->ItemType == EQ::item::ItemType::ItemTypeArmor}, - {EQ::item::ItemType::ItemType1HBlunt, item->ItemType == EQ::item::ItemType::ItemType1HBlunt}, - {EQ::item::ItemType::ItemType1HPiercing, item->ItemType == EQ::item::ItemType::ItemType1HPiercing}, - {EQ::item::ItemType::ItemType1HSlash, item->ItemType == EQ::item::ItemType::ItemType1HSlash}, - {EQ::item::ItemType::ItemType2HBlunt, item->ItemType == EQ::item::ItemType::ItemType2HBlunt}, - {EQ::item::ItemType::ItemType2HSlash, item->ItemType == EQ::item::ItemType::ItemType2HSlash}, - {EQ::item::ItemType::ItemTypeBow, item->ItemType == EQ::item::ItemType::ItemTypeBow}, - {EQ::item::ItemType::ItemTypeShield, item->ItemType == EQ::item::ItemType::ItemTypeShield}, - {EQ::item::ItemType::ItemTypeMisc, item->ItemType == EQ::item::ItemType::ItemTypeMisc}, - {EQ::item::ItemType::ItemTypeFood, item->ItemType == EQ::item::ItemType::ItemTypeFood}, - {EQ::item::ItemType::ItemTypeDrink, item->ItemType == EQ::item::ItemType::ItemTypeDrink}, - {EQ::item::ItemType::ItemTypeLight, item->ItemType == EQ::item::ItemType::ItemTypeLight}, - {EQ::item::ItemType::ItemTypeCombinable, item->ItemType == EQ::item::ItemType::ItemTypeCombinable}, - {EQ::item::ItemType::ItemTypeBandage, item->ItemType == EQ::item::ItemType::ItemTypeBandage}, - {EQ::item::ItemType::ItemTypeSmallThrowing, item->ItemType == EQ::item::ItemType::ItemTypeSmallThrowing || - item->ItemType == EQ::item::ItemType::ItemTypeLargeThrowing}, - {EQ::item::ItemType::ItemTypeSpell, item->ItemType == EQ::item::ItemType::ItemTypeSpell}, - {EQ::item::ItemType::ItemTypePotion, item->ItemType == EQ::item::ItemType::ItemTypePotion}, - {EQ::item::ItemType::ItemTypeBrassInstrument, item->ItemType == EQ::item::ItemType::ItemTypeBrassInstrument}, - {EQ::item::ItemType::ItemTypeWindInstrument, item->ItemType == EQ::item::ItemType::ItemTypeWindInstrument}, - {EQ::item::ItemType::ItemTypeStringedInstrument, item->ItemType == EQ::item::ItemType::ItemTypeStringedInstrument}, - {EQ::item::ItemType::ItemTypePercussionInstrument, item->ItemType == EQ::item::ItemType::ItemTypePercussionInstrument}, - {EQ::item::ItemType::ItemTypeArrow, item->ItemType == EQ::item::ItemType::ItemTypeArrow}, - {EQ::item::ItemType::ItemTypeJewelry, item->ItemType == EQ::item::ItemType::ItemTypeJewelry}, - {EQ::item::ItemType::ItemTypeNote, item->ItemType == EQ::item::ItemType::ItemTypeNote}, - {EQ::item::ItemType::ItemTypeKey, item->ItemType == EQ::item::ItemType::ItemTypeKey}, - {EQ::item::ItemType::ItemType2HPiercing, item->ItemType == EQ::item::ItemType::ItemType2HPiercing}, - {EQ::item::ItemType::ItemTypeAlcohol, item->ItemType == EQ::item::ItemType::ItemTypeAlcohol}, - {EQ::item::ItemType::ItemTypeMartial, item->ItemType == EQ::item::ItemType::ItemTypeMartial}, - {EQ::item::ItemType::ItemTypeAugmentation, item->ItemType == EQ::item::ItemType::ItemTypeAugmentation}, - {EQ::item::ItemType::ItemTypeAlternateAbility, item->ItemType == EQ::item::ItemType::ItemTypeAlternateAbility}, - {EQ::item::ItemType::ItemTypeCount, item->ItemType == EQ::item::ItemType::ItemTypeCount}, - {EQ::item::ItemType::ItemTypeCollectible, item->ItemType == EQ::item::ItemType::ItemTypeCollectible} - }; - - bool met_filter = false; - bool has_filter = false; - - for (auto &i: item_search_types) { - if (i.type == search.type) { - has_filter = true; - if (i.condition) { - LogTradingDetail("Item [{}] met search criteria for type [{}]", item->Name, uint8(i.type)); - met_filter = true; - break; - } - } - } - if (has_filter && !met_filter) { - continue; - } - - // TODO: Add catch-all item type filter for specific item types - - // item additive searches - std::vector item_additive_searches = { - { - .should_check = search.min_level != 1 && inst->GetItemRequiredLevel(true) > 0, - .condition = inst->GetItemRequiredLevel(true) >= search.min_level - }, - { - .should_check = search.max_level != 1 && inst->GetItemRequiredLevel(true) > 0, - .condition = inst->GetItemRequiredLevel(true) <= search.max_level - }, - { - .should_check = !std::string(search.item_name).empty(), - .condition = Strings::ContainsLower(item->Name, search.item_name) - }, - { - .should_check = search._class != 0xFFFFFFFF, - .condition = static_cast(item->Classes & GetPlayerClassBit(search._class)) - }, - { - .should_check = search.race != 0xFFFFFFFF, - .condition = static_cast(item->Races & GetPlayerRaceBit(GetRaceIDFromPlayerRaceValue(search.race))) - }, - { - .should_check = search.augment != 0, - .condition = FindItemAugSlot() - }, - { - .should_check = search.slot != 0xFFFFFFFF, - .condition = static_cast(item->Slots & GetEquipmentSlotBit(search.slot)) - }, - }; - - bool should_add = true; - - for (auto &i: item_additive_searches) { - LogTradingDetail( - "Checking item [{}] for search criteria - should_check [{}] condition [{}]", - item->Name, - i.should_check, - i.condition - ); - if (i.should_check && !i.condition) { - should_add = false; - continue; - } - } - - if (!should_add) { - continue; - } - - LogTradingDetail("Found item [{}] meeting search criteria.", r.item_name); if (RuleB(Bazaar, UseAlternateBazaarSearch)) { if (convert || (r.trader_zone_id == Zones::BAZAAR && r.trader_zone_instance_id != char_zone_instance_id)) { r.trader_id = TraderRepository::TRADER_CONVERT_ID + r.trader_zone_instance_id; diff --git a/common/bazaar.h b/common/bazaar.h index fca2573af..bec1ae264 100644 --- a/common/bazaar.h +++ b/common/bazaar.h @@ -3,11 +3,13 @@ #include #include "shareddb.h" +#include "../../common/item_instance.h" class Bazaar { public: static std::vector - GetSearchResults(SharedDatabase &db, BazaarSearchCriteria_Struct search, unsigned int char_zone_id, int char_zone_instance_id); + GetSearchResults(Database &content_db, Database &db, BazaarSearchCriteria_Struct search, unsigned int char_zone_id, int char_zone_instance_id); + }; diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 57819b700..e0c93e552 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -6333,6 +6333,52 @@ CREATE TABLE `character_pet_name` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; )", }, + ManifestEntry{ + .version = 9294, + .description = "2025_01_26_items_table_bazaar_search_indexes.sql", + .check = "SHOW CREATE TABLE `items`", + .condition = "missing", + .match = "idx_slots_reclevel", + .sql = R"( +-- indexes for the `items` table +CREATE INDEX idx_slots_reclevel ON items (slots, reclevel); +CREATE INDEX idx_itemclass_itemtype ON items (itemclass, itemtype); +CREATE INDEX idx_augment_slots ON items ( + augslot1type, + augslot2type, + augslot3type, + augslot4type, + augslot5type, + augslot6type +); +CREATE INDEX idx_races_classes ON items (races, classes); + +-- common stat fields +CREATE INDEX idx_item_ac ON items (ac); +CREATE INDEX idx_item_hp ON items (hp); +CREATE INDEX idx_item_mana ON items (mana); +CREATE INDEX idx_item_reclevel ON items (reclevel); +CREATE INDEX idx_item_type_skill ON items (itemtype, skillmodtype); +)", + .content_schema_update = true + }, + ManifestEntry{ + .version = 9295, + .description = "2025_01_26_trader_table_bazaar_search_indexes.sql", + .check = "SHOW CREATE TABLE `trader`", + .condition = "missing", + .match = "idx_trader_item", + .sql = R"( +-- indexes for the `trader` table +CREATE INDEX idx_trader_item ON trader (item_id, item_cost); +CREATE INDEX idx_trader_char ON trader (char_id, char_zone_id, char_zone_instance_id); +CREATE INDEX idx_trader_item_sn ON trader (item_sn); +CREATE INDEX idx_trader_item_cost ON trader (item_cost); +CREATE INDEX idx_trader_active_transaction ON trader (active_transaction); +)", + .content_schema_update = false + }, + // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/inventory_profile.h b/common/inventory_profile.h index b1d1e4850..8e364aa5a 100644 --- a/common/inventory_profile.h +++ b/common/inventory_profile.h @@ -218,6 +218,8 @@ namespace EQ std::map& GetPersonal() { return m_inv; } int16 HasEvolvingItem(uint64 evolve_unique_id, uint8 quantity, uint8 where); + inline int16 PushItem(int16 slot_id, ItemInstance* inst) { return _PutItem(slot_id, inst); } + protected: /////////////////////////////// // Protected Methods diff --git a/common/net/daybreak_connection.cpp b/common/net/daybreak_connection.cpp index 0fefc9982..6af6576b1 100644 --- a/common/net/daybreak_connection.cpp +++ b/common/net/daybreak_connection.cpp @@ -500,10 +500,9 @@ void EQ::Net::DaybreakConnection::ProcessQueue() break; } - auto packet = iter->second; + auto &packet = iter->second; stream->packet_queue.erase(iter); ProcessDecodedPacket(*packet); - delete packet; } } } @@ -513,9 +512,8 @@ void EQ::Net::DaybreakConnection::RemoveFromQueue(int stream, uint16_t seq) auto s = &m_streams[stream]; auto iter = s->packet_queue.find(seq); if (iter != s->packet_queue.end()) { - auto packet = iter->second; + auto &packet = iter->second; s->packet_queue.erase(iter); - delete packet; } } @@ -527,7 +525,7 @@ void EQ::Net::DaybreakConnection::AddToQueue(int stream, uint16_t seq, const Pac DynamicPacket *out = new DynamicPacket(); out->PutPacket(0, p); - s->packet_queue.emplace(std::make_pair(seq, out)); + s->packet_queue.emplace(std::make_pair(seq, std::unique_ptr(out))); } } diff --git a/common/net/daybreak_connection.h b/common/net/daybreak_connection.h index c2c714813..2561f91a4 100644 --- a/common/net/daybreak_connection.h +++ b/common/net/daybreak_connection.h @@ -201,7 +201,7 @@ namespace EQ uint16_t sequence_in; uint16_t sequence_out; - std::map packet_queue; + std::map> packet_queue; DynamicPacket fragment_packet; uint32_t fragment_current_bytes; diff --git a/common/repositories/items_repository.h b/common/repositories/items_repository.h index 919c11dd3..4e5a8868e 100644 --- a/common/repositories/items_repository.h +++ b/common/repositories/items_repository.h @@ -7,6 +7,14 @@ class ItemsRepository: public BaseItemsRepository { public: + struct Bazaar_Results { + uint32 item_id; + std::string name; + bool stackable; + uint32 icon; + uint32 stats; + }; + static std::vector GetItemIDsBySearchCriteria( Database& db, std::string search_string, @@ -37,6 +45,51 @@ public: return item_id_list; } + static std::unordered_map GetItemsForBazaarSearch( + Database& db, + const std::vector &search_ids, + const std::string &name, + const std::string &field_criteria_items, + const std::string &where_criteria_items, + const uint32 query_limit = 0 + ) + { + auto query = fmt::format( + "SELECT id, name, stackable, icon, {} " + "FROM items " + "WHERE `name` LIKE '%%{}%%' AND {} AND id IN({}) " + "ORDER BY id ASC", + field_criteria_items, + Strings::Escape(name), + where_criteria_items, + Strings::Implode(",", search_ids) + ); + + if (query_limit >= 1) { + query += fmt::format(" LIMIT {}", query_limit); + } + + std::unordered_map item_list; + + auto results = db.QueryDatabase(query); + if (!results.Success() || !results.RowCount()) { + return item_list; + } + + item_list.reserve(results.RowCount()); + for (auto row : results) { + Bazaar_Results br{}; + br.item_id = row[0] ? static_cast(atoi(row[0])) : 0; + br.name = row[1] ? row[1] : ""; + br.stackable = atoi(row[2]) ? true : false; + br.icon = row[3] ? static_cast(atoi(row[3])) : 0; + br.stats = row[4] ? static_cast(atoi(row[4])) : 0; + + item_list.emplace(br.item_id, br); + } + + return item_list; + } }; diff --git a/common/repositories/trader_repository.h b/common/repositories/trader_repository.h index a8e3c13c1..1892de6be 100644 --- a/common/repositories/trader_repository.h +++ b/common/repositories/trader_repository.h @@ -28,6 +28,11 @@ public: std::vector traders{}; }; + struct BazaarTraderSearch_Struct { + Trader trader; + std::string trader_name; + }; + struct WelcomeData_Struct { uint32 count_of_traders; uint32 count_of_items; @@ -265,6 +270,54 @@ public: return trader; } + + static std::vector GetBazaarTraderDetails( + Database &db, + std::string &search_criteria_trader + ) + { + std::vector all_entries{}; + + auto query = fmt::format( + "SELECT trader.*, c.`name` FROM `trader` INNER JOIN character_data AS c ON trader.char_id = c.id " + "WHERE {} ORDER BY trader.char_id ASC", + search_criteria_trader + ); + + auto results = db.QueryDatabase(query); + + if (results.RowCount() == 0) { + return all_entries; + } + + all_entries.reserve(results.RowCount()); + for (auto row = results.begin(); row != results.end(); ++row) { + BazaarTraderSearch_Struct e{}; + + e.trader.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.trader.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.trader.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.trader.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.trader.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.trader.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.trader.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.trader.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.trader.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.trader.item_sn = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.trader.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; + e.trader.item_cost = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.trader.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.trader.char_entity_id = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.trader.char_zone_id = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.trader.char_zone_instance_id = row[15] ? static_cast(atoi(row[15])) : 0; + e.trader.active_transaction = row[16] ? static_cast(strtoul(row[16], nullptr, 10)) : 0; + e.trader_name = row[17] ? row[17] : std::string(""); + + all_entries.push_back(e); + } + + return all_entries; + } }; #endif //EQEMU_TRADER_REPOSITORY_H diff --git a/common/version.h b/common/version.h index 1cbb41f15..19a7bf16f 100644 --- a/common/version.h +++ b/common/version.h @@ -25,7 +25,7 @@ // Build variables // these get injected during the build pipeline -#define CURRENT_VERSION "22.61.0-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.62.0-dev" // always append -dev to the current version for custom-builds #define LOGIN_VERSION "0.8.0" #define COMPILE_DATE __DATE__ #define COMPILE_TIME __TIME__ @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9293 +#define CURRENT_BINARY_DATABASE_VERSION 9295 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #endif diff --git a/package.json b/package.json index 68a2da55e..64fa8fb46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.61.0", + "version": "22.62.0", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" diff --git a/utils/scripts/build/should-release/go.mod b/utils/scripts/build/should-release/go.mod index 929c8e621..a4a1b0988 100644 --- a/utils/scripts/build/should-release/go.mod +++ b/utils/scripts/build/should-release/go.mod @@ -11,6 +11,6 @@ require ( github.com/golang/protobuf v1.3.2 // indirect github.com/google/go-querystring v1.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/net v0.33.0 // indirect google.golang.org/appengine v1.6.7 // indirect ) diff --git a/utils/scripts/build/should-release/go.sum b/utils/scripts/build/should-release/go.sum index 2caeabf3c..4ace6da5b 100644 --- a/utils/scripts/build/should-release/go.sum +++ b/utils/scripts/build/should-release/go.sum @@ -14,8 +14,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/zone/client.h b/zone/client.h index bfd02cce3..28b2de376 100644 --- a/zone/client.h +++ b/zone/client.h @@ -352,7 +352,6 @@ public: const char *message9 = nullptr); void Tell_StringID(uint32 string_id, const char *who, const char *message); void SendColoredText(uint32 color, std::string message); - void SendBazaarResults(uint32 trader_id, uint32 in_class, uint32 in_race, uint32 item_stat, uint32 item_slot, uint32 item_type, char item_name[64], uint32 min_price, uint32 max_price); void SendTraderItem(uint32 item_id,uint16 quantity, TraderRepository::Trader &trader); void DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria); uint16 FindTraderItem(int32 SerialNumber,uint16 Quantity); @@ -1188,7 +1187,7 @@ public: void Escape(); //keep or quest function void DisenchantSummonedBags(bool client_update = true); void RemoveNoRent(bool client_update = true); - void RemoveDuplicateLore(bool client_update = true); + void RemoveDuplicateLore(); void MoveSlotNotAllowed(bool client_update = true); virtual bool RangedAttack(Mob* other, bool CanDoubleAttack = false); virtual void ThrowingAttack(Mob* other, bool CanDoubleAttack = false); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index d54f60804..4d0d9b6f8 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -3260,11 +3260,11 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) if (!new_aug) { // Shouldn't get the OP code without the augment on the user's cursor, but maybe it's h4x. LogError("AugmentItem OpCode with 'Insert' or 'Swap' action received, but no augment on client's cursor"); Message(Chat::Red, "Error: No augment found on cursor for inserting."); - return; + break; } else { if (!RuleB(Inventory, AllowMultipleOfSameAugment) && tobe_auged->ContainsAugmentByID(new_aug->GetID())) { Message(Chat::Red, "Error: Cannot put multiple of the same augment in an item."); - return; + break; } if ( @@ -3348,7 +3348,7 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) in_augment->augment_index ).c_str() ); - return; + break; } item_one_to_push = tobe_auged->Clone(); @@ -3420,7 +3420,7 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) } } else { Message(Chat::Red, "Error: Could not find augmentation to remove at index %i. Aborting.", in_augment->augment_index); - return; + break; } old_aug = tobe_auged->RemoveAugment(in_augment->augment_index); @@ -3453,7 +3453,7 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) if (!PutItemInInventory(EQ::invslot::slotCursor, *item_two_to_push, true)) { LogError("Problem returning augment to player's cursor after safe removal"); Message(Chat::Yellow, "Error: Failed to return augment after removal from item!"); - return; + break; } } break; @@ -3498,7 +3498,7 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) in_augment->augment_index ).c_str() ); - return; + break; } tobe_auged->DeleteAugment(in_augment->augment_index); @@ -3519,6 +3519,7 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) if (material != EQ::textures::materialInvalid) { SendWearChange(material); } + break; default: // Unknown LogInventory( @@ -3532,9 +3533,12 @@ void Client::Handle_OP_AugmentItem(const EQApplicationPacket *app) ); break; } + safe_delete(item_one_to_push); + safe_delete(item_two_to_push); } else { Object::HandleAugmentation(this, in_augment, m_tradeskill_object); // Delegate to tradeskill object to perform combine } + return; } @@ -10896,6 +10900,7 @@ void Client::Handle_OP_MoveMultipleItems(const EQApplicationPacket *app) InterrogateInventory(this, true, false, true, error); } } + safe_delete(mi); } // This is the swap. // Client behavior is just to move stacks without combining them diff --git a/zone/client_process.cpp b/zone/client_process.cpp index ea3e82252..07158afa7 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -770,7 +770,7 @@ void Client::BulkSendInventoryItems() RemoveNoRent(false); } - RemoveDuplicateLore(false); + RemoveDuplicateLore(); MoveSlotNotAllowed(false); EQ::OutBuffer ob; diff --git a/zone/data_bucket.cpp b/zone/data_bucket.cpp index 23d4eb9d6..8aeadef0c 100644 --- a/zone/data_bucket.cpp +++ b/zone/data_bucket.cpp @@ -1,12 +1,15 @@ #include "data_bucket.h" -#include "entity.h" #include "zonedb.h" #include "mob.h" #include "worldserver.h" #include #include +#include "../common/json/json.hpp" + +using json = nlohmann::json; extern WorldServer worldserver; +const std::string NESTED_KEY_DELIMITER = "."; std::vector g_data_bucket_cache = {}; @@ -25,8 +28,13 @@ void DataBucket::SetData(const std::string &bucket_key, const std::string &bucke DataBucket::SetData(k); } -void DataBucket::SetData(const DataBucketKey &k) +void DataBucket::SetData(const DataBucketKey &k_) { + DataBucketKey k = k_; // copy the key so we can modify it + if (k.key.find(NESTED_KEY_DELIMITER) != std::string::npos) { + k.key = Strings::Split(k.key, NESTED_KEY_DELIMITER).front(); + } + auto b = DataBucketsRepository::NewEntity(); auto r = GetData(k, true); // if we have an entry, use it @@ -60,9 +68,48 @@ void DataBucket::SetData(const DataBucketKey &k) b.expires = expires_time_unix; b.value = k.value; + b.key_ = k.key; + + // Check for nested keys (keys with dots) + if (k_.key.find(NESTED_KEY_DELIMITER) != std::string::npos) { + // Retrieve existing JSON or create a new one + std::string existing_value = r.id > 0 ? r.value : "{}"; + json json_value = json::object(); + + try { + json_value = json::parse(existing_value); + } catch (json::parse_error &e) { + LogError("Failed to parse JSON for key [{}]: {}", k_.key, e.what()); + json_value = json::object(); // Reset to an empty object on error + } + + // Recursively merge new key-value pair into the JSON object + auto nested_keys = Strings::Split(k_.key, NESTED_KEY_DELIMITER); + json *current = &json_value; + + for (size_t i = 0; i < nested_keys.size(); ++i) { + const std::string &key_part = nested_keys[i]; + if (i == nested_keys.size() - 1) { + // Set the value at the final key + (*current)[key_part] = k_.value; + } else { + // Traverse or create nested objects + if (!current->contains(key_part)) { + (*current)[key_part] = json::object(); + } else if (!(*current)[key_part].is_object()) { + // If key exists but is not an object, reset to object to avoid conflicts + (*current)[key_part] = json::object(); + } + current = &(*current)[key_part]; + } + } + + // Serialize JSON back to string + b.value = json_value.dump(); + b.key_ = nested_keys.front(); // Use the top-level key + } if (bucket_id) { - // update the cache if it exists if (CanCache(k)) { for (auto &e: g_data_bucket_cache) { @@ -76,7 +123,6 @@ void DataBucket::SetData(const DataBucketKey &k) DataBucketsRepository::UpdateOne(database, b); } else { - b.key_ = k.key; b = DataBucketsRepository::InsertOne(database, b); // add to cache if it doesn't exist @@ -92,12 +138,56 @@ std::string DataBucket::GetData(const std::string &bucket_key) return GetData(DataBucketKey{.key = bucket_key}).value; } +DataBucketsRepository::DataBuckets DataBucket::ExtractNestedValue( + const DataBucketsRepository::DataBuckets &bucket, + const std::string &full_key) +{ + auto nested_keys = Strings::Split(full_key, NESTED_KEY_DELIMITER); + json json_value; + + try { + json_value = json::parse(bucket.value); // Parse the JSON + } catch (json::parse_error &ex) { + LogError("Failed to parse JSON for key [{}]: {}", bucket.key_, ex.what()); + return DataBucketsRepository::NewEntity(); // Return empty entity on parse error + } + + // Start from the top-level key (e.g., "progression") + json *current = &json_value; + + // Traverse the JSON structure + for (const auto &key_part: nested_keys) { + LogDataBuckets("Looking for key part [{}] in JSON", key_part); + + if (!current->contains(key_part)) { + LogDataBuckets("Key part [{}] not found in JSON for [{}]", key_part, full_key); + return DataBucketsRepository::NewEntity(); + } + + current = &(*current)[key_part]; + } + + // Create a new entity with the extracted value + DataBucketsRepository::DataBuckets result = bucket; // Copy the original bucket + result.value = current->is_string() ? current->get() : current->dump(); + return result; +} + // GetData fetches bucket data from the database or cache if it exists // if the bucket doesn't exist, it will be added to the cache as a miss // if ignore_misses_cache is true, the bucket will not be added to the cache as a miss // the only place we should be ignoring the misses cache is on the initial read during SetData -DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k, bool ignore_misses_cache) +DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k_, bool ignore_misses_cache) { + DataBucketKey k = k_; // Copy the key so we can modify it + + bool is_nested_key = k.key.find(NESTED_KEY_DELIMITER) != std::string::npos; + + // Extract the top-level key for nested keys + if (is_nested_key) { + k.key = Strings::Split(k.key, NESTED_KEY_DELIMITER).front(); + } + LogDataBuckets( "Getting bucket key [{}] bot_id [{}] account_id [{}] character_id [{}] npc_id [{}]", k.key, @@ -109,9 +199,9 @@ DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k, b bool can_cache = CanCache(k); - // check the cache first if we can cache + // Attempt to retrieve the value from the cache if (can_cache) { - for (const auto &e: g_data_bucket_cache) { + for (const auto &e : g_data_bucket_cache) { if (CheckBucketMatch(e, k)) { if (e.expires > 0 && e.expires < std::time(nullptr)) { LogDataBuckets("Attempted to read expired key [{}] removing from cache", e.key_); @@ -119,37 +209,32 @@ DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k, b return DataBucketsRepository::NewEntity(); } - // this is a bucket miss, return empty entity - // we still cache bucket misses, so we don't have to hit the database - if (e.id == 0) { - return DataBucketsRepository::NewEntity(); + LogDataBuckets("Returning key [{}] value [{}] from cache", e.key_, e.value); + + if (is_nested_key) { + return ExtractNestedValue(e, k_.key); } - LogDataBuckets("Returning key [{}] value [{}] from cache", e.key_, e.value); return e; } } } + // Fetch the value from the database auto r = DataBucketsRepository::GetWhere( database, fmt::format( - "{} `key` = '{}' LIMIT 1", + " {} `key` = '{}' LIMIT 1", DataBucket::GetScopedDbFilters(k), k.key ) ); if (r.empty()) { - - // if we're ignoring the misses cache, don't add to the cache - // the only place this is ignored is during the initial read of SetData - bool add_to_misses_cache = !ignore_misses_cache && can_cache; - if (add_to_misses_cache) { + // Handle cache misses + if (!ignore_misses_cache && can_cache) { size_t size_before = g_data_bucket_cache.size(); - // cache bucket misses, so we don't have to hit the database - // when scripts try to read a bucket that doesn't exist g_data_bucket_cache.emplace_back( DataBucketsRepository::DataBuckets{ .id = 0, @@ -175,22 +260,21 @@ DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k, b ); } - return {}; + return DataBucketsRepository::NewEntity(); } auto bucket = r.front(); - // if the entry has expired, delete it - if (bucket.expires > 0 && bucket.expires < (long long) std::time(nullptr)) { + // If the entry has expired, delete it + if (bucket.expires > 0 && bucket.expires < static_cast(std::time(nullptr))) { DeleteData(k); - return {}; + return DataBucketsRepository::NewEntity(); } - // add to cache if it doesn't exist + // Add the value to the cache if it doesn't exist if (can_cache) { bool has_cache = false; - - for (auto &e: g_data_bucket_cache) { + for (const auto &e : g_data_bucket_cache) { if (e.id == bucket.id) { has_cache = true; break; @@ -202,6 +286,11 @@ DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k, b } } + // Handle nested key extraction + if (is_nested_key) { + return ExtractNestedValue(bucket, k_.key); + } + return bucket; } diff --git a/zone/data_bucket.h b/zone/data_bucket.h index 1bd216630..1c4946442 100644 --- a/zone/data_bucket.h +++ b/zone/data_bucket.h @@ -45,9 +45,9 @@ public: static bool GetDataBuckets(Mob *mob); // scoped bucket methods - static void SetData(const DataBucketKey &k); + static void SetData(const DataBucketKey &k_); static bool DeleteData(const DataBucketKey &k); - static DataBucketsRepository::DataBuckets GetData(const DataBucketKey &k, bool ignore_misses_cache = false); + static DataBucketsRepository::DataBuckets GetData(const DataBucketKey &k_, bool ignore_misses_cache = false); static std::string GetDataExpires(const DataBucketKey &k); static std::string GetDataRemaining(const DataBucketKey &k); static std::string GetScopedDbFilters(const DataBucketKey &k); @@ -63,6 +63,8 @@ public: static void ClearCache(); static void DeleteFromCache(uint64 id, DataBucketLoadType::Type type); static bool CanCache(const DataBucketKey &key); + static DataBucketsRepository::DataBuckets + ExtractNestedValue(const DataBucketsRepository::DataBuckets &bucket, const std::string &full_key); }; #endif //EQEMU_DATABUCKET_H diff --git a/zone/doors.cpp b/zone/doors.cpp index 036f38928..32baf2ee5 100644 --- a/zone/doors.cpp +++ b/zone/doors.cpp @@ -612,7 +612,7 @@ void Doors::HandleClick(Client *sender, uint8 trigger) } } - if (GetOpenType() == 40 && GetZone(GetDoorZone(),0)) { + if (GetOpenType() == 40 && sender->GetZoneID() == Zones::CORATHUS) { sender->SendEvolveXPTransferWindow(); } } diff --git a/zone/inventory.cpp b/zone/inventory.cpp index f89c1005c..e031d5208 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -2979,107 +2979,72 @@ void Client::RemoveNoRent(bool client_update) } // Two new methods to alleviate perpetual login desyncs -void Client::RemoveDuplicateLore(bool client_update) +void Client::RemoveDuplicateLore() { - for (auto slot_id = EQ::invslot::EQUIPMENT_BEGIN; slot_id <= EQ::invslot::EQUIPMENT_END; ++slot_id) { - if ((((uint64)1 << slot_id) & GetInv().GetLookup()->PossessionsBitmask) == 0) + for (auto slot_id : GetInventorySlots()) { + if ((((uint64) 1 << slot_id) & GetInv().GetLookup()->PossessionsBitmask) == 0) { continue; - - auto inst = m_inv.PopItem(slot_id); - if (inst == nullptr) { continue; } - if(CheckLoreConflict(inst->GetItem())) { - LogInventory("Lore Duplication Error: Deleting [{}] from slot [{}]", inst->GetItem()->Name, slot_id); - database.SaveInventory(character_id, nullptr, slot_id); } - else { - m_inv.PutItem(slot_id, *inst); - } - safe_delete(inst); - } - for (auto slot_id = EQ::invslot::GENERAL_BEGIN; slot_id <= EQ::invslot::GENERAL_END; ++slot_id) { - if ((((uint64)1 << slot_id) & GetInv().GetLookup()->PossessionsBitmask) == 0) + // ignore shared bank slots + if (slot_id >= EQ::invslot::SHARED_BANK_BEGIN && slot_id <= EQ::invslot::SHARED_BANK_END) { continue; + } + if (slot_id >= EQ::invbag::SHARED_BANK_BAGS_BEGIN && slot_id <= EQ::invbag::SHARED_BANK_BAGS_END) { + continue; + } + + // slot gets handled in a queue + if (slot_id == EQ::invslot::slotCursor) { + continue; + } + + // temporarily move the item off of the slot auto inst = m_inv.PopItem(slot_id); - if (inst == nullptr) { continue; } + if (!inst) { + continue; + } + if (CheckLoreConflict(inst->GetItem())) { - LogInventory("Lore Duplication Error: Deleting [{}] from slot [{}]", inst->GetItem()->Name, slot_id); + LogError( + "Lore Duplication Error | Deleting [{}] ({}) from slot [{}] client [{}]", + inst->GetItem()->Name, + inst->GetItem()->ID, + slot_id, + GetCleanName() + ); database.SaveInventory(character_id, nullptr, slot_id); + safe_delete(inst); } - else { - m_inv.PutItem(slot_id, *inst); - } - safe_delete(inst); + + // if no lore conflict, put the item back in the slot + m_inv.PushItem(slot_id, inst); } - for (auto slot_id = EQ::invbag::GENERAL_BAGS_BEGIN; slot_id <= EQ::invbag::CURSOR_BAG_END; ++slot_id) { - auto temp_slot = EQ::invslot::GENERAL_BEGIN + ((slot_id - EQ::invbag::GENERAL_BAGS_BEGIN) / EQ::invbag::SLOT_COUNT); - if ((((uint64)1 << temp_slot) & GetInv().GetLookup()->PossessionsBitmask) == 0) - continue; - - auto inst = m_inv.PopItem(slot_id); - if (inst == nullptr) { continue; } - if(CheckLoreConflict(inst->GetItem())) { - LogInventory("Lore Duplication Error: Deleting [{}] from slot [{}]", inst->GetItem()->Name, slot_id); - database.SaveInventory(character_id, nullptr, slot_id); - } - else { - m_inv.PutItem(slot_id, *inst); - } - safe_delete(inst); - } - - for (auto slot_id = EQ::invslot::BANK_BEGIN; slot_id <= EQ::invslot::BANK_END; ++slot_id) { - if ((slot_id - EQ::invslot::BANK_BEGIN) >= GetInv().GetLookup()->InventoryTypeSize.Bank) - continue; - - auto inst = m_inv.PopItem(slot_id); - if (inst == nullptr) { continue; } - if(CheckLoreConflict(inst->GetItem())) { - LogInventory("Lore Duplication Error: Deleting [{}] from slot [{}]", inst->GetItem()->Name, slot_id); - database.SaveInventory(character_id, nullptr, slot_id); - } - else { - m_inv.PutItem(slot_id, *inst); - } - safe_delete(inst); - } - - for (auto slot_id = EQ::invbag::BANK_BAGS_BEGIN; slot_id <= EQ::invbag::BANK_BAGS_END; ++slot_id) { - auto temp_slot = (slot_id - EQ::invbag::BANK_BAGS_BEGIN) / EQ::invbag::SLOT_COUNT; - if (temp_slot >= GetInv().GetLookup()->InventoryTypeSize.Bank) - continue; - - auto inst = m_inv.PopItem(slot_id); - if (inst == nullptr) { continue; } - if(CheckLoreConflict(inst->GetItem())) { - LogInventory("Lore Duplication Error: Deleting [{}] from slot [{}]", inst->GetItem()->Name, slot_id); - database.SaveInventory(character_id, nullptr, slot_id); - } - else { - m_inv.PutItem(slot_id, *inst); - } - safe_delete(inst); - } - - // Shared Bank and Shared Bank Containers are not checked due to their allowing duplicate lore items - if (!m_inv.CursorEmpty()) { std::list local_1; std::list local_2; while (!m_inv.CursorEmpty()) { auto inst = m_inv.PopItem(EQ::invslot::slotCursor); - if (inst == nullptr) { continue; } + if (!inst) { + continue; + } local_1.push_back(inst); } - for (auto iter = local_1.begin(); iter != local_1.end(); ++iter) { - auto inst = *iter; - if (inst == nullptr) { continue; } + for (auto inst: local_1) { + if (!inst) { + continue; + } if (CheckLoreConflict(inst->GetItem())) { - LogInventory("Lore Duplication Error: Deleting [{}] from `Limbo`", inst->GetItem()->Name); + LogError( + "Lore Duplication Error | Deleting [{}] ({}) from `Limbo` client [{}]", + inst->GetItem()->Name, + inst->GetItem()->ID, + GetCleanName() + ); safe_delete(inst); } else { @@ -3088,17 +3053,25 @@ void Client::RemoveDuplicateLore(bool client_update) } local_1.clear(); - for (auto iter = local_2.begin(); iter != local_2.end(); ++iter) { - auto inst = *iter; - if (inst == nullptr) { continue; } + for (auto inst: local_2) { + if (!inst) { + continue; + } if (!inst->GetItem()->LoreFlag || - ((inst->GetItem()->LoreGroup == -1) && (m_inv.HasItem(inst->GetID(), 0, invWhereCursor) == INVALID_INDEX)) || - (inst->GetItem()->LoreGroup && (~inst->GetItem()->LoreGroup) && (m_inv.HasItemByLoreGroup(inst->GetItem()->LoreGroup, invWhereCursor) == INVALID_INDEX)) + ((inst->GetItem()->LoreGroup == -1) && + (m_inv.HasItem(inst->GetID(), 0, invWhereCursor) == INVALID_INDEX)) || + (inst->GetItem()->LoreGroup && (~inst->GetItem()->LoreGroup) && + (m_inv.HasItemByLoreGroup(inst->GetItem()->LoreGroup, invWhereCursor) == INVALID_INDEX)) ) { m_inv.PushCursor(*inst); } else { - LogInventory("Lore Duplication Error: Deleting [{}] from `Limbo`", inst->GetItem()->Name); + LogError( + "Lore Duplication Error | Deleting [{}] ({}) from `Limbo` client [{}]", + inst->GetItem()->Name, + inst->GetItem()->ID, + GetCleanName() + ); } safe_delete(inst); } diff --git a/zone/loot.cpp b/zone/loot.cpp index a330914e2..bb53ddc55 100644 --- a/zone/loot.cpp +++ b/zone/loot.cpp @@ -687,6 +687,7 @@ void NPC::RemoveItem(uint32 item_id, uint16 quantity, uint16 slot) LootItem *item = *cur; if (item->item_id == item_id && slot <= 0 && quantity <= 0) { m_loot_items.erase(cur); + safe_delete(item); UpdateEquipmentLight(); if (UpdateActiveLight()) { SendAppearancePacket(AppearanceType::Light, GetActiveLightType()); } return; @@ -695,7 +696,10 @@ void NPC::RemoveItem(uint32 item_id, uint16 quantity, uint16 slot) if (item->charges <= quantity) { m_loot_items.erase(cur); UpdateEquipmentLight(); - if (UpdateActiveLight()) { SendAppearancePacket(AppearanceType::Light, GetActiveLightType()); } + if (UpdateActiveLight()) { + SendAppearancePacket(AppearanceType::Light, GetActiveLightType()); + } + safe_delete(item); } else { item->charges -= quantity; diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index 1b11ebbe1..257f088e6 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -3449,7 +3449,9 @@ std::string QuestManager::varlink( linker.SetLinkType(EQ::saylink::SayLinkItemInst); linker.SetItemInst(item); - return linker.GenerateLink(); + auto link = linker.GenerateLink(); + safe_delete(item); + return link; } std::string QuestManager::getitemcomment(uint32 item_id) { diff --git a/zone/trading.cpp b/zone/trading.cpp index bdbb6326c..486c2f4ff 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -1800,7 +1800,13 @@ void Client::SendBarterWelcome() void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria) { - auto results = Bazaar::GetSearchResults(database, search_criteria, GetZoneID(), GetInstanceID()); + std::vector results = Bazaar::GetSearchResults( + database, + content_db, + search_criteria, + GetZoneID(), + GetInstanceID() + ); if (results.empty()) { SendBazaarDone(GetID()); return; @@ -1822,317 +1828,6 @@ void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria) SendBazaarDeliveryCosts(); } -void Client::SendBazaarResults( - uint32 trader_id, - uint32 in_class, - uint32 in_race, - uint32 item_stat, - uint32 item_slot, - uint32 item_type, - char item_name[64], - uint32 min_price, - uint32 max_price -) -{ - std::string search_values = " COUNT(item_id), trader.*, items.name "; - std::string search_criteria = " WHERE trader.item_id = items.id "; - - if (trader_id > 0) { - Client *trader = entity_list.GetClientByID(trader_id); - - if (trader) { - search_criteria.append(StringFormat(" AND trader.char_id = %i", trader->CharacterID())); - } - } - - if (min_price != 0) { - search_criteria.append(StringFormat(" AND trader.item_cost >= %i", min_price)); - } - - if (max_price != 0) { - search_criteria.append(StringFormat(" AND trader.item_cost <= %i", max_price)); - } - - if (strlen(item_name) > 0) { - char *safeName = RemoveApostrophes(item_name); - search_criteria.append(StringFormat(" AND items.name LIKE '%%%s%%'", safeName)); - safe_delete_array(safeName); - } - - if (in_class != 0xFFFFFFFF) { - search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.classes)), %i, 1) = 1", in_class)); - } - - if (in_race != 0xFFFFFFFF) { - search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.races)), %i, 1) = 1", in_race)); - } - - if (item_slot != 0xFFFFFFFF) { - search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.slots)), %i, 1) = 1", item_slot + 1)); - } - - switch (item_type) { - case 0xFFFFFFFF: - break; - case 0: - // 1H Slashing - search_criteria.append(" AND items.itemtype = 0 AND damage > 0"); - break; - case 31: - search_criteria.append(" AND items.itemclass = 2"); - break; - case 46: - search_criteria.append(" AND items.scrolleffect > 0 AND items.scrolleffect < 65000"); - break; - case 47: - search_criteria.append(" AND items.worneffect = 998"); - break; - case 48: - search_criteria.append(" AND items.worneffect >= 1298 AND items.worneffect <= 1307"); - break; - case 49: - search_criteria.append(" AND items.focuseffect > 0"); - break; - - default: - search_criteria.append(StringFormat(" AND items.itemtype = %i", item_type)); - } - - switch (item_stat) { - - case STAT_AC: - search_criteria.append(" AND items.ac > 0"); - search_values.append(", items.ac"); - break; - - case STAT_AGI: - search_criteria.append(" AND items.aagi > 0"); - search_values.append(", items.aagi"); - break; - - case STAT_CHA: - search_criteria.append(" AND items.acha > 0"); - search_values.append(", items.acha"); - break; - - case STAT_DEX: - search_criteria.append(" AND items.adex > 0"); - search_values.append(", items.adex"); - break; - - case STAT_INT: - search_criteria.append(" AND items.aint > 0"); - search_values.append(", items.aint"); - break; - - case STAT_STA: - search_criteria.append(" AND items.asta > 0"); - search_values.append(", items.asta"); - break; - - case STAT_STR: - search_criteria.append(" AND items.astr > 0"); - search_values.append(", items.astr"); - break; - - case STAT_WIS: - search_criteria.append(" AND items.awis > 0"); - search_values.append(", items.awis"); - break; - - case STAT_COLD: - search_criteria.append(" AND items.cr > 0"); - search_values.append(", items.cr"); - break; - - case STAT_DISEASE: - search_criteria.append(" AND items.dr > 0"); - search_values.append(", items.dr"); - break; - - case STAT_FIRE: - search_criteria.append(" AND items.fr > 0"); - search_values.append(", items.fr"); - break; - - case STAT_MAGIC: - search_criteria.append(" AND items.mr > 0"); - search_values.append(", items.mr"); - break; - - case STAT_POISON: - search_criteria.append(" AND items.pr > 0"); - search_values.append(", items.pr"); - break; - - case STAT_HP: - search_criteria.append(" AND items.hp > 0"); - search_values.append(", items.hp"); - break; - - case STAT_MANA: - search_criteria.append(" AND items.mana > 0"); - search_values.append(", items.mana"); - break; - - case STAT_ENDURANCE: - search_criteria.append(" AND items.endur > 0"); - search_values.append(", items.endur"); - break; - - case STAT_ATTACK: - search_criteria.append(" AND items.attack > 0"); - search_values.append(", items.attack"); - break; - - case STAT_HP_REGEN: - search_criteria.append(" AND items.regen > 0"); - search_values.append(", items.regen"); - break; - - case STAT_MANA_REGEN: - search_criteria.append(" AND items.manaregen > 0"); - search_values.append(", items.manaregen"); - break; - - case STAT_HASTE: - search_criteria.append(" AND items.haste > 0"); - search_values.append(", items.haste"); - break; - - case STAT_DAMAGE_SHIELD: - search_criteria.append(" AND items.damageshield > 0"); - search_values.append(", items.damageshield"); - break; - - default: - search_values.append(", 0"); - break; - } - - std::string query = StringFormat( - "SELECT %s, SUM(charges), items.stackable " - "FROM trader, items %s GROUP BY items.id, charges, char_id LIMIT %i", - search_values.c_str(), - search_criteria.c_str(), - RuleI(Bazaar, MaxSearchResults) - ); - - auto results = database.QueryDatabase(query); - - if (!results.Success()) { - return; - } - - LogTrading("SRCH: [{}]", query.c_str()); - - int Size = 0; - uint32 ID = 0; - - if (results.RowCount() == static_cast(RuleI(Bazaar, MaxSearchResults))) { - Message( - Chat::Yellow, - "Your search reached the limit of %i results. Please narrow your search down by selecting more options.", - RuleI(Bazaar, MaxSearchResults)); - } - - if (results.RowCount() == 0) { - auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); - BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *) outapp2->pBuffer; - brds->TraderID = ID; - brds->Type = BazaarSearchDone; - brds->Unknown008 = 0xFFFFFFFF; - brds->Unknown012 = 0xFFFFFFFF; - brds->Unknown016 = 0xFFFFFFFF; - QueuePacket(outapp2); - safe_delete(outapp2); - return; - } - - Size = results.RowCount() * sizeof(BazaarSearchResults_Struct); - auto buffer = new uchar[Size]; - uchar *bufptr = buffer; - memset(buffer, 0, Size); - - int Action = BazaarSearchResults; - uint32 Cost = 0; - int32 SerialNumber = 0; - char temp_buffer[64] = {0}; - int Count = 0; - uint32 StatValue = 0; - - for (auto &row = results.begin(); row != results.end(); ++row) { - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Action); - Count = Strings::ToInt(row[0]); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Count); - SerialNumber = Strings::ToInt(row[3]); - VARSTRUCT_ENCODE_TYPE(int32, bufptr, SerialNumber); - Client *Trader2 = entity_list.GetClientByCharID(Strings::ToInt(row[1])); - if (Trader2) { - ID = Trader2->GetID(); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, ID); - } - else { - LogTrading("Unable to find trader: [{}]\n", Strings::ToInt(row[1])); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 0); - } - Cost = Strings::ToInt(row[5]); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Cost); - StatValue = Strings::ToInt(row[8]); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, StatValue); - bool Stackable = Strings::ToInt(row[10]); - if (Stackable) { - int Charges = Strings::ToInt(row[9]); - sprintf(temp_buffer, "%s(%i)", row[7], Charges); - } - else { - sprintf(temp_buffer, "%s(%i)", row[7], Count); - } - - memcpy(bufptr, &temp_buffer, strlen(temp_buffer)); - - bufptr += 64; - - // Extra fields for SoD+ - // - if (Trader2) { - sprintf(temp_buffer, "%s", Trader2->GetName()); - } - else { - sprintf(temp_buffer, "Unknown"); - } - - memcpy(bufptr, &temp_buffer, strlen(temp_buffer)); - - bufptr += 64; - - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Strings::ToInt(row[1])); // ItemID - } - - auto outapp = new EQApplicationPacket(OP_BazaarSearch, Size); - - memcpy(outapp->pBuffer, buffer, Size); - - QueuePacket(outapp); - - safe_delete(outapp); - safe_delete_array(buffer); - - auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); - BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *) outapp2->pBuffer; - - brds->TraderID = ID; - brds->Type = BazaarSearchDone; - - brds->Unknown008 = 0xFFFFFFFF; - brds->Unknown012 = 0xFFFFFFFF; - brds->Unknown016 = 0xFFFFFFFF; - - QueuePacket(outapp2); - - safe_delete(outapp2); -} - static void UpdateTraderCustomerItemsAdded( uint32 customer_id, std::vector trader_items, @@ -3602,7 +3297,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati out_data->id = trader_item.id; strn0cpy(out_data->trader_buy_struct.buyer_name, GetCleanName(), sizeof(out_data->trader_buy_struct.buyer_name)); - worldserver.SendPacket(out_server.release()); + worldserver.SendPacket(out_server.get()); } void Client::SetBuyerWelcomeMessage(const char *welcome_message)