From fc79614facf6bb2a833548e8f2de6da43cce2650 Mon Sep 17 00:00:00 2001 From: Mitch Freeman <65987027+neckkola@users.noreply.github.com> Date: Sun, 26 May 2024 17:38:25 -0300 Subject: [PATCH] [Feature] Add RoF2 Bazaar Support (#4315) * Add RoF2 Bazaar Support Enable RoF2 bazaar features * Add augments to Trader Items * Cleanup Cleanup of formatting and unused functions * Update PlayerProfile for correct char_id in trader transactions. Further cleanup. * Add parcel delivery price functionality Add parcel delivery price functionality via rules and new delivery cost struct. * Add RoF support for bazaar window outside of bazaar with parcel delivery * Further Testing and ActiveTransaction added Further testing and a few fixes and messages added. Add active transaction check to ensure two clients cannot purchase from the bazaar window at the same time * Cleanup and Formatting updates Cleanup and Formatting updates * Update database manifest for the trader table against default peq trader table * Logs and formatting * Update bazaarsearch to be content_db aware * Fix crash * Simplify search * Search fixes * Push up more search logging * More search fixes * Formatting * Update trader_repository.h * Add Rule for Bazaar Parcel Delivery Add a rule Bazaar:EnableParcelDelivery to enable/disable bazaar parcel delivery. Default is True. * Fix crash * Update Bazaar Search Adds/Tested bazaar search with move to content_db - race, class, money, number of returned items, stats, name, slot, level, traders, local traders, specific trader. Outstanding - type, more stats to add (heroic, etc) * Formatting * Push * Update bazaarsearch to include all stats that are available in RoF2 * Update BazaarSearch Updates the bazaar search for item types. They should be working as per RoF2+ types. * Formatting * Final updates to BazaarSearch Add search by augmentation slots available on the item. This enables all but Prestige, which I believe are not implemented yet. * Add Titanium functionality correct ItemType Search Add Titanium /trader /bazaar functionality. Added itemtype=armor bazaar search. It was missed in the search work * Close off for loops --------- Co-authored-by: Akkadius --- common/CMakeLists.txt | 2 + common/bazaar.cpp | 359 +++ common/bazaar.h | 14 + common/database/database_update_manifest.cpp | 27 + common/emu_oplist.h | 1 + common/eq_constants.h | 62 +- common/eq_packet_structs.h | 269 ++- common/item_data.h | 135 +- common/item_instance.cpp | 136 ++ common/item_instance.h | 8 + common/patches/rof.cpp | 50 +- common/patches/rof2.cpp | 804 +++++-- common/patches/rof2_ops.h | 1 + common/patches/rof2_structs.h | 254 ++- common/patches/rof_structs.h | 22 +- common/patches/sod.cpp | 26 +- common/patches/sod_structs.h | 23 +- common/patches/sof.cpp | 30 +- common/patches/sof_structs.h | 22 +- common/patches/ss_define.h | 6 +- common/patches/titanium.cpp | 465 +++- common/patches/titanium_ops.h | 4 + common/patches/titanium_structs.h | 116 +- common/patches/uf.cpp | 470 +++- common/patches/uf_ops.h | 4 + common/patches/uf_structs.h | 114 +- .../base/base_trader_repository.h | 217 +- common/repositories/items_repository.h | 39 +- common/repositories/trader_repository.h | 248 +- common/ruletypes.h | 3 + common/servertalk.h | 24 + common/strings.cpp | 9 + common/strings.h | 1 + common/version.h | 2 +- utils/patches/patch_RoF2.conf | 1 + world/zoneserver.cpp | 13 + zone/bonuses.cpp | 4 +- zone/client.cpp | 14 +- zone/client.h | 52 +- zone/client_packet.cpp | 499 ++-- zone/client_process.cpp | 10 +- zone/gm_commands/parcels.cpp | 46 +- zone/gm_commands/show/inventory.cpp | 6 +- zone/inventory.cpp | 29 +- zone/parcels.cpp | 2 +- zone/string_ids.h | 4 + zone/trading.cpp | 2028 ++++++++++------- zone/worldserver.cpp | 96 +- zone/zone.cpp | 3 +- zone/zonedb.cpp | 273 +-- zone/zonedb.h | 4 +- 51 files changed, 4793 insertions(+), 2258 deletions(-) create mode 100644 common/bazaar.cpp create mode 100644 common/bazaar.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index eee1c7562..07d9e2358 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -2,6 +2,7 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.12) SET(common_sources base_packet.cpp + bazaar.cpp classes.cpp cli/eqemu_command_handler.cpp compression.cpp @@ -501,6 +502,7 @@ SET(repositories SET(common_headers additive_lagged_fibonacci_engine.h + bazaar.h base_packet.h bodytypes.h classes.h diff --git a/common/bazaar.cpp b/common/bazaar.cpp new file mode 100644 index 000000000..5db33e537 --- /dev/null +++ b/common/bazaar.cpp @@ -0,0 +1,359 @@ +#include "bazaar.h" + +#include "../../common/item_instance.h" +#include "repositories/trader_repository.h" +#include + +std::vector +Bazaar::GetSearchResults( + SharedDatabase &db, + BazaarSearchCriteria_Struct search, + uint32 char_zone_id +) +{ + LogTrading( + "Searching for items with search criteria - item_name [{}] min_cost [{}] max_cost [{}] min_level [{}] " + "max_level [{}] max_results [{}] prestige [{}] augment [{}] trader_entity_id [{}] trader_id [{}] " + "search_scope [{}] char_zone_id [{}]", + search.item_name, + search.min_cost, + search.max_cost, + search.min_level, + search.max_level, + search.max_results, + search.prestige, + search.augment, + search.trader_entity_id, + search.trader_id, + search.search_scope, + char_zone_id + ); + + std::string search_criteria_trader("TRUE "); + + if (search.search_scope == NonRoFBazaarSearchScope) { + search_criteria_trader.append( + fmt::format( + " AND trader.char_entity_id = {} AND trader.char_zone_id = {}", + search.trader_entity_id, + Zones::BAZAAR + ) + ); + } + else if (search.search_scope == Local_Scope) { + search_criteria_trader.append(fmt::format(" AND trader.char_zone_id = {}", char_zone_id)); + } + else if (search.trader_id > 0) { + search_criteria_trader.append(fmt::format(" AND trader.char_id = {}", search.trader_id)); + } + if (search.min_cost != 0) { + search_criteria_trader.append(fmt::format(" AND trader.item_cost >= {}", search.min_cost)); + } + if (search.max_cost != 0) { + 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)); + // } + + 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 " + "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() + ); + + std::vector all_entries; + + auto results = db.QueryDatabase(query); + + if (!results.Success()) { + return all_entries; + } + + struct ItemSearchType { + EQ::item::ItemType type; + bool condition; + }; + + struct AddititiveSearchCriteria { + bool should_check; + bool condition; + }; + + for (auto row: results) { + BazaarSearchResultsFromDB_Struct r{}; + + 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_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}, + {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(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); + all_entries.push_back(r); + } + + if (all_entries.size() > search.max_results) { + all_entries.resize(search.max_results); + } + + LogTrading("Returning [{}] items from search results", all_entries.size()); + + return all_entries; +} diff --git a/common/bazaar.h b/common/bazaar.h new file mode 100644 index 000000000..6ce906724 --- /dev/null +++ b/common/bazaar.h @@ -0,0 +1,14 @@ +#ifndef EQEMU_BAZAAR_H +#define EQEMU_BAZAAR_H + +#include +#include "shareddb.h" + +class Bazaar { +public: + static std::vector + GetSearchResults(SharedDatabase &db, BazaarSearchCriteria_Struct search, unsigned int char_zone_id); +}; + + +#endif //EQEMU_BAZAAR_H diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index cf6f80645..9dbd4b1ad 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5635,6 +5635,33 @@ ALTER TABLE `npc_spells_entries` ADD `content_flags_disabled` varchar(100) NULL; )", .content_schema_update = true }, + ManifestEntry{ + .version = 9280, + .description = "2024_05_11_update_trader_support.sql", + .check = "SHOW COLUMNS FROM `trader` LIKE 'aug_slot_1'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `trader` + ADD COLUMN `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + CHANGE COLUMN `char_id` `char_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `id`, + CHANGE COLUMN `item_id` `item_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `char_id`, + ADD COLUMN `aug_slot_1` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `item_id`, + ADD COLUMN `aug_slot_2` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `aug_slot_1`, + ADD COLUMN `aug_slot_3` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `aug_slot_2`, + ADD COLUMN `aug_slot_4` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `aug_slot_3`, + ADD COLUMN `aug_slot_5` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `aug_slot_4`, + ADD COLUMN `aug_slot_6` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `aug_slot_5`, + CHANGE COLUMN `serialnumber` `item_sn` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `aug_slot_6`, + CHANGE COLUMN `charges` `item_charges` INT(11) NOT NULL DEFAULT '0' AFTER `item_sn`, + ADD COLUMN `char_entity_id` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `slot_id`, + ADD COLUMN `char_zone_id` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `char_entity_id`, + ADD COLUMN `active_transaction` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0 AFTER `char_zone_id`, + DROP PRIMARY KEY, + ADD PRIMARY KEY (`id`), + ADD INDEX `charid_slotid` (`char_id`, `slot_id`); +)" + } // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/emu_oplist.h b/common/emu_oplist.h index c45cac1ae..0d09c6bdf 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -557,6 +557,7 @@ N(OP_TradeBusy), N(OP_TradeCoins), N(OP_TradeMoneyUpdate), N(OP_Trader), +N(OP_TraderBulkSend), N(OP_TraderBuy), N(OP_TraderDelItem), N(OP_TradeRequest), diff --git a/common/eq_constants.h b/common/eq_constants.h index e378f5626..117ac4d26 100644 --- a/common/eq_constants.h +++ b/common/eq_constants.h @@ -772,27 +772,47 @@ typedef enum { FilterShowSelfOnly } eqFilterMode; -#define STAT_STR 0 -#define STAT_STA 1 -#define STAT_AGI 2 -#define STAT_DEX 3 -#define STAT_INT 4 -#define STAT_WIS 5 -#define STAT_CHA 6 -#define STAT_MAGIC 7 -#define STAT_COLD 8 -#define STAT_FIRE 9 -#define STAT_POISON 10 -#define STAT_DISEASE 11 -#define STAT_MANA 12 -#define STAT_HP 13 -#define STAT_AC 14 -#define STAT_ENDURANCE 15 -#define STAT_ATTACK 16 -#define STAT_HP_REGEN 17 -#define STAT_MANA_REGEN 18 -#define STAT_HASTE 19 -#define STAT_DAMAGE_SHIELD 20 +#define STAT_STR 0 +#define STAT_STA 1 +#define STAT_AGI 2 +#define STAT_DEX 3 +#define STAT_INT 4 +#define STAT_WIS 5 +#define STAT_CHA 6 +#define STAT_MAGIC 7 +#define STAT_COLD 8 +#define STAT_FIRE 9 +#define STAT_POISON 10 +#define STAT_DISEASE 11 +#define STAT_MANA 12 +#define STAT_HP 13 +#define STAT_AC 14 +#define STAT_ENDURANCE 15 +#define STAT_ATTACK 16 +#define STAT_HP_REGEN 17 +#define STAT_MANA_REGEN 18 +#define STAT_HASTE 19 +#define STAT_DAMAGE_SHIELD 20 +#define STAT_DS_MITIGATION 22 +#define STAT_HEAL_AMOUNT 23 +#define STAT_SPELL_DAMAGE 24 +#define STAT_CLAIRVOYANCE 25 +#define STAT_HEROIC_AGILITY 26 +#define STAT_HEROIC_CHARISMA 27 +#define STAT_HEROIC_DEXTERITY 28 +#define STAT_HEROIC_INTELLIGENCE 29 +#define STAT_HEROIC_STAMINA 30 +#define STAT_HEROIC_STRENGTH 31 +#define STAT_HEROIC_WISDOM 32 +#define STAT_BASH 33 +#define STAT_BACKSTAB 34 +#define STAT_DRAGON_PUNCH 35 +#define STAT_EAGLE_STRIKE 36 +#define STAT_FLYING_KICK 37 +#define STAT_KICK 38 +#define STAT_ROUND_KICK 39 +#define STAT_TIGER_CLAW 40 +#define STAT_FRENZY 41 /* ** Recast timer types. Used as an off set to charProfileStruct timers. diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 12e2c3d35..38655fd78 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -31,7 +31,6 @@ #include "../cereal/include/cereal/types/string.hpp" #include "../cereal/include/cereal/types/vector.hpp" - static const uint32 BUFF_COUNT = 42; static const uint32 PET_BUFF_COUNT = 30; static const uint32 MAX_MERC = 100; @@ -323,6 +322,7 @@ union bool targetable_with_hotkey; bool show_name; bool guild_show; + bool trader; }; struct PlayerState_Struct { @@ -1119,7 +1119,7 @@ struct PlayerProfile_Struct /*19558*/ uint8 guildAutoconsent; // 0=off, 1=on /*19559*/ uint8 unknown19595[5]; // ***Placeholder (6/29/2005) /*19564*/ uint32 RestTimer; -/*19568*/ +/*19568*/ uint32 char_id; // Found as part of bazaar revamp (5/15/2024) // All player profile packets are translated and this overhead is ignored in out-bound packets PlayerProfile_Struct() : m_player_profile_version(EQ::versions::MobVersion::Unknown) { } @@ -3002,26 +3002,26 @@ struct EnvDamage2_Struct { // enum { - BazaarTrader_StartTraderMode = 1, - BazaarTrader_EndTraderMode = 2, - BazaarTrader_UpdatePrice = 3, - BazaarTrader_EndTransaction = 4, - BazaarSearchResults = 7, - BazaarWelcome = 9, - BazaarBuyItem = 10, - BazaarTrader_ShowItems = 11, - BazaarSearchDone = 12, + BazaarTrader_StartTraderMode = 1, + BazaarTrader_EndTraderMode = 2, + BazaarTrader_UpdatePrice = 3, + BazaarTrader_EndTransaction = 4, + BazaarSearchResults = 7, + BazaarWelcome = 9, + BazaarBuyItem = 10, + BazaarTrader_ShowItems = 11, + BazaarSearchDone = 12, BazaarTrader_CustomerBrowsing = 13, - BazaarInspectItem = 18, - BazaarSearchDone2 = 19, + BazaarInspectItem = 18, + BazaarSearchDone2 = 19, BazaarTrader_StartTraderMode2 = 22 }; enum { - BazaarPriceChange_Fail = 0, + BazaarPriceChange_Fail = 0, BazaarPriceChange_UpdatePrice = 1, - BazaarPriceChange_RemoveItem = 2, - BazaarPriceChange_AddItem = 3 + BazaarPriceChange_RemoveItem = 2, + BazaarPriceChange_AddItem = 3 }; struct BazaarWindowStart_Struct { @@ -3032,31 +3032,41 @@ struct BazaarWindowStart_Struct { struct BazaarWelcome_Struct { - BazaarWindowStart_Struct Beginning; - uint32 Traders; - uint32 Items; - uint32 Unknown012; - uint32 Unknown016; + uint32 action; + uint32 traders; + uint32 items; + uint32 unknown_012; + uint32 unknown_016; }; -struct BazaarSearch_Struct { - BazaarWindowStart_Struct Beginning; - uint32 TraderID; - uint32 Class_; - uint32 Race; - uint32 ItemStat; - uint32 Slot; - uint32 Type; - char Name[64]; - uint32 MinPrice; - uint32 MaxPrice; - uint32 Minlevel; - uint32 MaxLlevel; +struct BazaarSearchCriteria_Struct { + /*000*/ uint32 action{0}; + /*004*/ uint32 search_scope{0}; // 1 all traders 0 local traders + /*008*/ uint32 unknown_008{0}; + /*012*/ uint32 unknown_012{0}; + /*016*/ uint32 trader_id{0}; + /*020*/ uint32 _class{0}; + /*024*/ uint32 race{0}; + /*028*/ uint32 item_stat{0}; + /*032*/ uint32 slot{0}; + /*036*/ uint32 type{0}; + /*040*/ char item_name[64]{""}; + /*104*/ uint32 min_cost{0}; + /*108*/ uint32 max_cost{0}; + /*112*/ uint32 min_level{1}; + /*116*/ uint32 max_level{0}; + /*120*/ uint32 max_results{0}; + /*124*/ uint32 prestige{0}; + /*128*/ uint32 augment{0}; + /*132*/ uint32 trader_entity_id{0}; }; -struct BazaarInspect_Struct{ - uint32 ItemID; - uint32 Unknown004; - char Name[64]; + +struct BazaarInspect_Struct { + uint32 action; + char player_name[64]; + uint32 serial_number; + uint32 item_id; + uint32 trader_id; }; struct NewBazaarInspect_Struct { @@ -3076,6 +3086,14 @@ struct BazaarReturnDone_Struct{ uint32 Unknown012; uint32 Unknown016; }; + +struct BazaarDeliveryCost_Struct { + uint32 action; + uint16 voucher_delivery_cost; + float parcel_deliver_cost; //percentage of item cost + uint32 unknown_010; +}; + struct BazaarSearchResults_Struct { /*000*/ BazaarWindowStart_Struct Beginning; /*004*/ uint32 NumItems; @@ -3088,8 +3106,10 @@ struct BazaarSearchResults_Struct { // New fields for SoD+, stripped off when encoding for older clients. char SellerName[64]; uint32 ItemID; + uint32 ItemID2; }; + // Barter/Buyer // // @@ -3389,32 +3409,31 @@ struct WhoAllReturnStruct { }; struct Trader_Struct { -/*000*/ uint32 Code; -/*004*/ uint32 Unknown004; -/*008*/ uint64 Items[80]; -/*648*/ uint32 ItemCost[80]; +/*000*/ uint32 action; +/*004*/ uint32 unknown_004; +/*008*/ uint64 items[EQ::invtype::BAZAAR_SIZE]; +/*648*/ uint32 item_cost[EQ::invtype::BAZAAR_SIZE]; }; struct ClickTrader_Struct { -/*000*/ uint32 Code; -/*004*/ uint32 Unknown004; -/*008*/ int64 SerialNumber[80]; -/*648*/ uint32 ItemCost[80]; +/*000*/ uint32 action; +/*004*/ uint32 unknown_004; +/*008*/ int64 serial_number[EQ::invtype::BAZAAR_SIZE] {}; +/*648*/ uint32 item_cost[EQ::invtype::BAZAAR_SIZE] {}; }; struct GetItems_Struct{ - uint32 Items[80]; - int32 SerialNumber[80]; - int32 Charges[80]; + uint32 items[EQ::invtype::BAZAAR_SIZE]; + int32 serial_number[EQ::invtype::BAZAAR_SIZE]; + int32 charges[EQ::invtype::BAZAAR_SIZE]; }; -struct BecomeTrader_Struct -{ -/*000*/ uint32 ID; -/*004*/ uint32 Code; -/*008*/ char Name[64]; -/*072*/ uint32 Unknown072; // Observed 0x33,0x91 etc on zone-in, 0x00 when sent for a new trader after zone-in -/*076*/ +struct BecomeTrader_Struct { + uint32 action; + uint32 zone_id; + uint32 trader_id; + uint32 entity_id; + char trader_name[64]; }; struct TraderStatus_Struct{ @@ -3424,20 +3443,30 @@ struct TraderStatus_Struct{ }; struct Trader_ShowItems_Struct{ -/*000*/ uint32 Code; -/*004*/ uint32 TraderID; +/*000*/ uint32 action; +/*004*/ uint32 entity_id; /*008*/ uint32 Unknown08[3]; /*020*/ }; -struct TraderBuy_Struct{ -/*000*/ uint32 Action; -/*004*/ uint32 TraderID; -/*008*/ uint32 ItemID; -/*012*/ uint32 AlreadySold; -/*016*/ uint32 Price; -/*020*/ uint32 Quantity; -/*024*/ char ItemName[64]; +struct TraderBuy_Struct { +/*000*/ uint32 action; +/*004*/ uint32 method; +/*008*/ uint32 sub_action; +/*012*/ uint32 unknown_012; +/*016*/ uint32 trader_id; +/*020*/ char buyer_name[64]; +/*084*/ char seller_name[64]; +/*148*/ char unknown_148[32]; +/*180*/ char item_name[64]; +/*244*/ char serial_number[17]; +/*261*/ char unknown_261[3]; +/*264*/ uint32 item_id; +/*268*/ uint32 price; +/*272*/ uint32 already_sold; +/*276*/ uint32 unknown_276; +/*280*/ uint32 quantity; +/*284*/ }; struct TraderItemUpdate_Struct{ @@ -3465,15 +3494,15 @@ struct MoneyUpdate_Struct{ }; struct TraderDelItem_Struct{ - uint32 Unknown000; - uint32 TraderID; - uint32 ItemID; - uint32 Unknown012; + uint32 unknown_000; + uint32 trader_id; + uint32 item_id; + uint32 unknown_012; }; struct TraderClick_Struct{ -/*000*/ uint32 TraderID; -/*004*/ uint32 Code; +/*000*/ uint32 Code; +/*004*/ uint32 TraderID; /*008*/ uint32 Unknown008; /*012*/ uint32 Approval; /*016*/ @@ -5991,6 +6020,102 @@ struct UnderWorld { /* 16 */ }; +enum BazaarTraderBarterActions { + TraderOff = 0, + TraderOn = 1, + PriceUpdate = 3, + EndTransaction = 4, + BazaarSearch = 7, + WelcomeMessage = 9, + BuyTraderItem = 10, + ListTraderItems = 11, + CustomerBrowsing = 13, + BazaarInspect = 18, + ItemMove = 19, + TraderAck2 = 22, + AddTraderToBazaarWindow = 24, + RemoveTraderFromBazaarWindow = 25, + ClickTrader = 28, + DeliveryCostUpdate = 29 +}; + +enum BazaarPurchaseActions { + ByVendor = 0, + ByParcel = 1, + ByDirectToInventory = 2 +}; + +enum BazaarPurchaseSubActions { + Success = 0, + Failed = 1, + DataOutDated = 3, + TooManyParcels = 5, + TransactionInProgress = 6, + InsufficientFunds = 7 +}; + +enum BazaarSearchScopes { + Local_Scope = 0, + AllTraders_Scope = 1, + NonRoFBazaarSearchScope = 99 +}; + +struct BazaarSearchResultsFromDB_Struct { + uint32 count; + uint32 trader_id; + uint32 item_id; + uint32 serial_number; + uint32 charges; + uint32 cost; + uint32 slot_id; + uint32 icon_id; + uint32 sum_charges; + uint32 trader_zone_id; + uint32 trader_entity_id; + uint32 item_stat; + bool stackable; + std::string item_name; + std::string serial_number_RoF; + std::string trader_name; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(count), + CEREAL_NVP(trader_id), + CEREAL_NVP(item_id), + CEREAL_NVP(serial_number), + CEREAL_NVP(charges), + CEREAL_NVP(cost), + CEREAL_NVP(slot_id), + CEREAL_NVP(icon_id), + CEREAL_NVP(sum_charges), + CEREAL_NVP(trader_zone_id), + CEREAL_NVP(trader_entity_id), + CEREAL_NVP(item_stat), + CEREAL_NVP(stackable), + CEREAL_NVP(item_name), + CEREAL_NVP(serial_number_RoF), + CEREAL_NVP(trader_name) + ); + } +}; + +struct BazaarSearchMessaging_Struct { + uint32 action; + char payload[]; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(payload) + ); + } +}; + // Restore structure packing to default #pragma pack() diff --git a/common/item_data.h b/common/item_data.h index f0ce79fb0..449a86759 100644 --- a/common/item_data.h +++ b/common/item_data.h @@ -58,72 +58,75 @@ namespace EQ }; enum ItemType : uint8 { -/*9138*/ ItemType1HSlash = 0, -/*9141*/ ItemType2HSlash, -/*9140*/ ItemType1HPiercing, -/*9139*/ ItemType1HBlunt, -/*9142*/ ItemType2HBlunt, -/*5504*/ ItemTypeBow, // 5 -/*----*/ ItemTypeUnknown1, -/*----*/ ItemTypeLargeThrowing, -/*5505*/ ItemTypeShield, -/*5506*/ ItemTypeScroll, -/*5507*/ ItemTypeArmor, // 10 -/*5508*/ ItemTypeMisc, // a lot of random crap has this item use. -/*7564*/ ItemTypeLockPick, -/*----*/ ItemTypeUnknown2, -/*5509*/ ItemTypeFood, -/*5510*/ ItemTypeDrink, // 15 -/*5511*/ ItemTypeLight, -/*5512*/ ItemTypeCombinable, // not all stackable items are this use... -/*5513*/ ItemTypeBandage, -/*----*/ ItemTypeSmallThrowing, -/*----*/ ItemTypeSpell, // 20 // spells and tomes -/*5514*/ ItemTypePotion, -/*----*/ ItemTypeUnknown3, -/*0406*/ ItemTypeWindInstrument, -/*0407*/ ItemTypeStringedInstrument, -/*0408*/ ItemTypeBrassInstrument, // 25 -/*0405*/ ItemTypePercussionInstrument, -/*5515*/ ItemTypeArrow, -/*----*/ ItemTypeUnknown4, -/*5521*/ ItemTypeJewelry, -/*----*/ ItemTypeSkull, // 30 -/*5516*/ ItemTypeBook, // skill-up tomes/books? (would probably need a pp flag if true...) -/*5517*/ ItemTypeNote, -/*5518*/ ItemTypeKey, -/*----*/ ItemTypeCoin, -/*5520*/ ItemType2HPiercing, // 35 -/*----*/ ItemTypeFishingPole, -/*----*/ ItemTypeFishingBait, -/*5519*/ ItemTypeAlcohol, -/*----*/ ItemTypeKey2, // keys and satchels?? (questable keys?) -/*----*/ ItemTypeCompass, // 40 -/*----*/ ItemTypeUnknown5, -/*----*/ ItemTypePoison, // might be wrong, but includes poisons -/*----*/ ItemTypeUnknown6, -/*----*/ ItemTypeUnknown7, -/*5522*/ ItemTypeMartial, // 45 -/*----*/ ItemTypeUnknown8, -/*----*/ ItemTypeUnknown9, -/*----*/ ItemTypeUnknown10, -/*----*/ ItemTypeUnknown11, -/*----*/ ItemTypeSinging, // 50 -/*5750*/ ItemTypeAllInstrumentTypes, -/*5776*/ ItemTypeCharm, -/*----*/ ItemTypeDye, -/*----*/ ItemTypeAugmentation, -/*----*/ ItemTypeAugmentationSolvent, // 55 -/*----*/ ItemTypeAugmentationDistiller, -/*----*/ ItemTypeUnknown12, -/*----*/ ItemTypeFellowshipKit, -/*----*/ ItemTypeUnknown13, -/*----*/ ItemTypeRecipe, // 60 -/*----*/ ItemTypeAdvancedRecipe, -/*----*/ ItemTypeJournal, // only one(1) database entry -/*----*/ ItemTypeAltCurrency, // alt-currency (as opposed to coinage) -/*5881*/ ItemTypePerfectedAugmentationDistiller, -/*----*/ ItemTypeCount + /*9138*/ ItemType1HSlash = 0, + /*9141*/ ItemType2HSlash, + /*9140*/ ItemType1HPiercing, + /*9139*/ ItemType1HBlunt, + /*9142*/ ItemType2HBlunt, + /*5504*/ ItemTypeBow, // 5 + /*----*/ ItemTypeUnknown1, + /*----*/ ItemTypeLargeThrowing, + /*5505*/ ItemTypeShield, + /*5506*/ ItemTypeScroll, + /*5507*/ ItemTypeArmor, // 10 + /*5508*/ ItemTypeMisc, // a lot of random crap has this item use. + /*7564*/ ItemTypeLockPick, + /*----*/ ItemTypeUnknown2, + /*5509*/ ItemTypeFood, + /*5510*/ ItemTypeDrink, // 15 + /*5511*/ ItemTypeLight, + /*5512*/ ItemTypeCombinable, // not all stackable items are this use... + /*5513*/ ItemTypeBandage, + /*----*/ ItemTypeSmallThrowing, + /*----*/ ItemTypeSpell, // 20 // spells and tomes + /*5514*/ ItemTypePotion, + /*----*/ ItemTypeUnknown3, + /*0406*/ ItemTypeWindInstrument, + /*0407*/ ItemTypeStringedInstrument, + /*0408*/ ItemTypeBrassInstrument, // 25 + /*0405*/ ItemTypePercussionInstrument, + /*5515*/ ItemTypeArrow, + /*----*/ ItemTypeUnknown4, + /*5521*/ ItemTypeJewelry, + /*----*/ ItemTypeSkull, // 30 + /*5516*/ ItemTypeBook, // skill-up tomes/books? (would probably need a pp flag if true...) + /*5517*/ ItemTypeNote, + /*5518*/ ItemTypeKey, + /*----*/ ItemTypeCoin, + /*5520*/ ItemType2HPiercing, // 35 + /*----*/ ItemTypeFishingPole, + /*----*/ ItemTypeFishingBait, + /*5519*/ ItemTypeAlcohol, + /*----*/ ItemTypeKey2, // keys and satchels?? (questable keys?) + /*----*/ ItemTypeCompass, // 40 + /*----*/ ItemTypeUnknown5, + /*----*/ ItemTypePoison, // might be wrong, but includes poisons + /*----*/ ItemTypeUnknown6, + /*----*/ ItemTypeUnknown7, + /*5522*/ ItemTypeMartial, // 45 + /*----*/ ItemTypeAllEffects, + /*----*/ ItemTypeUnknown9, + /*----*/ ItemTypeUnknown10, + /*----*/ ItemTypeFocusEffect, + /*----*/ ItemTypeSinging, // 50 + /*5750*/ ItemTypeAllInstrumentTypes, + /*5776*/ ItemTypeCharm, + /*----*/ ItemTypeDye, + /*----*/ ItemTypeAugmentation, + /*----*/ ItemTypeAugmentationSolvent, // 55 + /*----*/ ItemTypeAugmentationDistiller, + /*----*/ ItemTypeAlternateAbility, + /*----*/ ItemTypeFellowshipKit, + /*----*/ ItemTypeUnknown13, + /*----*/ ItemTypeRecipe, // 60 + /*----*/ ItemTypeAdvancedRecipe, + /*----*/ ItemTypeJournal, // only one(1) database entry + /*----*/ ItemTypeAltCurrency, // alt-currency (as opposed to coinage) + /*5881*/ ItemTypePerfectedAugmentationDistiller, + /*----*/ ItemTypeCount, + /*----*/ ItemTypeCollectible, + /*----*/ ItemTypeContainer, + /*----*/ ItemTypeAll = 0xFF /* Unknowns: diff --git a/common/item_instance.cpp b/common/item_instance.cpp index 01cbfb379..5dcd55cfe 100644 --- a/common/item_instance.cpp +++ b/common/item_instance.cpp @@ -1812,6 +1812,142 @@ std::vector EQ::ItemInstance::GetAugmentIDs() const return augments; } +int EQ::ItemInstance::GetItemRegen(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->Regen; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemRegen(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemManaRegen(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->ManaRegen; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemManaRegen(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemDamageShield(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->DamageShield; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemDamageShield(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemDSMitigation(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->DSMitigation; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemDSMitigation(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemHealAmt(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->HealAmt; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemHealAmt(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemSpellDamage(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->SpellDmg; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemSpellDamage(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemClairvoyance(bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->Clairvoyance; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemClairvoyance(); + } + } + } + } + return stat; +} + +int EQ::ItemInstance::GetItemSkillsStat(EQ::skills::SkillType skill, bool augments) const +{ + int stat = 0; + const auto item = GetItem(); + if (item) { + stat = item->ExtraDmgSkill == skill ? item->ExtraDmgAmt : 0; + if (augments) { + for (int i = invaug::SOCKET_BEGIN; i <= invaug::SOCKET_END; ++i) { + if (GetAugment(i)) { + stat += GetAugment(i)->GetItemSkillsStat(skill); + } + } + } + } + return stat; +} + // // class EvolveInfo // diff --git a/common/item_instance.h b/common/item_instance.h index 530512be2..a451568bd 100644 --- a/common/item_instance.h +++ b/common/item_instance.h @@ -270,6 +270,7 @@ namespace EQ int GetItemMagical(bool augments = false) const; int GetItemHP(bool augments = false) const; int GetItemMana(bool augments = false) const; + int GetItemManaRegen(bool augments = false) const; int GetItemEndur(bool augments = false) const; int GetItemAttack(bool augments = false) const; int GetItemStr(bool augments = false) const; @@ -299,6 +300,13 @@ namespace EQ int GetItemHeroicDR(bool augments = false) const; int GetItemHeroicCorrup(bool augments = false) const; int GetItemHaste(bool augments = false) const; + int GetItemRegen(bool augments = false) const; + int GetItemDamageShield(bool augments = false) const; + int GetItemDSMitigation(bool augments = false) const; + int GetItemHealAmt(bool augments = false) const; + int GetItemSpellDamage(bool augments = false) const; + int GetItemClairvoyance(bool augments = false) const; + int GetItemSkillsStat(EQ::skills::SkillType skill, bool augments = false) const; uint32 GetItemGuildFavor() const; std::vector GetAugmentIDs() const; diff --git a/common/patches/rof.cpp b/common/patches/rof.cpp index b9dfcedac..7d16d95e4 100644 --- a/common/patches/rof.cpp +++ b/common/patches/rof.cpp @@ -3494,14 +3494,13 @@ namespace RoF ENCODE_LENGTH_EXACT(ClickTrader_Struct); SETUP_DIRECT_ENCODE(ClickTrader_Struct, structs::ClickTrader_Struct); - eq->Code = emu->Code; - // Live actually has 200 items now, but 80 is the most our internal struct supports - for (uint32 i = 0; i < 200; i++) + eq->Code = emu->action; + for (uint32 i = 0; i < RoF::invtype::BAZAAR_SIZE; i++) { strncpy(eq->items[i].SerialNumber, "0000000000000000", sizeof(eq->items[i].SerialNumber)); eq->items[i].Unknown18 = 0; if (i < 80) { - eq->ItemCost[i] = emu->ItemCost[i]; + eq->ItemCost[i] = emu->item_cost[i]; } else { eq->ItemCost[i] = 0; @@ -3515,9 +3514,9 @@ namespace RoF ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); - eq->Code = emu->Code; + eq->Code = emu->action; strncpy(eq->SerialNumber, "0000000000000000", sizeof(eq->SerialNumber)); - eq->TraderID = emu->TraderID; + eq->TraderID = emu->entity_id; eq->Stacksize = 0; eq->Price = 0; @@ -3543,13 +3542,13 @@ namespace RoF ENCODE_LENGTH_EXACT(TraderBuy_Struct); SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); - OUT(Action); - OUT(Price); - OUT(TraderID); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(Quantity); - OUT(AlreadySold); + OUT(action); + OUT(price); + OUT(trader_id); + memcpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + OUT(item_id); + OUT(quantity); + OUT(already_sold); FINISH_ENCODE(); } @@ -5041,12 +5040,11 @@ namespace RoF SETUP_DIRECT_DECODE(ClickTrader_Struct, structs::ClickTrader_Struct); MEMSET_IN(ClickTrader_Struct); - emu->Code = eq->Code; - // Live actually has 200 items now, but 80 is the most our internal struct supports - for (uint32 i = 0; i < 80; i++) + emu->action = eq->Code; + for (uint32 i = 0; i < RoF::invtype::BAZAAR_SIZE; i++) { - emu->SerialNumber[i] = 0; // eq->SerialNumber[i]; - emu->ItemCost[i] = eq->ItemCost[i]; + emu->serial_number[i] = 0; // eq->SerialNumber[i]; + emu->item_cost[i] = eq->ItemCost[i]; } FINISH_DIRECT_DECODE(); @@ -5057,8 +5055,8 @@ namespace RoF SETUP_DIRECT_DECODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); MEMSET_IN(Trader_ShowItems_Struct); - emu->Code = eq->Code; - emu->TraderID = eq->TraderID; + emu->action = eq->Code; + emu->entity_id = eq->TraderID; FINISH_DIRECT_DECODE(); } @@ -5080,12 +5078,12 @@ namespace RoF SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); MEMSET_IN(TraderBuy_Struct); - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); + IN(action); + IN(price); + IN(trader_id); + memcpy(emu->item_name, eq->item_name, sizeof(emu->item_name)); + IN(item_id); + IN(quantity); FINISH_DIRECT_DECODE(); } diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index ac5f61fa3..d853156f4 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -398,51 +398,182 @@ namespace RoF2 EQApplicationPacket *in = *p; *p = nullptr; - char *Buffer = (char *)in->pBuffer; + uint32 action = *(uint32 *) in->pBuffer; - uint8 SubAction = VARSTRUCT_DECODE_TYPE(uint8, Buffer); + switch (action) { + case BazaarSearch: { + LogTrading("(RoF2) BazaarSearch action [{}]", action); + std::vector results{}; + auto bsms = (BazaarSearchMessaging_Struct *) in->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(bsms->payload), + in->size - sizeof(BazaarSearchMessaging_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(results); - if (SubAction != BazaarSearchResults) - { - dest->FastQueuePacket(&in, ack_req); - return; + auto name_size = 0; + for (auto const &i: results) { + name_size += i.item_name.length() + 1; + } + + auto p_size = 41 * results.size() + name_size + 14; + auto buffer = std::make_unique(p_size); + auto bufptr = buffer.get(); + + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 0); + VARSTRUCT_ENCODE_TYPE(uint16, bufptr, results[0].trader_zone_id); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, results[0].trader_id); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, results.size()); + + for (auto i: results) { + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.trader_id); //trader ID + VARSTRUCT_ENCODE_STRING(bufptr, i.serial_number_RoF.c_str()); //serial + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.cost); //cost + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.stackable ? i.charges : i.count); //quantity + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.item_id); //ID + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.icon_id); //icon + VARSTRUCT_ENCODE_STRING(bufptr, i.item_name.c_str()); //name + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.item_stat); //itemstat + } + + safe_delete(in->pBuffer); + in->size = p_size; + in->pBuffer = (uchar *) buffer.get(); + dest->QueuePacket(in); + + break; + } + case BazaarInspect: { + LogTrading("(RoF2) BazaarInspect action [{}]", action); + dest->FastQueuePacket(&in, ack_req); + break; + } + case WelcomeMessage: { + auto buffer = std::make_unique(sizeof(structs::BazaarWelcome_Struct)); + auto emu = (BazaarWelcome_Struct *) in->pBuffer; + auto eq = (structs::BazaarWelcome_Struct *) buffer.get(); + + eq->action = structs::RoF2BazaarTraderBuyerActions::WelcomeMessage; + eq->num_of_traders = emu->traders; + eq->num_of_items = emu->items; + + safe_delete(in->pBuffer); + in->SetOpcode(OP_TraderShop); + in->size = sizeof(structs::BazaarWelcome_Struct); + in->pBuffer = (uchar *) buffer.get(); + + LogTrading("(RoF2) WelcomeMessage action [{}]", action); + dest->QueuePacket(in); + + break; + } + case DeliveryCostUpdate: { + auto data = (BazaarDeliveryCost_Struct *) in->pBuffer; + LogTrading("(RoF2) Delivery costs updated: vouchers [{}] parcel percentage [{}]", + data->voucher_delivery_cost, + data->parcel_deliver_cost + ); + data->action = 0; + dest->FastQueuePacket(&in); + break; + } + default: { + LogTrading("(RoF2) Unhandled action [{}]", action); + dest->FastQueuePacket(&in, ack_req); + } } + } - unsigned char *__emu_buffer = in->pBuffer; + ENCODE(OP_BecomeTrader) + { + EQApplicationPacket *inapp = *p; + *p = nullptr; - BazaarSearchResults_Struct *emu = (BazaarSearchResults_Struct *)__emu_buffer; + unsigned char *__emu_buffer = inapp->pBuffer; + auto in = (BecomeTrader_Struct *) __emu_buffer; - int EntryCount = in->size / sizeof(BazaarSearchResults_Struct); + switch (in->action) { + case TraderOff: { + auto emu = (BecomeTrader_Struct *) __emu_buffer; - if (EntryCount == 0 || (in->size % sizeof(BazaarSearchResults_Struct)) != 0) - { - LogNetcode("[STRUCTS] Wrong size on outbound [{}]: Got [{}], expected multiple of [{}]", opcodes->EmuToName(in->GetOpcode()), in->size, sizeof(BazaarSearchResults_Struct)); - delete in; - return; + auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(structs::BecomeTrader_Struct)); + auto eq = (structs::BecomeTrader_Struct *) outapp->pBuffer; + + eq->action = TraderOff; + eq->entity_id = emu->entity_id; + + LogTrading( + "(RoF2) TraderOff action [{}] for entity_id [{}]", + eq->action, + eq->entity_id + ); + dest->FastQueuePacket(&outapp); + break; + } + case TraderOn: { + auto emu = (BecomeTrader_Struct *) __emu_buffer; + + auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(structs::BecomeTrader_Struct)); + auto eq = (structs::BecomeTrader_Struct *) outapp->pBuffer; + + eq->action = TraderOn; + eq->entity_id = emu->entity_id; + + LogTrading( + "(RoF2) TraderOn action [{}] for entity_id [{}]", + eq->action, + eq->entity_id + ); + dest->FastQueuePacket(&outapp); + break; + } + case AddTraderToBazaarWindow: { + auto emu = (BecomeTrader_Struct *) __emu_buffer; + auto outapp = new EQApplicationPacket(OP_TraderShop, sizeof(BecomeTrader_Struct)); + auto eq = (BecomeTrader_Struct *) outapp->pBuffer; + + eq->action = emu->action; + eq->entity_id = emu->entity_id; + eq->trader_id = emu->trader_id; + eq->zone_id = emu->zone_id; + strn0cpy(eq->trader_name, emu->trader_name, sizeof(eq->trader_name)); + + LogTrading( + "(RoF2) AddTraderToBazaarWindow action [{}] trader_id [{}] entity_id [{}] zone_id [{}]", + eq->action, + eq->entity_id, + eq->trader_id, + eq->zone_id + ); + dest->FastQueuePacket(&outapp); + break; + } + case RemoveTraderFromBazaarWindow: { + auto emu = (BecomeTrader_Struct *) __emu_buffer; + auto outapp = new EQApplicationPacket(OP_TraderShop, sizeof(structs::BazaarWindowRemoveTrader_Struct)); + auto eq = (structs::BazaarWindowRemoveTrader_Struct *) outapp->pBuffer; + + eq->action = emu->action; + eq->trader_id = emu->trader_id; + + LogTrading( + "(RoF2) RemoveTraderFromBazaarWindow action [{}] for entity_id [{}]", + eq->action, + eq->trader_id + ); + dest->FastQueuePacket(&outapp); + break; + } + default: { + LogTrading( + "(RoF2) Unhandled action [{}]", + in->action + ); + dest->QueuePacket(inapp); + } } - - in->size = EntryCount * sizeof(structs::BazaarSearchResults_Struct); - in->pBuffer = new unsigned char[in->size]; - - memset(in->pBuffer, 0, in->size); - - structs::BazaarSearchResults_Struct *eq = (structs::BazaarSearchResults_Struct *)in->pBuffer; - - for (int i = 0; i < EntryCount; ++i, ++emu, ++eq) - { - OUT(Beginning.Action); - OUT(SellerID); - memcpy(eq->SellerName, emu->SellerName, sizeof(eq->SellerName)); - OUT(NumItems); - OUT(ItemID); - OUT(SerialNumber); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(Cost); - OUT(ItemStat); - } - - delete[] __emu_buffer; - dest->FastQueuePacket(&in, ack_req); + safe_delete(inapp); } ENCODE(OP_BeginCast) @@ -2591,7 +2722,7 @@ namespace RoF2 outapp->WriteUInt8(0); // Unknown } - outapp->WriteUInt32(0); // Unknown + outapp->WriteUInt32(emu->char_id); // character_id outapp->WriteUInt8(emu->leadAAActive); @@ -3586,54 +3717,146 @@ namespace RoF2 ENCODE(OP_Trader) { - if ((*p)->size == sizeof(ClickTrader_Struct)) - { - ENCODE_LENGTH_EXACT(ClickTrader_Struct); - SETUP_DIRECT_ENCODE(ClickTrader_Struct, structs::ClickTrader_Struct); + uint32 action = *(uint32 *) (*p)->pBuffer; - eq->Code = emu->Code; - // Live actually has 200 items now, but 80 is the most our internal struct supports - for (uint32 i = 0; i < 200; i++) - { - eq->items[i].Unknown18 = 0; - if (i < 80) { - snprintf(eq->items[i].SerialNumber, sizeof(eq->items[i].SerialNumber), "%016" PRId64, emu->SerialNumber[i]); - eq->ItemCost[i] = emu->ItemCost[i]; - } - else { - snprintf(eq->items[i].SerialNumber, sizeof(eq->items[i].SerialNumber), "%016d", 0); - eq->ItemCost[i] = 0; - } + switch (action) { + case TraderOn: { + ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); + SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + + eq->action = structs::RoF2BazaarTraderBuyerActions::BeginTraderMode; + OUT(entity_id); + + LogTrading("(RoF2) TraderOn action [{}] entity_id [{}]", action, eq->entity_id); + FINISH_ENCODE(); + break; } + case TraderOff: { + ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); + SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); - FINISH_ENCODE(); - } - else if ((*p)->size == sizeof(Trader_ShowItems_Struct)) - { - ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); - SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + eq->action = structs::RoF2BazaarTraderBuyerActions::EndTraderMode; + OUT(entity_id); - eq->Code = emu->Code; - //strncpy(eq->SerialNumber, "0000000000000000", sizeof(eq->SerialNumber)); - //snprintf(eq->SerialNumber, sizeof(eq->SerialNumber), "%016d", 0); - eq->TraderID = emu->TraderID; - //eq->Stacksize = 0; - //eq->Price = 0; + LogTrading("(RoF2) TraderOff action [{}] entity_id [{}]", action, eq->entity_id); + FINISH_ENCODE(); + break; + } + case ListTraderItems: { + ENCODE_LENGTH_EXACT(Trader_Struct); + SETUP_DIRECT_ENCODE(Trader_Struct, structs::ClickTrader_Struct); + LogTrading("(RoF2) action [{}]", action); - FINISH_ENCODE(); - } - else if ((*p)->size == sizeof(TraderStatus_Struct)) - { - ENCODE_LENGTH_EXACT(TraderStatus_Struct); - SETUP_DIRECT_ENCODE(TraderStatus_Struct, structs::TraderStatus_Struct); + eq->action = structs::RoF2BazaarTraderBuyerActions::ListTraderItems; + std::transform( + std::begin(emu->items), + std::end(emu->items), + std::begin(eq->items), + [&](const uint32 x) { + return x; + } + ); + std::copy_n( + std::begin(emu->item_cost), + EQ::invtype::BAZAAR_SIZE, + std::begin(eq->item_cost) + ); - eq->Code = emu->Code; + FINISH_ENCODE(); + break; + } + case TraderAck2: { + LogTrading("(RoF2) TraderAck2 action"); + EQApplicationPacket *in = *p; + *p = nullptr; - FINISH_ENCODE(); - } - else if ((*p)->size == sizeof(TraderBuy_Struct)) - { - ENCODE_FORWARD(OP_TraderBuy); + dest->FastQueuePacket(&in); + break; + } + case PriceUpdate: { + SETUP_DIRECT_ENCODE(TraderPriceUpdate_Struct, structs::TraderPriceUpdate_Struct); + switch (emu->SubAction) { + case BazaarPriceChange_AddItem: { + auto outapp = std::make_unique( + OP_Trader, + sizeof(structs::TraderStatus_Struct) + ); + + auto data = (structs::TraderStatus_Struct *) outapp->pBuffer; + data->action = emu->Action; + data->sub_action = BazaarPriceChange_AddItem; + LogTrading( + "(RoF2) PriceUpdate action [{}] AddItem subaction [{}]", + data->action, + data->sub_action + ); + + dest->QueuePacket(outapp.get()); + break; + } + case BazaarPriceChange_RemoveItem: { + auto outapp = std::make_unique( + OP_Trader, + sizeof(structs::TraderStatus_Struct) + ); + + auto data = (structs::TraderStatus_Struct *) outapp->pBuffer; + data->action = emu->Action; + data->sub_action = BazaarPriceChange_RemoveItem; + LogTrading( + "(RoF2) PriceUpdate action [{}] RemoveItem subaction [{}]", + data->action, + data->sub_action + ); + + dest->QueuePacket(outapp.get()); + break; + } + case BazaarPriceChange_UpdatePrice: { + auto outapp = std::make_unique( + OP_Trader, + sizeof(structs::TraderStatus_Struct) + ); + + auto data = (structs::TraderStatus_Struct *) outapp->pBuffer; + data->action = emu->Action; + data->sub_action = BazaarPriceChange_UpdatePrice; + LogTrading( + "(RoF2) PriceUpdate action [{}] UpdatePrice subaction [{}]", + data->action, + data->sub_action + ); + + dest->QueuePacket(outapp.get()); + break; + } + } + + FINISH_ENCODE(); + break; + } + case BuyTraderItem: { + EQApplicationPacket *in = *p; + *p = nullptr; + + auto eq = (structs::TraderBuy_Struct *) in->pBuffer; + LogTrading( + "(RoF2) BuyTraderItem action [{}] item_id [{}] item_sn [{}] buyer [{}]", + action, + eq->item_id, + eq->serial_number, + eq->buyer_name + ); + dest->FastQueuePacket(&in); + break; + } + default: { + LogTrading("(RoF2) action [{}]", action); + EQApplicationPacket *in = *p; + *p = nullptr; + + dest->FastQueuePacket(&in); + } } } @@ -3641,14 +3864,26 @@ namespace RoF2 { ENCODE_LENGTH_EXACT(TraderBuy_Struct); SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); - - OUT(Action); - OUT(Price); - OUT(TraderID); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(Quantity); - OUT(AlreadySold); + LogTrading( + "(RoF2) item_id [{}] price [{}] quantity [{}] trader_id [{}]", + emu->item_id, + emu->price, + emu->quantity, + emu->trader_id + ); + __packet->SetOpcode(OP_TraderShop); + OUT(action); + OUT(method); + OUT(sub_action); + OUT(trader_id); + OUT(item_id); + OUT(price); + OUT(already_sold); + OUT(quantity); + OUT_str(buyer_name); + OUT_str(seller_name); + OUT_str(item_name); + OUT_str(serial_number); FINISH_ENCODE(); } @@ -3657,71 +3892,74 @@ namespace RoF2 { ENCODE_LENGTH_EXACT(TraderDelItem_Struct); SETUP_DIRECT_ENCODE(TraderDelItem_Struct, structs::TraderDelItem_Struct); + LogTrading( + "(RoF2) trader_id [{}] item_id [{}]", + emu->trader_id, + emu->item_id + ); - OUT(TraderID); - snprintf(eq->SerialNumber, sizeof(eq->SerialNumber), "%016d", emu->ItemID); - LogTrading("ENCODE(OP_TraderDelItem): TraderID [{}], SerialNumber: [{}]", emu->TraderID, emu->ItemID); + eq->TraderID = emu->trader_id; + auto serial = fmt::format("{:016}\n", emu->item_id); + strn0cpy(eq->SerialNumber, serial.c_str(), sizeof(eq->SerialNumber)); + LogTrading("(RoF2) TraderID [{}], SerialNumber: [{}]", emu->trader_id, emu->item_id); FINISH_ENCODE(); } ENCODE(OP_TraderShop) { - uint32 psize = (*p)->size; - if (psize == sizeof(TraderClick_Struct)) - { - ENCODE_LENGTH_EXACT(TraderClick_Struct); - SETUP_DIRECT_ENCODE(TraderClick_Struct, structs::TraderClick_Struct); + auto action = *(uint32 *) (*p)->pBuffer; - eq->Code = 28; // Seen on Live - OUT(TraderID); - OUT(Approval); + switch (action) { + case ClickTrader: { + ENCODE_LENGTH_EXACT(TraderClick_Struct); + SETUP_DIRECT_ENCODE(TraderClick_Struct, structs::TraderClick_Struct); + LogTrading( + "(RoF2) ClickTrader action [{}] trader_id [{}]", + action, + emu->TraderID + ); - FINISH_ENCODE(); - } - else if (psize == sizeof(BazaarWelcome_Struct)) - { - ENCODE_LENGTH_EXACT(BazaarWelcome_Struct); - SETUP_DIRECT_ENCODE(BazaarWelcome_Struct, structs::BazaarWelcome_Struct); + eq->action = structs::RoF2BazaarTraderBuyerActions::ClickTrader; // Seen on Live + eq->trader_id = emu->TraderID; + eq->unknown_008 = emu->Approval; - eq->Code = emu->Beginning.Action; - eq->EntityID = emu->Unknown012; - OUT(Traders); - OUT(Items); - eq->Traders2 = emu->Traders; - eq->Items2 = emu->Items; + FINISH_ENCODE(); + break; + } + case structs::RoF2BazaarTraderBuyerActions::BuyTraderItem: { + ENCODE_LENGTH_EXACT(structs::TraderBuy_Struct); + SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "(RoF2) item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); - LogTrading("ENCODE(OP_TraderShop): BazaarWelcome_Struct Code [{}], Traders [{}], Items [{}]", - eq->Code, eq->Traders, eq->Items); + OUT(action); + OUT(method); + OUT(trader_id); + OUT(item_id); + OUT(price); + OUT(already_sold); + OUT(quantity); + OUT_str(buyer_name); + OUT_str(seller_name); + OUT_str(item_name); + OUT_str(serial_number); - FINISH_ENCODE(); - } - else if (psize == sizeof(TraderBuy_Struct)) - { - ENCODE_LENGTH_EXACT(TraderBuy_Struct); - SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + FINISH_ENCODE(); + break; + } + default: { + LogTrading("(RoF2) Unhandled action [{}]", action); + EQApplicationPacket *in = *p; + *p = nullptr; - OUT(Action); - OUT(TraderID); - - //memcpy(eq->BuyerName, emu->BuyerName, sizeof(eq->BuyerName)); - //memcpy(eq->SellerName, emu->SellerName, sizeof(eq->SellerName)); - - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(AlreadySold); - OUT(Price); - OUT(Quantity); - snprintf(eq->SerialNumber, sizeof(eq->SerialNumber), "%016d", emu->ItemID); - - LogTrading("ENCODE(OP_TraderShop): Buy Action [{}], Price [{}], Trader [{}], ItemID [{}], Quantity [{}], ItemName, [{}]", - eq->Action, eq->Price, eq->TraderID, eq->ItemID, eq->Quantity, emu->ItemName); - - FINISH_ENCODE(); - } - else - { - LogTrading("ENCODE(OP_TraderShop): Encode Size Unknown ([{}])", psize); + dest->FastQueuePacket(&in); + } } } @@ -4067,20 +4305,20 @@ namespace RoF2 structs::Spawn_Struct_Bitfields *Bitfields = (structs::Spawn_Struct_Bitfields*)Buffer; - Bitfields->gender = emu->gender; - Bitfields->ispet = emu->is_pet; - Bitfields->afk = emu->afk; - Bitfields->anon = emu->anon; - Bitfields->gm = emu->gm; - Bitfields->sneak = 0; - Bitfields->lfg = emu->lfg; - Bitfields->invis = emu->invis; - Bitfields->linkdead = 0; - Bitfields->showhelm = emu->showhelm; - Bitfields->trader = 0; - Bitfields->targetable = 1; + Bitfields->gender = emu->gender; + Bitfields->ispet = emu->is_pet; + Bitfields->afk = emu->afk; + Bitfields->anon = emu->anon; + Bitfields->gm = emu->gm; + Bitfields->sneak = 0; + Bitfields->lfg = emu->lfg; + Bitfields->invis = emu->invis; + Bitfields->linkdead = 0; + Bitfields->showhelm = emu->showhelm; + Bitfields->trader = emu->trader ? 1 : 0; + Bitfields->targetable = 1; Bitfields->targetable_with_hotkey = emu->targetable_with_hotkey ? 1 : 0; - Bitfields->showname = ShowName; + Bitfields->showname = ShowName; if (emu->DestructibleObject) { @@ -4431,6 +4669,7 @@ namespace RoF2 char *Buffer = (char *)__packet->pBuffer; uint8 SubAction = VARSTRUCT_DECODE_TYPE(uint8, Buffer); + LogTrading("(RoF2) action [{}]", SubAction); if ((SubAction != BazaarInspectItem) || (__packet->size != sizeof(structs::NewBazaarInspect_Struct))) return; @@ -4440,6 +4679,7 @@ namespace RoF2 IN(Beginning.Action); memcpy(emu->Name, eq->Name, sizeof(emu->Name)); IN(SerialNumber); + LogTrading("(RoF2) action [{}] serial_number [{}]", eq->Beginning.Action, eq->SerialNumber); FINISH_DIRECT_DECODE(); } @@ -5331,40 +5571,61 @@ namespace RoF2 DECODE(OP_Trader) { - uint32 psize = __packet->size; - if (psize == sizeof(structs::ClickTrader_Struct)) - { - DECODE_LENGTH_EXACT(structs::ClickTrader_Struct); - SETUP_DIRECT_DECODE(ClickTrader_Struct, structs::ClickTrader_Struct); + auto action = *(uint32 *)__packet->pBuffer; - emu->Code = eq->Code; - // Live actually has 200 items now, but 80 is the most our internal struct supports - for (uint32 i = 0; i < 80; i++) - { - emu->SerialNumber[i] = 0; // eq->SerialNumber[i]; - emu->ItemCost[i] = eq->ItemCost[i]; + switch (action) { + case structs::RoF2BazaarTraderBuyerActions::BeginTraderMode: { + DECODE_LENGTH_EXACT(structs::BeginTrader_Struct); + SETUP_DIRECT_DECODE(ClickTrader_Struct, structs::BeginTrader_Struct); + LogTrading("(RoF2) BeginTraderMode action [{}]", action); + + emu->action = TraderOn; + std::copy_n(eq->item_cost, RoF2::invtype::BAZAAR_SIZE, emu->item_cost); + std::transform( + std::begin(eq->items), + std::end(eq->items), + std::begin(emu->serial_number), + [&](const structs::TraderItemSerial_Struct x) { + return Strings::ToUnsignedBigInt(x.serial_number,0); + } + ); + + FINISH_DIRECT_DECODE(); + break; } + case structs::RoF2BazaarTraderBuyerActions::EndTraderMode: { + DECODE_LENGTH_EXACT(structs::Trader_ShowItems_Struct); + SETUP_DIRECT_DECODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading("(RoF2) EndTraderMode action [{}]", action); - FINISH_DIRECT_DECODE(); - } - else if (psize == sizeof(structs::Trader_ShowItems_Struct)) - { - DECODE_LENGTH_EXACT(structs::Trader_ShowItems_Struct); - SETUP_DIRECT_DECODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + emu->action = TraderOff; + emu->entity_id = eq->entity_id; - emu->Code = eq->Code; - emu->TraderID = eq->TraderID; + FINISH_DIRECT_DECODE(); + break; + } + case structs::RoF2BazaarTraderBuyerActions::ListTraderItems: { + LogTrading("(RoF2) ListTraderItems action [{}]", action); + break; + } + case structs::RoF2BazaarTraderBuyerActions::PriceUpdate: { + DECODE_LENGTH_EXACT(structs::TraderPriceUpdate_Struct); + SETUP_DIRECT_DECODE(TraderPriceUpdate_Struct, structs::TraderPriceUpdate_Struct); + LogTrading("(RoF2) PriceUpdate action [{}]", action); - FINISH_DIRECT_DECODE(); - } - else if (psize == sizeof(structs::TraderStatus_Struct)) - { - DECODE_LENGTH_EXACT(structs::TraderStatus_Struct); - SETUP_DIRECT_DECODE(TraderStatus_Struct, structs::TraderStatus_Struct); + emu->Action = PriceUpdate; + emu->SerialNumber = Strings::ToUnsignedBigInt(eq->serial_number, 0); + if (emu->SerialNumber == 0) { + LogTrading("(RoF2) Price change with invalid serial number [{}]", eq->serial_number); + } + emu->NewPrice = eq->new_price; - emu->Code = eq->Code; // 11 = Start Trader, 2 = End Trader, 22 = ? - Guessing - - FINISH_DIRECT_DECODE(); + FINISH_DIRECT_DECODE(); + break; + } + default: { + LogTrading("(RoF2) Unhandled action [{}]", action); + } } } @@ -5372,73 +5633,136 @@ namespace RoF2 { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "(RoF2) item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); + IN(action); + IN(price); + IN(trader_id); + memcpy(emu->item_name, eq->item_name, sizeof(emu->item_name)); + IN(item_id); + IN(quantity); FINISH_DIRECT_DECODE(); } DECODE(OP_TraderShop) { - uint32 psize = __packet->size; - if (psize == sizeof(structs::TraderClick_Struct)) - { - DECODE_LENGTH_EXACT(structs::TraderClick_Struct); - SETUP_DIRECT_DECODE(TraderClick_Struct, structs::TraderClick_Struct); + uint32 action = *(uint32 *)__packet->pBuffer; - IN(Code); - IN(TraderID); - IN(Approval); - LogTrading("DECODE(OP_TraderShop): TraderClick_Struct Code [{}], TraderID [{}], Approval [{}]", - eq->Code, eq->TraderID, eq->Approval); + switch (action) { + case structs::RoF2BazaarTraderBuyerActions::BazaarSearch: { + DECODE_LENGTH_EXACT(structs::BazaarSearch_Struct); + SETUP_DIRECT_DECODE(BazaarSearchCriteria_Struct, structs::BazaarSearch_Struct); + LogTrading( + "(RoF2) BazaarSearch action [{}]", + action + ); - FINISH_DIRECT_DECODE(); - } - else if (psize == sizeof(structs::BazaarWelcome_Struct)) - { - // Don't think this size gets used in RoF+ - Leaving for now... - DECODE_LENGTH_EXACT(structs::BazaarWelcome_Struct); - SETUP_DIRECT_DECODE(BazaarWelcome_Struct, structs::BazaarWelcome_Struct); + __packet->SetOpcode(OP_BazaarSearch); + emu->action = BazaarSearch; + emu->type = eq->type == UINT32_MAX ? UINT8_MAX : eq->type; + IN(item_stat); + IN(max_cost); + IN(min_cost); + IN(max_level); + IN(min_level); + IN(race); + IN(slot); + IN(trader_id); + IN(_class); + IN(prestige); + IN(search_scope); + IN(max_results); + IN(augment); + IN_str(item_name); - emu->Beginning.Action = eq->Code; - IN(Traders); - IN(Items); - LogTrading("DECODE(OP_TraderShop): BazaarWelcome_Struct Code [{}], Traders [{}], Items [{}]", - eq->Code, eq->Traders, eq->Items); + FINISH_DIRECT_DECODE(); + break; + } + case structs::RoF2BazaarTraderBuyerActions::ClickTrader: { + DECODE_LENGTH_EXACT(structs::TraderClick_Struct); + SETUP_DIRECT_DECODE(TraderClick_Struct, structs::TraderClick_Struct); - FINISH_DIRECT_DECODE(); - } - else if (psize == sizeof(structs::TraderBuy_Struct)) - { + emu->Code = ClickTrader; + emu->TraderID = eq->trader_id; + emu->Approval = eq->unknown_008; + LogTrading("(RoF2) ClickTrader action [{}], trader_id [{}], approval [{}]", + eq->action, + eq->trader_id, + eq->unknown_008 + ); + FINISH_DIRECT_DECODE(); + break; + } + case structs::RoF2BazaarTraderBuyerActions::BazaarInspect: { + DECODE_LENGTH_EXACT(structs::BazaarInspect_Struct); + SETUP_DIRECT_DECODE(BazaarInspect_Struct, structs::BazaarInspect_Struct); - DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); - SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); + __packet->SetOpcode(OP_BazaarSearch); + IN(item_id); + IN(trader_id); + emu->action = BazaarInspect; + emu->serial_number = Strings::ToUnsignedInt(eq->serial_number, 0); + if (emu->serial_number == 0) { + LogTrading( + "(RoF2) trader_id = [{}] requested a BazaarInspect with an invalid serial number of [{}]", + eq->trader_id, + eq->serial_number + ); + FINISH_DIRECT_DECODE(); + return; + } - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); - LogTrading("DECODE(OP_TraderShop): TraderBuy_Struct (Unknowns) Unknown004 [{}], Unknown008 [{}], Unknown012 [{}], Unknown076 [{}], Unknown276 [{}]", - eq->Unknown004, eq->Unknown008, eq->Unknown012, eq->Unknown076, eq->Unknown276); - LogTrading("DECODE(OP_TraderShop): TraderBuy_Struct Buy Action [{}], Price [{}], Trader [{}], ItemID [{}], Quantity [{}], ItemName, [{}]", - eq->Action, eq->Price, eq->TraderID, eq->ItemID, eq->Quantity, eq->ItemName); + LogTrading("(RoF2) BazaarInspect action [{}] item_id [{}] serial_number [{}]", + action, + eq->item_id, + eq->serial_number + ); + FINISH_DIRECT_DECODE(); + break; + } + case structs::RoF2BazaarTraderBuyerActions::WelcomeMessage: { + __packet->SetOpcode(OP_BazaarSearch); + LogTrading("(RoF2) WelcomeMessage action [{}]", action); + break; + } + case structs::RoF2BazaarTraderBuyerActions::BuyTraderItem: { + DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); + SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "(RoF2) item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); - FINISH_DIRECT_DECODE(); - } - else if (psize == 4) - { - LogTrading("DECODE(OP_TraderShop): Forwarding packet as-is with size 4"); - } - else - { - LogTrading("DECODE(OP_TraderShop): Decode Size Unknown ([{}])", psize); + __packet->SetOpcode(OP_TraderBuy); + IN(action); + IN(method); + IN(trader_id); + IN(item_id); + IN(price); + IN(already_sold); + IN(quantity); + IN_str(buyer_name); + IN_str(seller_name); + IN_str(item_name); + IN_str(serial_number); + + FINISH_DIRECT_DECODE(); + break; + } + default: { + LogTrading("(RoF2) Unhandled action [{}]", action); + } + return; } } @@ -5541,19 +5865,7 @@ namespace RoF2 RoF2::structs::ItemSerializationHeader hdr; //sprintf(hdr.unknown000, "06e0002Y1W00"); - - snprintf(hdr.unknown000, sizeof(hdr.unknown000), "%016d", item->ID); - if (packet_type == ItemPacketParcel) { - strn0cpy( - hdr.unknown000, - fmt::format( - "{:03}PAR{:010}\0", - inst->GetMerchantSlot(), - item->ID - ).c_str(), - sizeof(hdr.unknown000) - ); - } + strn0cpy(hdr.unknown000, fmt::format("{:016}\0", inst->GetSerialNumber()).c_str(),sizeof(hdr.unknown000)); hdr.stacksize = item->ID == PARCEL_MONEY_ITEM_ID ? inst->GetPrice() : (inst->IsStackable() ? ((inst->GetCharges() > 1000) diff --git a/common/patches/rof2_ops.h b/common/patches/rof2_ops.h index 7bc6c074a..a775c06d7 100644 --- a/common/patches/rof2_ops.h +++ b/common/patches/rof2_ops.h @@ -43,6 +43,7 @@ E(OP_ApplyPoison) E(OP_AugmentInfo) E(OP_Barter) E(OP_BazaarSearch) +E(OP_BecomeTrader) E(OP_BeginCast) E(OP_BlockedBuffs) E(OP_Buff) diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 1063a79b7..30ced24a0 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -3106,28 +3106,43 @@ struct EnvDamage2_Struct { }; //Bazaar Stuff +enum RoF2BazaarTraderBuyerActions { + Zero = 0, + BeginTraderMode = 1, + EndTraderMode = 2, + PriceUpdate = 3, + EndTransaction = 4, + BazaarSearch = 7, + WelcomeMessage = 9, + BuyTraderItem = 10, + ListTraderItems = 11, + BazaarInspect = 18, + ClickTrader = 28, + ItemMove = 19, + ReconcileItems = 20 +}; enum { - BazaarTrader_StartTraderMode = 1, - BazaarTrader_EndTraderMode = 2, - BazaarTrader_UpdatePrice = 3, - BazaarTrader_EndTransaction = 4, - BazaarSearchResults = 7, - BazaarWelcome = 9, - BazaarBuyItem = 10, - BazaarTrader_ShowItems = 11, - BazaarSearchDone = 12, + BazaarTrader_StartTraderMode = 1, + BazaarTrader_EndTraderMode = 2, + BazaarTrader_UpdatePrice = 3, + BazaarTrader_EndTransaction = 4, + BazaarSearchResults = 7, + BazaarWelcome = 9, + BazaarBuyItem = 10, + BazaarTrader_ShowItems = 11, + BazaarSearchDone = 12, BazaarTrader_CustomerBrowsing = 13, - BazaarInspectItem = 18, - BazaarSearchDone2 = 19, + BazaarInspectItem = 18, + BazaarSearchDone2 = 19, BazaarTrader_StartTraderMode2 = 22 }; enum { - BazaarPriceChange_Fail = 0, + BazaarPriceChange_Fail = 0, BazaarPriceChange_UpdatePrice = 1, - BazaarPriceChange_RemoveItem = 2, - BazaarPriceChange_AddItem = 3 + BazaarPriceChange_RemoveItem = 2, + BazaarPriceChange_AddItem = 3 }; struct BazaarWindowStart_Struct { @@ -3136,34 +3151,51 @@ struct BazaarWindowStart_Struct { uint16 Unknown002; }; +struct BazaarDeliveryCost_Struct { + uint32 action; + uint32 voucher_delivery_cost; + float parcel_deliver_cost; //percentage of item cost + uint32 unknown_010; +}; struct BazaarWelcome_Struct { - uint32 Code; - uint32 EntityID; - uint32 Traders; - uint32 Items; - uint32 Traders2; - uint32 Items2; + uint32 action; + uint32 unknown_004; + uint32 num_of_traders; + uint32 num_of_items; }; struct BazaarSearch_Struct { - BazaarWindowStart_Struct Beginning; - uint32 TraderID; - uint32 Class_; - uint32 Race; - uint32 ItemStat; - uint32 Slot; - uint32 Type; - char Name[64]; - uint32 MinPrice; - uint32 MaxPrice; - uint32 Minlevel; - uint32 MaxLlevel; +/*000*/ uint32 action; +/*004*/ uint8 search_scope;//1 all traders 0 local traders +/*005*/ uint8 unknown_005{0}; +/*006*/ uint16 unknown_006{0}; +/*008*/ uint32 unknown_008{0}; +/*012*/ uint32 unknown_012{0}; +/*016*/ uint32 trader_id; +/*020*/ uint32 _class; +/*024*/ uint32 race; +/*028*/ uint32 item_stat; +/*032*/ uint32 slot; +/*036*/ uint32 type; +/*040*/ char item_name[64]; +/*104*/ uint32 min_cost; +/*108*/ uint32 max_cost; +/*112*/ uint32 min_level; +/*116*/ uint32 max_level; +/*120*/ uint32 max_results; +/*124*/ uint32 prestige; +/*128*/ uint32 augment; }; -struct BazaarInspect_Struct{ - uint32 ItemID; - uint32 Unknown004; - char Name[64]; + +struct BazaarInspect_Struct { + uint32 action; + uint32 unknown_004; + uint32 trader_id; + char serial_number[17]; + char unknown_029[3]; + uint32 item_id; + uint32 unknown_036; }; struct NewBazaarInspect_Struct { @@ -3184,18 +3216,23 @@ struct BazaarReturnDone_Struct{ uint32 Unknown016; }; -struct BazaarSearchResults_Struct { -/*000*/ BazaarWindowStart_Struct Beginning; -/*004*/ uint32 SellerID; -/*008*/ char SellerName[64]; -/*072*/ uint32 NumItems; -/*076*/ uint32 ItemID; -/*080*/ uint32 SerialNumber; -/*084*/ uint32 Unknown084; -/*088*/ char ItemName[64]; -/*152*/ uint32 Cost; -/*156*/ uint32 ItemStat; -/*160*/ +struct BazaarSearchDetails_Struct { //24+name+17 +/*014*/ uint32 trader_id; +/*018*/ char serial_num[17]; +/*022*/ uint32 cost; +/*026*/ uint32 quanity; +/*030*/ uint32 id; +/*034*/ uint32 icon; +/*038*/ char name[1]; +/*039*/ uint32 stat; +}; + +struct BazaarSearchResults_Struct { //14 +/*000*/ uint32 unknown000; +/*004*/ uint16 zone_id; +/*006*/ uint32 entity_id; +/*010*/ uint32 num_items; +/*014*/ BazaarSearchDetails_Struct items[]; }; struct ServerSideFilters_Struct { @@ -3398,21 +3435,26 @@ struct WhoAllPlayerPart4 { }; struct TraderItemSerial_Struct { - char SerialNumber[17]; - uint8 Unknown18; + char serial_number[17]; + uint8 unknown_018; + + void operator=(uint32 a) { + auto _tmp = fmt::format("{:016}", a); + strn0cpy(this->serial_number, _tmp.c_str(), sizeof(this->serial_number)); + } }; -struct Trader_Struct { -/*0000*/ uint32 Code; -/*0004*/ TraderItemSerial_Struct items[200]; -/*3604*/ uint32 ItemCost[200]; +struct BeginTrader_Struct { +/*0000*/ uint32 action; +/*0004*/ TraderItemSerial_Struct items[200]; +/*3604*/ uint32 item_cost[200]; /*4404*/ }; struct ClickTrader_Struct { -/*0000*/ uint32 Code; -/*0004*/ TraderItemSerial_Struct items[200]; -/*3604*/ uint32 ItemCost[200]; +/*0000*/ uint32 action; +/*0004*/ TraderItemSerial_Struct items[200]; +/*3604*/ uint32 item_cost[200]; /*4404*/ }; @@ -3421,67 +3463,55 @@ struct GetItems_Struct { }; struct BecomeTrader_Struct { - uint32 id; - uint32 code; + uint32 entity_id; + uint32 action; +}; + +struct BazaarWindowRemoveTrader_Struct { + uint32 action; + uint32 trader_id; +}; + +struct TraderPriceUpdate_Struct { + uint32 action; + char serial_number[17]; + char unknown_021[3]; + uint32 unknown_024; + uint32 new_price; }; struct Trader_ShowItems_Struct { - /*000*/ uint32 Code; - /*004*/ uint16 TraderID; - /*008*/ uint32 Unknown08; - /*012*/ -}; - -struct Trader_ShowItems_Struct_WIP { -/*000*/ uint32 Code; -/*004*/ char SerialNumber[17]; -/*021*/ uint8 Unknown21; -/*022*/ uint16 TraderID; -/*026*/ uint32 Stacksize; -/*030*/ uint32 Price; -/*032*/ +/*000*/ uint32 action; +/*004*/ uint32 entity_id; +/*008*/ uint32 unknown_008; +/*012*/ }; struct TraderStatus_Struct { -/*000*/ uint32 Code; -/*004*/ uint32 Uknown04; -/*008*/ uint32 Uknown08; +/*000*/ uint32 action; +/*004*/ uint32 sub_action; +/*008*/ uint32 uknown_008; /*012*/ }; struct TraderBuy_Struct { - /*000*/ uint32 Action; - /*004*/ uint32 Unknown004; - /*008*/ uint32 Unknown008; - /*012*/ uint32 Unknown012; - /*016*/ uint32 TraderID; - /*020*/ char BuyerName[64]; - /*084*/ char SellerName[64]; - /*148*/ char Unknown148[32]; - /*180*/ char ItemName[64]; - /*244*/ char SerialNumber[16]; - /*260*/ uint32 Unknown076; - /*264*/ uint32 ItemID; - /*268*/ uint32 Price; - /*272*/ uint32 AlreadySold; - /*276*/ uint32 Unknown276; - /*280*/ uint32 Quantity; - /*284*/ -}; - -struct TraderBuy_Struct_OLD { -/*000*/ uint32 Action; -/*004*/ uint32 Unknown004; -/*008*/ uint32 Price; -/*012*/ uint32 Unknown008; // Probably high order bits of a 64 bit price. -/*016*/ uint32 TraderID; -/*020*/ char ItemName[64]; -/*084*/ uint32 Unknown076; -/*088*/ uint32 ItemID; -/*092*/ uint32 AlreadySold; -/*096*/ uint32 Quantity; -/*100*/ uint32 Unknown092; -/*104*/ +/*000*/ uint32 action; +/*004*/ uint32 method; +/*008*/ uint32 sub_action; +/*012*/ uint32 unknown_012; +/*016*/ uint32 trader_id; +/*020*/ char buyer_name[64]; +/*084*/ char seller_name[64]; +/*148*/ char unknown_148[32]; +/*180*/ char item_name[64]; +/*244*/ char serial_number[17]; +/*261*/ char unknown_261[3]; +/*264*/ uint32 item_id; +/*268*/ uint32 price; +/*272*/ uint32 already_sold; +/*276*/ uint32 unknown_276; +/*280*/ uint32 quantity; +/*284*/ }; struct TraderItemUpdate_Struct{ @@ -3502,15 +3532,15 @@ struct MoneyUpdate_Struct{ struct TraderDelItem_Struct{ /*000*/ uint32 Unknown000; /*004*/ uint32 TraderID; - /*008*/ char SerialNumber[16]; + /*008*/ char SerialNumber[17]; /*024*/ uint32 Unknown012; /*028*/ }; struct TraderClick_Struct{ - /*000*/ uint32 Code; - /*004*/ uint32 TraderID; - /*008*/ uint32 Approval; + /*000*/ uint32 action; + /*004*/ uint32 trader_id; + /*008*/ uint32 unknown_008; /*012*/ }; diff --git a/common/patches/rof_structs.h b/common/patches/rof_structs.h index 72ae67b1e..d502eddd6 100644 --- a/common/patches/rof_structs.h +++ b/common/patches/rof_structs.h @@ -3376,17 +3376,17 @@ struct TraderStatus_Struct { }; struct TraderBuy_Struct { -/*000*/ uint32 Action; -/*004*/ uint32 Unknown004; -/*008*/ uint32 Price; -/*012*/ uint32 Unknown008; // Probably high order bits of a 64 bit price. -/*016*/ uint32 TraderID; -/*020*/ char ItemName[64]; -/*084*/ uint32 Unknown076; -/*088*/ uint32 ItemID; -/*092*/ uint32 AlreadySold; -/*096*/ uint32 Quantity; -/*100*/ uint32 Unknown092; +/*000*/ uint32 action; +/*004*/ uint32 unknown_004; +/*008*/ uint32 price; +/*012*/ uint32 unknown_008; // Probably high order bits of a 64 bit price. +/*016*/ uint32 trader_id; +/*020*/ char item_name[64]; +/*084*/ uint32 unknown_076; +/*088*/ uint32 item_id; +/*092*/ uint32 already_sold; +/*096*/ uint32 quantity; +/*100*/ uint32 unknown_092; /*104*/ }; diff --git a/common/patches/sod.cpp b/common/patches/sod.cpp index 6e15503d9..d8f174196 100644 --- a/common/patches/sod.cpp +++ b/common/patches/sod.cpp @@ -2286,13 +2286,13 @@ namespace SoD ENCODE_LENGTH_EXACT(TraderBuy_Struct); SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); - OUT(Action); - OUT(Price); - OUT(TraderID); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(Quantity); - OUT(AlreadySold); + OUT(action); + OUT(price); + OUT(trader_id); + memcpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + OUT(item_id); + OUT(quantity); + OUT(already_sold); FINISH_ENCODE(); } @@ -3517,12 +3517,12 @@ namespace SoD SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); MEMSET_IN(TraderBuy_Struct); - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); + IN(action); + IN(price); + IN(trader_id); + memcpy(emu->item_name, eq->item_name, sizeof(emu->item_name)); + IN(item_id); + IN(quantity); FINISH_DIRECT_DECODE(); } diff --git a/common/patches/sod_structs.h b/common/patches/sod_structs.h index 7ec857208..03648c116 100644 --- a/common/patches/sod_structs.h +++ b/common/patches/sod_structs.h @@ -2879,20 +2879,21 @@ struct Trader_ShowItems_Struct{ }; struct TraderBuy_Struct { -/*000*/ uint32 Action; -/*004*/ uint32 Unknown004; -/*008*/ uint32 Price; -/*012*/ uint32 Unknown008; // Probably high order bits of a 64 bit price. -/*016*/ uint32 TraderID; -/*020*/ char ItemName[64]; -/*084*/ uint32 Unknown076; -/*088*/ uint32 ItemID; -/*092*/ uint32 AlreadySold; -/*096*/ uint32 Quantity; -/*100*/ uint32 Unknown092; +/*000*/ uint32 action; +/*004*/ uint32 unknown_004; +/*008*/ uint32 price; +/*012*/ uint32 unknown_008; // Probably high order bits of a 64 bit price. +/*016*/ uint32 trader_id; +/*020*/ char item_name[64]; +/*084*/ uint32 unknown_076; +/*088*/ uint32 item_id; +/*092*/ uint32 already_sold; +/*096*/ uint32 quantity; +/*100*/ uint32 unknown_092; /*104*/ }; + struct TraderItemUpdate_Struct{ uint32 unknown0; uint32 traderid; diff --git a/common/patches/sof.cpp b/common/patches/sof.cpp index 73abceb4a..c8cc5748b 100644 --- a/common/patches/sof.cpp +++ b/common/patches/sof.cpp @@ -270,8 +270,8 @@ namespace SoF ENCODE_LENGTH_EXACT(BecomeTrader_Struct); SETUP_DIRECT_ENCODE(BecomeTrader_Struct, structs::BecomeTrader_Struct); - OUT(ID); - OUT(Code); + OUT(trader_id); + OUT(action); FINISH_ENCODE(); } @@ -1902,13 +1902,13 @@ namespace SoF ENCODE_LENGTH_EXACT(TraderBuy_Struct); SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); - OUT(Action); - OUT(Price); - OUT(TraderID); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(Quantity); - OUT(AlreadySold); + OUT(action); + OUT(price); + OUT(trader_id); + memcpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + OUT(item_id); + OUT(quantity); + OUT(already_sold); FINISH_ENCODE(); } @@ -2908,12 +2908,12 @@ namespace SoF SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); MEMSET_IN(TraderBuy_Struct); - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); + IN(action); + IN(price); + IN(trader_id); + memcpy(emu->item_name, eq->item_name, sizeof(emu->item_name)); + IN(item_id); + IN(quantity); FINISH_DIRECT_DECODE(); } diff --git a/common/patches/sof_structs.h b/common/patches/sof_structs.h index c12527db5..918f8e32a 100644 --- a/common/patches/sof_structs.h +++ b/common/patches/sof_structs.h @@ -2771,8 +2771,8 @@ struct GetItems_Struct{ }; struct BecomeTrader_Struct{ - uint32 ID; - uint32 Code; + uint32 trader_id; + uint32 action; }; struct Trader_ShowItems_Struct{ @@ -2782,15 +2782,15 @@ struct Trader_ShowItems_Struct{ }; struct TraderBuy_Struct { -/*000*/ uint32 Action; -/*004*/ uint32 Price; -/*008*/ uint32 TraderID; -/*012*/ char ItemName[64]; -/*076*/ uint32 Unknown076; -/*080*/ uint32 ItemID; -/*084*/ uint32 AlreadySold; -/*088*/ uint32 Quantity; -/*092*/ uint32 Unknown092; +/*000*/ uint32 action; +/*004*/ uint32 price; +/*008*/ uint32 trader_id; +/*012*/ char item_name[64]; +/*076*/ uint32 unknown_076; +/*080*/ uint32 item_id; +/*084*/ uint32 already_sold; +/*088*/ uint32 quantity; +/*092*/ uint32 unknown_092; }; struct TraderItemUpdate_Struct{ diff --git a/common/patches/ss_define.h b/common/patches/ss_define.h index cb52136e5..63eef99b3 100644 --- a/common/patches/ss_define.h +++ b/common/patches/ss_define.h @@ -60,7 +60,7 @@ eq_struct *eq = (eq_struct *) __packet->pBuffer; \ #define ALLOC_LEN_ENCODE(len) \ - __packet->pBuffer = new unsigned char[len]; \ + __packet->pBuffer = new unsigned char[len] {}; \ __packet->size = len; \ memset(__packet->pBuffer, 0, len); \ @@ -124,7 +124,7 @@ #define SETUP_DIRECT_DECODE(emu_struct, eq_struct) \ unsigned char *__eq_buffer = __packet->pBuffer; \ __packet->size = sizeof(emu_struct); \ - __packet->pBuffer = new unsigned char[__packet->size]; \ + __packet->pBuffer = new unsigned char[__packet->size] {}; \ emu_struct *emu = (emu_struct *) __packet->pBuffer; \ eq_struct *eq = (eq_struct *) __eq_buffer; @@ -133,7 +133,7 @@ eq_struct* in = (eq_struct*)__packet->pBuffer; \ auto size = strlen(in->var_field); \ __packet->size = sizeof(emu_struct) + size + 1; \ - __packet->pBuffer = new unsigned char[__packet->size]; \ + __packet->pBuffer = new unsigned char[__packet->size] {}; \ emu_struct *emu = (emu_struct *) __packet->pBuffer; \ eq_struct *eq = (eq_struct *) __eq_buffer; diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp index 568ab66c0..961ff9793 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -32,6 +32,7 @@ #include "../strings.h" #include "../item_instance.h" #include "titanium_structs.h" +#include "../rulesys.h" #include "../path_manager.h" #include "../raid.h" #include "../guilds.h" @@ -197,64 +198,135 @@ namespace Titanium ENCODE(OP_BazaarSearch) { - if (((*p)->size == sizeof(BazaarReturnDone_Struct)) || ((*p)->size == sizeof(BazaarWelcome_Struct))) { - - EQApplicationPacket *in = *p; - *p = nullptr; - dest->FastQueuePacket(&in, ack_req); - return; - } - - //consume the packet EQApplicationPacket *in = *p; *p = nullptr; - //store away the emu struct - unsigned char *__emu_buffer = in->pBuffer; - BazaarSearchResults_Struct *emu = (BazaarSearchResults_Struct *)__emu_buffer; + uint32 action = *(uint32 *)in->pBuffer; - //determine and verify length - int entrycount = in->size / sizeof(BazaarSearchResults_Struct); - if (entrycount == 0 || (in->size % sizeof(BazaarSearchResults_Struct)) != 0) { - Log(Logs::General, Logs::Netcode, "[STRUCTS] Wrong size on outbound %s: Got %d, expected multiple of %d", - opcodes->EmuToName(in->GetOpcode()), in->size, sizeof(BazaarSearchResults_Struct)); - delete in; - return; + switch (action) { + case BazaarSearch: { + LogTrading( + "Encode OP_BazaarSearch(Ti) BazaarSearch action [{}]", + action + ); + std::vector results {}; + auto bsms = (BazaarSearchMessaging_Struct *)in->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(bsms->payload), + in->size - sizeof(BazaarSearchMessaging_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(results); + + auto size = results.size() * sizeof(structs::BazaarSearchResults_Struct); + auto buffer = new uchar[size]; + uchar *bufptr = buffer; + memset(buffer, 0, size); + + for (auto row = results.begin(); row != results.end(); ++row) { + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, structs::TiBazaarTraderBuyerActions::BazaarSearch); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->trader_entity_id); + bufptr += 4; + VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->item_id); + VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->serial_number); + bufptr += 4; + if (row->stackable) { + strn0cpy( + reinterpret_cast(bufptr), + fmt::format("{}({})", row->item_name.c_str(), row->charges).c_str(), + 64 + ); + } + else { + strn0cpy( + reinterpret_cast(bufptr), + fmt::format("{}({})", row->item_name.c_str(), row->count).c_str(), + 64 + ); + } + bufptr += 64; + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->cost); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->item_stat); + } + + auto outapp = new EQApplicationPacket(OP_BazaarSearch, size); + memcpy(outapp->pBuffer, buffer, size); + dest->FastQueuePacket(&outapp); + + safe_delete(outapp); + safe_delete_array(buffer); + safe_delete(in); + break; + } + case BazaarInspect: + case WelcomeMessage: { + LogTrading( + "Encode OP_BazaarSearch(Ti) BazaarInspect/WelcomeMessage action [{}]", + action + ); + dest->FastQueuePacket(&in, ack_req); + break; + } + default: { + LogTrading( + "Encode OP_BazaarSearch(Ti) unhandled action [{}]", + action + ); + dest->FastQueuePacket(&in, ack_req); + } } - - //make the EQ struct. - in->size = sizeof(structs::BazaarSearchResults_Struct)*entrycount; - in->pBuffer = new unsigned char[in->size]; - structs::BazaarSearchResults_Struct *eq = (structs::BazaarSearchResults_Struct *) in->pBuffer; - - //zero out the packet. We could avoid this memset by setting all fields (including unknowns) in the loop. - memset(in->pBuffer, 0, in->size); - - for (int i = 0; i < entrycount; i++, eq++, emu++) { - eq->Beginning.Action = emu->Beginning.Action; - eq->Beginning.Unknown001 = emu->Beginning.Unknown001; - eq->Beginning.Unknown002 = emu->Beginning.Unknown002; - eq->NumItems = emu->NumItems; - eq->SerialNumber = emu->SerialNumber; - eq->SellerID = emu->SellerID; - eq->Cost = emu->Cost; - eq->ItemStat = emu->ItemStat; - strcpy(eq->ItemName, emu->ItemName); - } - - delete[] __emu_buffer; - dest->FastQueuePacket(&in, ack_req); } ENCODE(OP_BecomeTrader) { - ENCODE_LENGTH_EXACT(BecomeTrader_Struct); - SETUP_DIRECT_ENCODE(BecomeTrader_Struct, structs::BecomeTrader_Struct); + uint32 action = *(uint32 *)(*p)->pBuffer; - OUT(ID); - OUT(Code); - - FINISH_ENCODE(); + switch (action) + { + case TraderOff: + { + ENCODE_LENGTH_EXACT(BecomeTrader_Struct); + SETUP_DIRECT_ENCODE(BecomeTrader_Struct, structs::BecomeTrader_Struct); + LogTrading( + "Encode OP_BecomeTrader(Ti) TraderOff action [{}] entity_id [{}] trader_name " + "[{}]", + emu->action, + emu->entity_id, + emu->trader_name + ); + eq->action = structs::TiBazaarTraderBuyerActions::Zero; + eq->entity_id = emu->entity_id; + FINISH_ENCODE(); + break; + } + case TraderOn: + { + ENCODE_LENGTH_EXACT(BecomeTrader_Struct); + SETUP_DIRECT_ENCODE(BecomeTrader_Struct, structs::BecomeTrader_Struct); + LogTrading( + "Encode OP_BecomeTrader(Ti) TraderOn action [{}] entity_id [{}] trader_name " + "[{}]", + emu->action, + emu->entity_id, + emu->trader_name + ); + eq->action = structs::TiBazaarTraderBuyerActions::BeginTraderMode; + eq->entity_id = emu->entity_id; + strn0cpy(eq->trader_name, emu->trader_name, sizeof(eq->trader_name)); + FINISH_ENCODE(); + break; + } + default: + { + LogTrading( + "Encode OP_BecomeTrader(Ti) unhandled action [{}] Sending packet as is.", + action + ); + EQApplicationPacket *in = *p; + *p = nullptr; + dest->FastQueuePacket(&in, ack_req); + } + } } ENCODE(OP_Buff) @@ -2010,32 +2082,170 @@ namespace Titanium ENCODE(OP_Trader) { - if ((*p)->size != sizeof(TraderBuy_Struct)) { - EQApplicationPacket *in = *p; - *p = nullptr; - dest->FastQueuePacket(&in, ack_req); - return; - } + auto action = *(uint32 *) (*p)->pBuffer; - ENCODE_FORWARD(OP_TraderBuy); + switch (action) { + case TraderOn: { + ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); + SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading( + "Encode OP_Trader BeginTraderMode action [{}]", + action + ); + + eq->action = structs::TiBazaarTraderBuyerActions::BeginTraderMode; + OUT(entity_id); + + FINISH_ENCODE(); + break; + } + case TraderOff: { + ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); + SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading( + "Encode OP_Trader EndTraderMode action [{}]", + action + ); + + eq->action = structs::TiBazaarTraderBuyerActions::EndTraderMode; + OUT(entity_id); + + FINISH_ENCODE(); + break; + } + case ListTraderItems: { + ENCODE_LENGTH_EXACT(Trader_Struct); + SETUP_DIRECT_ENCODE(Trader_Struct, structs::Trader_Struct); + LogTrading( + "Encode OP_Trader ListTraderItems action [{}]", + action + ); + + eq->action = structs::TiBazaarTraderBuyerActions::ListTraderItems; + std::copy_n(emu->items, UF::invtype::BAZAAR_SIZE, eq->item_id); + std::copy_n(emu->item_cost, UF::invtype::BAZAAR_SIZE, eq->item_cost); + + FINISH_ENCODE(); + break; + } + case BuyTraderItem: { + ENCODE_LENGTH_EXACT(TraderBuy_Struct); + SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "Encode OP_Trader item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); + + eq->action = structs::TiBazaarTraderBuyerActions::BuyTraderItem; + OUT(price); + OUT(trader_id); + OUT(item_id); + OUT(already_sold); + OUT(quantity); + strn0cpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + + FINISH_ENCODE(); + break; + } + case ItemMove: { + LogTrading( + "Encode OP_Trader ItemMove action [{}]", + action + ); + EQApplicationPacket *in = *p; + *p = nullptr; + dest->FastQueuePacket(&in, ack_req); + break; + } + default: { + EQApplicationPacket *in = *p; + *p = nullptr; + + dest->FastQueuePacket(&in, ack_req); + LogError("Unknown Encode OP_Trader action {} received. Unhandled.", action); + } + } } ENCODE(OP_TraderBuy) { ENCODE_LENGTH_EXACT(TraderBuy_Struct); SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "Encode OP_TraderBuy item_id [{}] price [{}] quantity [{}] trader_id [{}]", + emu->item_id, + emu->price, + emu->quantity, + emu->trader_id + ); - OUT(Action); - OUT(Price); - OUT(TraderID); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(Quantity); - OUT(AlreadySold); + OUT(action); + OUT(price); + OUT(trader_id); + OUT(item_id); + OUT(already_sold); + OUT(quantity); + OUT_str(item_name); FINISH_ENCODE(); } + ENCODE(OP_TraderShop) + { + auto action = *(uint32 *)(*p)->pBuffer; + + switch (action) { + case ClickTrader: { + ENCODE_LENGTH_EXACT(TraderClick_Struct); + SETUP_DIRECT_ENCODE(TraderClick_Struct, structs::TraderClick_Struct); + LogTrading( + "ClickTrader action [{}] trader_id [{}]", + action, + emu->TraderID + ); + + eq->action = 0; + eq->trader_id = emu->TraderID; + eq->approval = emu->Approval; + + FINISH_ENCODE(); + break; + } + case BuyTraderItem: { + ENCODE_LENGTH_EXACT(TraderBuy_Struct); + SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "Encode OP_TraderShop item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); + + eq->action = structs::TiBazaarTraderBuyerActions::BuyTraderItem; + OUT(price); + OUT(trader_id); + OUT(item_id); + OUT(already_sold); + OUT(quantity); + strn0cpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + + FINISH_ENCODE(); + break; + } + default: { + EQApplicationPacket *in = *p; + *p = nullptr; + + dest->FastQueuePacket(&in, ack_req); + LogError("Unknown Encode OP_TraderShop action [{}] received. Unhandled.", action); + } + } + } + ENCODE(OP_TributeItem) { ENCODE_LENGTH_EXACT(TributeItem_Struct); @@ -2286,6 +2496,53 @@ namespace Titanium FINISH_DIRECT_DECODE(); } + DECODE(OP_BazaarSearch) + { + uint32 action = *(uint32 *) __packet->pBuffer; + + switch (action) { + case structs::TiBazaarTraderBuyerActions::BazaarSearch: { + DECODE_LENGTH_EXACT(structs::BazaarSearch_Struct); + SETUP_DIRECT_DECODE(BazaarSearchCriteria_Struct, structs::BazaarSearch_Struct); + + emu->action = eq->Beginning.Action; + emu->item_stat = eq->ItemStat; + emu->max_cost = eq->MaxPrice; + emu->min_cost = eq->MinPrice; + emu->max_level = eq->MaxLlevel; + emu->min_level = eq->Minlevel; + emu->race = eq->Race; + emu->slot = eq->Slot; + emu->type = eq->Type == UINT32_MAX ? UINT8_MAX : eq->Type; + emu->trader_entity_id = eq->TraderID; + emu->trader_id = 0; + emu->_class = eq->Class_; + emu->search_scope = eq->TraderID > 0 ? NonRoFBazaarSearchScope : Local_Scope; + emu->max_results = RuleI(Bazaar, MaxSearchResults); + strn0cpy(emu->item_name, eq->Name, sizeof(emu->item_name)); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::TiBazaarTraderBuyerActions::BazaarInspect: { + SETUP_DIRECT_DECODE(BazaarInspect_Struct, structs::BazaarInspect_Struct); + + IN(action); + memcpy(emu->player_name, eq->player_name, sizeof(emu->player_name)); + IN(serial_number); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::TiBazaarTraderBuyerActions::WelcomeMessage: { + break; + } + default: { + LogTrading("(Ti) Unhandled action [{}]", action); + } + } + } + DECODE(OP_Buff) { DECODE_LENGTH_EXACT(structs::SpellBuffPacket_Struct); @@ -2925,18 +3182,90 @@ namespace Titanium FINISH_DIRECT_DECODE(); } + DECODE(OP_Trader) + { + auto action = (uint32) __packet->pBuffer[0]; + + switch (action) { + case structs::TiBazaarTraderBuyerActions::BeginTraderMode: { + DECODE_LENGTH_EXACT(structs::BeginTrader_Struct); + SETUP_DIRECT_DECODE(ClickTrader_Struct, structs::BeginTrader_Struct); + LogTrading( + "Decode OP_Trader BeginTraderMode action [{}]", + action + ); + + emu->action = TraderOn; + emu->unknown_004 = 0; + std::copy_n(eq->serial_number, Titanium::invtype::BAZAAR_SIZE, emu->serial_number); + std::copy_n(eq->cost, Titanium::invtype::BAZAAR_SIZE, emu->item_cost); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::TiBazaarTraderBuyerActions::EndTraderMode: { + DECODE_LENGTH_EXACT(structs::Trader_ShowItems_Struct); + SETUP_DIRECT_DECODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading( + "Decode OP_Trader(Ti) EndTraderMode action [{}]", + action + ); + + emu->action = TraderOff; + IN(entity_id); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::TiBazaarTraderBuyerActions::PriceUpdate: + case structs::TiBazaarTraderBuyerActions::ItemMove: + case structs::TiBazaarTraderBuyerActions::EndTransaction: + case structs::TiBazaarTraderBuyerActions::ListTraderItems: { + LogTrading( + "Decode OP_Trader(Ti) Price/ItemMove/EndTransaction/ListTraderItems action [{}]", + action + ); + break; + } + case structs::TiBazaarTraderBuyerActions::ReconcileItems: { + break; + } + default: { + LogError("Unhandled(Ti) action [{}] received.", action); + } + } + } + DECODE(OP_TraderBuy) { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); MEMSET_IN(TraderBuy_Struct); - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); + IN(action); + IN(price); + IN(trader_id); + memcpy(emu->item_name, eq->item_name, sizeof(emu->item_name)); + IN(item_id); + IN(quantity); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_TraderShop) + { + DECODE_LENGTH_EXACT(structs::TraderClick_Struct); + SETUP_DIRECT_DECODE(TraderClick_Struct, structs::TraderClick_Struct); + LogTrading( + "(Ti) action [{}] trader_id [{}] approval [{}]", + eq->action, + eq->trader_id, + eq->approval + ); + + emu->Code = ClickTrader; + emu->TraderID = eq->trader_id; + emu->Approval = eq->approval; FINISH_DIRECT_DECODE(); } diff --git a/common/patches/titanium_ops.h b/common/patches/titanium_ops.h index 9f4867965..a891b8102 100644 --- a/common/patches/titanium_ops.h +++ b/common/patches/titanium_ops.h @@ -82,6 +82,7 @@ E(OP_TaskDescription) E(OP_Track) E(OP_Trader) E(OP_TraderBuy) +E(OP_TraderShop) E(OP_TributeItem) E(OP_VetRewardsAvaliable) E(OP_WearChange) @@ -92,6 +93,7 @@ E(OP_ZoneSpawns) D(OP_AdventureMerchantSell) D(OP_ApplyPoison) D(OP_AugmentItem) +D(OP_BazaarSearch) D(OP_Buff) D(OP_Bug) D(OP_CastSpell) @@ -123,7 +125,9 @@ D(OP_ReadBook) D(OP_SetServerFilter) D(OP_ShopPlayerSell) D(OP_ShopRequest) +D(OP_Trader) D(OP_TraderBuy) +D(OP_TraderShop) D(OP_TradeSkillCombine) D(OP_TributeItem) D(OP_WearChange) diff --git a/common/patches/titanium_structs.h b/common/patches/titanium_structs.h index 7030dd565..831198363 100644 --- a/common/patches/titanium_structs.h +++ b/common/patches/titanium_structs.h @@ -2273,24 +2273,29 @@ struct BazaarWelcome_Struct { }; struct BazaarSearch_Struct { - BazaarWindowStart_Struct beginning; - uint32 traderid; - uint32 class_; - uint32 race; - uint32 stat; - uint32 slot; - uint32 type; - char name[64]; - uint32 minprice; - uint32 maxprice; - uint32 minlevel; - uint32 maxlevel; + BazaarWindowStart_Struct Beginning; + uint32 TraderID; + uint32 Class_; + uint32 Race; + uint32 ItemStat; + uint32 Slot; + uint32 Type; + char Name[64]; + uint32 MinPrice; + uint32 MaxPrice; + uint32 Minlevel; + uint32 MaxLlevel; }; -struct BazaarInspect_Struct{ + +struct BazaarInspect_Struct { + uint32 action; + char player_name[64]; + uint32 unknown_068; + uint32 serial_number; + uint32 unknown_076; uint32 item_id; - uint32 unknown; - char name[64]; }; + struct BazaarReturnDone_Struct{ uint32 type; uint32 traderid; @@ -2298,16 +2303,17 @@ struct BazaarReturnDone_Struct{ uint32 unknown12; uint32 unknown16; }; + struct BazaarSearchResults_Struct { BazaarWindowStart_Struct Beginning; - uint32 SellerID; - uint32 NumItems; // Don't know. Don't know the significance of this field. - uint32 SerialNumber; - uint32 Unknown016; - uint32 Unknown020; // Something to do with stats as well - char ItemName[64]; - uint32 Cost; - uint32 ItemStat; + uint32 entity_id; + uint32 unknown_008; + uint32 item_id; + uint32 serial_number; + uint32 unknown_020; + char item_name[64]; + uint32 item_cost; + uint32 item_stat; }; struct ServerSideFilters_Struct { @@ -2454,11 +2460,18 @@ struct WhoAllReturnStruct { struct WhoAllPlayer player[0]; }; +struct BeginTrader_Struct { + uint32 action; + uint32 unknown04; + uint64 serial_number[80]; + uint32 cost[80]; +}; + struct Trader_Struct { - uint32 code; - uint32 itemid[160]; - uint32 unknown; - uint32 itemcost[80]; + uint32 action; + uint32 unknown004; + uint64 item_id[80]; + uint32 item_cost[80]; }; struct ClickTrader_Struct { @@ -2471,27 +2484,28 @@ struct GetItems_Struct{ uint32 items[80]; }; -struct BecomeTrader_Struct{ - uint32 ID; - uint32 Code; +struct BecomeTrader_Struct { + uint32 entity_id; + uint32 action; + char trader_name[64]; }; struct Trader_ShowItems_Struct{ - uint32 code; - uint32 traderid; + uint32 action; + uint32 entity_id; uint32 unknown08[3]; }; struct TraderBuy_Struct { -/*000*/ uint32 Action; -/*004*/ uint32 Price; -/*008*/ uint32 TraderID; -/*012*/ char ItemName[64]; -/*076*/ uint32 Unknown076; -/*080*/ uint32 ItemID; -/*084*/ uint32 AlreadySold; -/*088*/ uint32 Quantity; -/*092*/ uint32 Unknown092; +/*000*/ uint32 action; +/*004*/ uint32 price; +/*008*/ uint32 trader_id; +/*012*/ char item_name[64]; +/*076*/ uint32 unknown_076; +/*080*/ uint32 item_id; +/*084*/ uint32 already_sold; +/*088*/ uint32 quantity; +/*092*/ uint32 unknown_092; }; @@ -2517,8 +2531,9 @@ struct TraderDelItem_Struct{ }; struct TraderClick_Struct{ - uint32 traderid; - uint32 unknown4[2]; + uint32 trader_id; + uint32 action; + uint32 unknown_004; uint32 approval; }; @@ -3744,6 +3759,21 @@ struct GuildMemberRank_Struct { /*076*/ uint32 alt_banker; //Banker/Alt bit 00 - none 10 - Alt 11 - Alt and Banker 01 - Banker. Banker not functional for RoF2+ }; +enum TiBazaarTraderBuyerActions { + Zero = 0, + BeginTraderMode = 1, + EndTraderMode = 2, + PriceUpdate = 3, + EndTransaction = 4, + BazaarSearch = 7, + WelcomeMessage = 9, + BuyTraderItem = 10, + ListTraderItems = 11, + BazaarInspect = 18, + ItemMove = 19, + ReconcileItems = 20 +}; + }; /*structs*/ }; /*Titanium*/ diff --git a/common/patches/uf.cpp b/common/patches/uf.cpp index 290468cd3..da07f068e 100644 --- a/common/patches/uf.cpp +++ b/common/patches/uf.cpp @@ -37,6 +37,8 @@ #include "../races.h" #include "../raid.h" #include "../guilds.h" +//#include "../repositories/trader_repository.h" +#include "../cereal/include/cereal/types/vector.hpp" #include #include @@ -307,50 +309,134 @@ namespace UF EQApplicationPacket *in = *p; *p = nullptr; - char *Buffer = (char *)in->pBuffer; + uint32 action = *(uint32 *)in->pBuffer; - uint8 SubAction = VARSTRUCT_DECODE_TYPE(uint8, Buffer); + switch (action) { + case BazaarSearch: { + LogTrading( + "Encode OP_BazaarSearch(UF) BazaarSearch action [{}]", + action + ); + std::vector results {}; + auto bsms = (BazaarSearchMessaging_Struct *)in->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(bsms->payload), + in->size - sizeof(BazaarSearchMessaging_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(results); - if (SubAction != BazaarSearchResults) - { - dest->FastQueuePacket(&in, ack_req); - return; + auto size = results.size() * sizeof(BazaarSearchResults_Struct); + auto buffer = new uchar[size]; + uchar *bufptr = buffer; + memset(buffer, 0, size); + + for (auto row = results.begin(); row != results.end(); ++row) { + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, structs::UFBazaarTraderBuyerActions::BazaarSearch); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->trader_entity_id); + strn0cpy(reinterpret_cast(bufptr), row->trader_name.c_str(), 64); + bufptr += 64; + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 1); + VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->item_id); + VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->serial_number); + bufptr += 4; + if (row->stackable) { + strn0cpy( + reinterpret_cast(bufptr), + fmt::format("{}({})", row->item_name.c_str(), row->charges).c_str(), + 64 + ); + } + else { + strn0cpy( + reinterpret_cast(bufptr), + fmt::format("{}({})", row->item_name.c_str(), row->count).c_str(), + 64 + ); + } + bufptr += 64; + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->cost); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->item_stat); + } + + auto outapp = new EQApplicationPacket(OP_BazaarSearch, size); + memcpy(outapp->pBuffer, buffer, size); + dest->FastQueuePacket(&outapp); + + safe_delete(outapp); + safe_delete_array(buffer); + safe_delete(in); + break; + } + case BazaarInspect: + case WelcomeMessage: { + LogTrading( + "Encode OP_BazaarSearch(UF) BazaarInspect/WelcomeMessage action [{}]", + action + ); + dest->FastQueuePacket(&in, ack_req); + break; + } + default: { + LogTrading( + "Encode OP_BazaarSearch(UF) unhandled action [{}]", + action + ); + dest->FastQueuePacket(&in, ack_req); + } } + } - unsigned char *__emu_buffer = in->pBuffer; + ENCODE(OP_BecomeTrader) + { + uint32 action = *(uint32 *)(*p)->pBuffer; - BazaarSearchResults_Struct *emu = (BazaarSearchResults_Struct *)__emu_buffer; - - int EntryCount = in->size / sizeof(BazaarSearchResults_Struct); - - if (EntryCount == 0 || (in->size % sizeof(BazaarSearchResults_Struct)) != 0) + switch (action) { - LogNetcode("[STRUCTS] Wrong size on outbound [{}]: Got [{}], expected multiple of [{}]", opcodes->EmuToName(in->GetOpcode()), in->size, sizeof(BazaarSearchResults_Struct)); - delete in; - return; + case TraderOff: + { + ENCODE_LENGTH_EXACT(BecomeTrader_Struct); + SETUP_DIRECT_ENCODE(BecomeTrader_Struct, structs::BecomeTrader_Struct); + LogTrading( + "Encode OP_BecomeTrader(UF) TraderOff action [{}] entity_id [{}] trader_name " + "[{}]", + emu->action, + emu->entity_id, + emu->trader_name + ); + eq->action = structs::UFBazaarTraderBuyerActions::Zero; + eq->entity_id = emu->entity_id; + FINISH_ENCODE(); + break; + } + case TraderOn: + { + ENCODE_LENGTH_EXACT(BecomeTrader_Struct); + SETUP_DIRECT_ENCODE(BecomeTrader_Struct, structs::BecomeTrader_Struct); + LogTrading( + "Encode OP_BecomeTrader(UF) TraderOn action [{}] entity_id [{}] trader_name " + "[{}]", + emu->action, + emu->entity_id, + emu->trader_name + ); + eq->action = structs::UFBazaarTraderBuyerActions::BeginTraderMode; + eq->entity_id = emu->entity_id; + strn0cpy(eq->trader_name, emu->trader_name, sizeof(eq->trader_name)); + FINISH_ENCODE(); + break; + } + default: + { + LogTrading( + "Encode OP_BecomeTrader(UF) unhandled action [{}] Sending packet as is.", + action + ); + EQApplicationPacket *in = *p; + *p = nullptr; + dest->FastQueuePacket(&in, ack_req); + } } - - in->size = EntryCount * sizeof(structs::BazaarSearchResults_Struct); - in->pBuffer = new unsigned char[in->size]; - memset(in->pBuffer, 0, in->size); - - structs::BazaarSearchResults_Struct *eq = (structs::BazaarSearchResults_Struct *)in->pBuffer; - - for (int i = 0; i < EntryCount; ++i, ++emu, ++eq) - { - OUT(Beginning.Action); - OUT(SellerID); - memcpy(eq->SellerName, emu->SellerName, sizeof(eq->SellerName)); - OUT(NumItems); - OUT(ItemID); - OUT(SerialNumber); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(Cost); - OUT(ItemStat); - } - - delete[] __emu_buffer; - dest->FastQueuePacket(&in, ack_req); } ENCODE(OP_Buff) @@ -2743,32 +2829,170 @@ namespace UF ENCODE(OP_Trader) { - if ((*p)->size != sizeof(TraderBuy_Struct)) { - EQApplicationPacket *in = *p; - *p = nullptr; - dest->FastQueuePacket(&in, ack_req); - return; - } + auto action = *(uint32 *) (*p)->pBuffer; - ENCODE_FORWARD(OP_TraderBuy); + switch (action) { + case TraderOn: { + ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); + SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading( + "Encode OP_Trader BeginTraderMode action [{}]", + action + ); + + eq->action = structs::UFBazaarTraderBuyerActions::BeginTraderMode; + OUT(entity_id); + + FINISH_ENCODE(); + break; + } + case TraderOff: { + ENCODE_LENGTH_EXACT(Trader_ShowItems_Struct); + SETUP_DIRECT_ENCODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading( + "Encode OP_Trader EndTraderMode action [{}]", + action + ); + + eq->action = structs::UFBazaarTraderBuyerActions::EndTraderMode; + OUT(entity_id); + + FINISH_ENCODE(); + break; + } + case ListTraderItems: { + ENCODE_LENGTH_EXACT(Trader_Struct); + SETUP_DIRECT_ENCODE(Trader_Struct, structs::Trader_Struct); + LogTrading( + "Encode OP_Trader ListTraderItems action [{}]", + action + ); + + eq->action = structs::UFBazaarTraderBuyerActions::ListTraderItems; + std::copy_n(emu->items, UF::invtype::BAZAAR_SIZE, eq->item_id); + std::copy_n(emu->item_cost, UF::invtype::BAZAAR_SIZE, eq->item_cost); + + FINISH_ENCODE(); + break; + } + case BuyTraderItem: { + ENCODE_LENGTH_EXACT(TraderBuy_Struct); + SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "Encode OP_Trader item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); + + eq->action = structs::UFBazaarTraderBuyerActions::BuyTraderItem; + OUT(price); + OUT(trader_id); + OUT(item_id); + OUT(already_sold); + OUT(quantity); + strn0cpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + + FINISH_ENCODE(); + break; + } + case ItemMove: { + LogTrading( + "Encode OP_Trader ItemMove action [{}]", + action + ); + EQApplicationPacket *in = *p; + *p = nullptr; + dest->FastQueuePacket(&in, ack_req); + break; + } + default: { + EQApplicationPacket *in = *p; + *p = nullptr; + + dest->FastQueuePacket(&in, ack_req); + LogError("Unknown Encode OP_Trader action {} received. Unhandled.", action); + } + } } ENCODE(OP_TraderBuy) { ENCODE_LENGTH_EXACT(TraderBuy_Struct); SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "Encode OP_TraderBuy item_id [{}] price [{}] quantity [{}] trader_id [{}]", + emu->item_id, + emu->price, + emu->quantity, + emu->trader_id + ); - OUT(Action); - OUT(Price); - OUT(TraderID); - memcpy(eq->ItemName, emu->ItemName, sizeof(eq->ItemName)); - OUT(ItemID); - OUT(Quantity); - OUT(AlreadySold); + OUT(action); + OUT(price); + OUT(trader_id); + OUT(item_id); + OUT(already_sold); + OUT(quantity); + OUT_str(item_name); FINISH_ENCODE(); } + ENCODE(OP_TraderShop) + { + auto action = *(uint32 *)(*p)->pBuffer; + + switch (action) { + case ClickTrader: { + ENCODE_LENGTH_EXACT(TraderClick_Struct); + SETUP_DIRECT_ENCODE(TraderClick_Struct, structs::TraderClick_Struct); + LogTrading( + "ClickTrader action [{}] trader_id [{}]", + action, + emu->TraderID + ); + + eq->action = 0; + eq->trader_id = emu->TraderID; + eq->approval = emu->Approval; + + FINISH_ENCODE(); + break; + } + case BuyTraderItem: { + ENCODE_LENGTH_EXACT(TraderBuy_Struct); + SETUP_DIRECT_ENCODE(TraderBuy_Struct, structs::TraderBuy_Struct); + LogTrading( + "Encode OP_TraderShop item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); + + eq->action = structs::UFBazaarTraderBuyerActions::BuyTraderItem; + OUT(price); + OUT(trader_id); + OUT(item_id); + OUT(already_sold); + OUT(quantity); + strn0cpy(eq->item_name, emu->item_name, sizeof(eq->item_name)); + + FINISH_ENCODE(); + break; + } + default: { + EQApplicationPacket *in = *p; + *p = nullptr; + + dest->FastQueuePacket(&in, ack_req); + LogError("Unknown Encode OP_TraderShop action [{}] received. Unhandled.", action); + } + } + } + ENCODE(OP_TributeItem) { ENCODE_LENGTH_EXACT(TributeItem_Struct); @@ -3040,7 +3264,7 @@ namespace UF Bitfields->targetable = 1; Bitfields->targetable_with_hotkey = emu->targetable_with_hotkey ? 1 : 0; Bitfields->statue = 0; - Bitfields->trader = 0; + Bitfields->trader = emu->trader ? 1 : 0; Bitfields->buyer = 0; Bitfields->showname = ShowName; @@ -3363,20 +3587,49 @@ namespace UF DECODE(OP_BazaarSearch) { - char *Buffer = (char *)__packet->pBuffer; + uint32 action = *(uint32 *) __packet->pBuffer; - uint8 SubAction = VARSTRUCT_DECODE_TYPE(uint8, Buffer); + switch (action) { + case structs::UFBazaarTraderBuyerActions::BazaarSearch: { + DECODE_LENGTH_EXACT(structs::BazaarSearch_Struct); + SETUP_DIRECT_DECODE(BazaarSearchCriteria_Struct, structs::BazaarSearch_Struct); - if ((SubAction != BazaarInspectItem) || (__packet->size != sizeof(structs::NewBazaarInspect_Struct))) - return; + emu->action = eq->Beginning.Action; + emu->item_stat = eq->ItemStat; + emu->max_cost = eq->MaxPrice; + emu->min_cost = eq->MinPrice; + emu->max_level = eq->MaxLlevel; + emu->min_level = eq->Minlevel; + emu->race = eq->Race; + emu->slot = eq->Slot; + emu->type = eq->Type == UINT32_MAX ? UINT8_MAX : eq->Type; + emu->trader_entity_id = eq->TraderID; + emu->trader_id = 0; + emu->_class = eq->Class_; + emu->search_scope = eq->TraderID > 0 ? NonRoFBazaarSearchScope : Local_Scope; + emu->max_results = RuleI(Bazaar, MaxSearchResults); + strn0cpy(emu->item_name, eq->Name, sizeof(emu->item_name)); - SETUP_DIRECT_DECODE(NewBazaarInspect_Struct, structs::NewBazaarInspect_Struct); - MEMSET_IN(structs::NewBazaarInspect_Struct); - IN(Beginning.Action); - memcpy(emu->Name, eq->Name, sizeof(emu->Name)); - IN(SerialNumber); + FINISH_DIRECT_DECODE(); + break; + } + case structs::UFBazaarTraderBuyerActions::BazaarInspect: { + SETUP_DIRECT_DECODE(BazaarInspect_Struct, structs::BazaarInspect_Struct); - FINISH_DIRECT_DECODE(); + IN(action); + memcpy(emu->player_name, eq->player_name, sizeof(emu->player_name)); + IN(serial_number); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::UFBazaarTraderBuyerActions::WelcomeMessage: { + break; + } + default: { + LogTrading("(UF) Unhandled action [{}]", action); + } + } } DECODE(OP_BookButton) @@ -4059,18 +4312,97 @@ namespace UF FINISH_DIRECT_DECODE(); } + DECODE(OP_Trader) + { + auto action = (uint32) __packet->pBuffer[0]; + + switch (action) { + case structs::UFBazaarTraderBuyerActions::BeginTraderMode: { + DECODE_LENGTH_EXACT(structs::BeginTrader_Struct); + SETUP_DIRECT_DECODE(ClickTrader_Struct, structs::BeginTrader_Struct); + LogTrading( + "Decode OP_Trader BeginTraderMode action [{}]", + action + ); + + emu->action = TraderOn; + emu->unknown_004 = 0; + std::copy_n(eq->serial_number, UF::invtype::BAZAAR_SIZE, emu->serial_number); + std::copy_n(eq->cost, UF::invtype::BAZAAR_SIZE, emu->item_cost); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::UFBazaarTraderBuyerActions::EndTraderMode: { + DECODE_LENGTH_EXACT(structs::Trader_ShowItems_Struct); + SETUP_DIRECT_DECODE(Trader_ShowItems_Struct, structs::Trader_ShowItems_Struct); + LogTrading( + "Decode OP_Trader(UF) EndTraderMode action [{}]", + action + ); + + emu->action = TraderOff; + IN(entity_id); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::UFBazaarTraderBuyerActions::PriceUpdate: + case structs::UFBazaarTraderBuyerActions::ItemMove: + case structs::UFBazaarTraderBuyerActions::EndTransaction: + case structs::UFBazaarTraderBuyerActions::ListTraderItems: { + LogTrading( + "Decode OP_Trader(UF) Price/ItemMove/EndTransaction/ListTraderItems action [{}]", + action + ); + break; + } + case structs::UFBazaarTraderBuyerActions::ReconcileItems: { + break; + } + default: { + LogError("Unhandled(UF) action [{}] received.", action); + } + } + } + DECODE(OP_TraderBuy) { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); SETUP_DIRECT_DECODE(TraderBuy_Struct, structs::TraderBuy_Struct); - MEMSET_IN(TraderBuy_Struct); + LogTrading( + "Decode OP_TraderBuy(UF) item_id [{}] price [{}] quantity [{}] trader_id [{}]", + eq->item_id, + eq->price, + eq->quantity, + eq->trader_id + ); - IN(Action); - IN(Price); - IN(TraderID); - memcpy(emu->ItemName, eq->ItemName, sizeof(emu->ItemName)); - IN(ItemID); - IN(Quantity); + emu->action = BuyTraderItem; + IN(price); + IN(trader_id); + IN(item_id); + IN(quantity); + IN(already_sold); + strn0cpy(emu->item_name, eq->item_name, sizeof(eq->item_name)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_TraderShop) + { + DECODE_LENGTH_EXACT(structs::TraderClick_Struct); + SETUP_DIRECT_DECODE(TraderClick_Struct, structs::TraderClick_Struct); + LogTrading( + "(UF) action [{}] trader_id [{}] approval [{}]", + eq->action, + eq->trader_id, + eq->approval + ); + + emu->Code = ClickTrader; + emu->TraderID = eq->trader_id; + emu->Approval = eq->approval; FINISH_DIRECT_DECODE(); } diff --git a/common/patches/uf_ops.h b/common/patches/uf_ops.h index 2ed03364f..16bdbf9bf 100644 --- a/common/patches/uf_ops.h +++ b/common/patches/uf_ops.h @@ -27,6 +27,7 @@ E(OP_ApplyPoison) E(OP_AugmentInfo) E(OP_Barter) E(OP_BazaarSearch) +E(OP_BecomeTrader) E(OP_Buff) E(OP_BuffCreate) E(OP_CancelTrade) @@ -100,6 +101,7 @@ E(OP_TaskDescription) E(OP_Track) E(OP_Trader) E(OP_TraderBuy) +E(OP_TraderShop) E(OP_TributeItem) E(OP_VetRewardsAvaliable) E(OP_WearChange) @@ -160,7 +162,9 @@ D(OP_SetServerFilter) D(OP_ShopPlayerBuy) D(OP_ShopPlayerSell) D(OP_ShopRequest) +D(OP_Trader) D(OP_TraderBuy) +D(OP_TraderShop) D(OP_TradeSkillCombine) D(OP_TributeItem) D(OP_WearChange) diff --git a/common/patches/uf_structs.h b/common/patches/uf_structs.h index 1f0ce2527..7f48929c5 100644 --- a/common/patches/uf_structs.h +++ b/common/patches/uf_structs.h @@ -2640,23 +2640,23 @@ struct EnvDamage2_Struct { //Bazaar Stuff enum { - BazaarTrader_StartTraderMode = 1, - BazaarTrader_EndTraderMode = 2, - BazaarTrader_UpdatePrice = 3, - BazaarTrader_EndTransaction = 4, - BazaarSearchResults = 7, - BazaarWelcome = 9, - BazaarBuyItem = 10, - BazaarTrader_ShowItems = 11, - BazaarSearchDone = 12, + BazaarTrader_StartTraderMode = 1, + BazaarTrader_EndTraderMode = 2, + BazaarTrader_UpdatePrice = 3, + BazaarTrader_EndTransaction = 4, + BazaarSearchResults = 7, + BazaarWelcome = 9, + BazaarBuyItem = 10, + BazaarTrader_ShowItems = 11, + BazaarSearchDone = 12, BazaarTrader_CustomerBrowsing = 13 }; enum { - BazaarPriceChange_Fail = 0, + BazaarPriceChange_Fail = 0, BazaarPriceChange_UpdatePrice = 1, - BazaarPriceChange_RemoveItem = 2, - BazaarPriceChange_AddItem = 3 + BazaarPriceChange_RemoveItem = 2, + BazaarPriceChange_AddItem = 3 }; struct BazaarWindowStart_Struct { @@ -2687,10 +2687,14 @@ struct BazaarSearch_Struct { uint32 Minlevel; uint32 MaxLlevel; }; -struct BazaarInspect_Struct{ - uint32 ItemID; - uint32 Unknown004; - char Name[64]; + +struct BazaarInspect_Struct { + uint32 action; + char player_name[64]; + uint32 unknown_068; + uint32 serial_number; + uint32 unknown_076; + uint32 item_id; }; struct NewBazaarInspect_Struct { @@ -2929,10 +2933,17 @@ struct WhoAllPlayerPart4 { }; struct Trader_Struct { - uint32 code; - uint32 itemid[160]; - uint32 unknown; - uint32 itemcost[80]; + uint32 action; + uint32 unknown004; + uint64 item_id[80]; + uint32 item_cost[80]; +}; + +struct BeginTrader_Struct { + uint32 action; + uint32 unknown04; + uint64 serial_number[80]; + uint32 cost[80]; }; struct ClickTrader_Struct { @@ -2945,30 +2956,30 @@ struct GetItems_Struct{ uint32 items[80]; }; -struct BecomeTrader_Struct{ - uint32 id; - uint32 code; +struct BecomeTrader_Struct { + uint32 entity_id; + uint32 action; + char trader_name[64]; }; struct Trader_ShowItems_Struct{ - uint32 code; - uint32 traderid; + uint32 action; + uint32 entity_id; uint32 unknown08[3]; }; struct TraderBuy_Struct { -/*000*/ uint32 Action; -/*004*/ uint32 Unknown004; -/*008*/ uint32 Price; -/*012*/ uint32 Unknown008; // Probably high order bits of a 64 bit price. -/*016*/ uint32 TraderID; -/*020*/ char ItemName[64]; -/*084*/ uint32 Unknown076; -/*088*/ uint32 ItemID; -/*092*/ uint32 AlreadySold; -/*096*/ uint32 Quantity; -/*100*/ uint32 Unknown092; -/*104*/ + uint32 action; + uint32 unknown_004; + uint32 price; + uint32 unknown_008; // Probably high order bits of a 64 bit price. + uint32 trader_id; + char item_name[64]; + uint32 unknown_076; + uint32 item_id; + uint32 already_sold; + uint32 quantity; + uint32 unknown_092; }; struct TraderItemUpdate_Struct{ @@ -3002,8 +3013,9 @@ struct TraderDelItem_Struct{ }; struct TraderClick_Struct{ - uint32 traderid; - uint32 unknown4[2]; + uint32 trader_id; + uint32 action; + uint32 unknown_004; uint32 approval; }; @@ -4674,6 +4686,30 @@ struct SayLinkBodyFrame_Struct { /*050*/ }; +struct TraderPriceUpdate_Struct { +/*000*/ uint32 action; +/*004*/ uint32 sub_action; +/*008*/ int32 serial_number; +/*012*/ uint32 unknown_012; +/*016*/ uint32 new_price; +/*020*/ uint32 unknown_016; +}; + +enum UFBazaarTraderBuyerActions { + Zero = 0, + BeginTraderMode = 1, + EndTraderMode = 2, + PriceUpdate = 3, + EndTransaction = 4, + BazaarSearch = 7, + WelcomeMessage = 9, + BuyTraderItem = 10, + ListTraderItems = 11, + BazaarInspect = 18, + ItemMove = 19, + ReconcileItems = 20 +}; + }; /*structs*/ }; /*UF*/ diff --git a/common/repositories/base/base_trader_repository.h b/common/repositories/base/base_trader_repository.h index 4bd23b9aa..d7cdbbefc 100644 --- a/common/repositories/base/base_trader_repository.h +++ b/common/repositories/base/base_trader_repository.h @@ -19,40 +19,70 @@ class BaseTraderRepository { public: struct Trader { + uint64_t id; uint32_t char_id; uint32_t item_id; - uint32_t serialnumber; - int32_t charges; - uint32_t item_cost; + uint32_t aug_slot_1; + uint32_t aug_slot_2; + uint32_t aug_slot_3; + uint32_t aug_slot_4; + uint32_t aug_slot_5; + uint32_t aug_slot_6; + int32_t item_sn; + int32_t item_charges; + uint64_t item_cost; uint8_t slot_id; + uint32_t char_entity_id; + uint32_t char_zone_id; + int8_t active_transaction; }; static std::string PrimaryKey() { - return std::string("char_id"); + return std::string("id"); } static std::vector Columns() { return { + "id", "char_id", "item_id", - "serialnumber", - "charges", + "aug_slot_1", + "aug_slot_2", + "aug_slot_3", + "aug_slot_4", + "aug_slot_5", + "aug_slot_6", + "item_sn", + "item_charges", "item_cost", "slot_id", + "char_entity_id", + "char_zone_id", + "active_transaction", }; } static std::vector SelectColumns() { return { + "id", "char_id", "item_id", - "serialnumber", - "charges", + "aug_slot_1", + "aug_slot_2", + "aug_slot_3", + "aug_slot_4", + "aug_slot_5", + "aug_slot_6", + "item_sn", + "item_charges", "item_cost", "slot_id", + "char_entity_id", + "char_zone_id", + "active_transaction", }; } @@ -93,12 +123,22 @@ public: { Trader e{}; - e.char_id = 0; - e.item_id = 0; - e.serialnumber = 0; - e.charges = 0; - e.item_cost = 0; - e.slot_id = 0; + e.id = 0; + e.char_id = 0; + e.item_id = 0; + e.aug_slot_1 = 0; + e.aug_slot_2 = 0; + e.aug_slot_3 = 0; + e.aug_slot_4 = 0; + e.aug_slot_5 = 0; + e.aug_slot_6 = 0; + e.item_sn = 0; + e.item_charges = 0; + e.item_cost = 0; + e.slot_id = 0; + e.char_entity_id = 0; + e.char_zone_id = 0; + e.active_transaction = 0; return e; } @@ -109,7 +149,7 @@ public: ) { for (auto &trader : traders) { - if (trader.char_id == trader_id) { + if (trader.id == trader_id) { return trader; } } @@ -135,12 +175,22 @@ public: if (results.RowCount() == 1) { Trader e{}; - e.char_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.item_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.serialnumber = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.charges = row[3] ? static_cast(atoi(row[3])) : 0; - e.item_cost = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.slot_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.item_sn = row[9] ? static_cast(atoi(row[9])) : 0; + e.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; + e.item_cost = row[11] ? strtoull(row[11], nullptr, 10) : 0; + e.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.char_entity_id = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.char_zone_id = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.active_transaction = row[15] ? static_cast(atoi(row[15])) : 0; return e; } @@ -174,12 +224,21 @@ public: auto columns = Columns(); - v.push_back(columns[0] + " = " + std::to_string(e.char_id)); - v.push_back(columns[1] + " = " + std::to_string(e.item_id)); - v.push_back(columns[2] + " = " + std::to_string(e.serialnumber)); - v.push_back(columns[3] + " = " + std::to_string(e.charges)); - v.push_back(columns[4] + " = " + std::to_string(e.item_cost)); - v.push_back(columns[5] + " = " + std::to_string(e.slot_id)); + v.push_back(columns[1] + " = " + std::to_string(e.char_id)); + v.push_back(columns[2] + " = " + std::to_string(e.item_id)); + v.push_back(columns[3] + " = " + std::to_string(e.aug_slot_1)); + v.push_back(columns[4] + " = " + std::to_string(e.aug_slot_2)); + v.push_back(columns[5] + " = " + std::to_string(e.aug_slot_3)); + v.push_back(columns[6] + " = " + std::to_string(e.aug_slot_4)); + v.push_back(columns[7] + " = " + std::to_string(e.aug_slot_5)); + v.push_back(columns[8] + " = " + std::to_string(e.aug_slot_6)); + v.push_back(columns[9] + " = " + std::to_string(e.item_sn)); + v.push_back(columns[10] + " = " + std::to_string(e.item_charges)); + v.push_back(columns[11] + " = " + std::to_string(e.item_cost)); + v.push_back(columns[12] + " = " + std::to_string(e.slot_id)); + v.push_back(columns[13] + " = " + std::to_string(e.char_entity_id)); + v.push_back(columns[14] + " = " + std::to_string(e.char_zone_id)); + v.push_back(columns[15] + " = " + std::to_string(e.active_transaction)); auto results = db.QueryDatabase( fmt::format( @@ -187,7 +246,7 @@ public: TableName(), Strings::Implode(", ", v), PrimaryKey(), - e.char_id + e.id ) ); @@ -201,12 +260,22 @@ public: { std::vector v; + v.push_back(std::to_string(e.id)); v.push_back(std::to_string(e.char_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.serialnumber)); - v.push_back(std::to_string(e.charges)); + v.push_back(std::to_string(e.aug_slot_1)); + v.push_back(std::to_string(e.aug_slot_2)); + v.push_back(std::to_string(e.aug_slot_3)); + v.push_back(std::to_string(e.aug_slot_4)); + v.push_back(std::to_string(e.aug_slot_5)); + v.push_back(std::to_string(e.aug_slot_6)); + v.push_back(std::to_string(e.item_sn)); + v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.active_transaction)); auto results = db.QueryDatabase( fmt::format( @@ -217,7 +286,7 @@ public: ); if (results.Success()) { - e.char_id = results.LastInsertedID(); + e.id = results.LastInsertedID(); return e; } @@ -236,12 +305,22 @@ public: for (auto &e: entries) { std::vector v; + v.push_back(std::to_string(e.id)); v.push_back(std::to_string(e.char_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.serialnumber)); - v.push_back(std::to_string(e.charges)); + v.push_back(std::to_string(e.aug_slot_1)); + v.push_back(std::to_string(e.aug_slot_2)); + v.push_back(std::to_string(e.aug_slot_3)); + v.push_back(std::to_string(e.aug_slot_4)); + v.push_back(std::to_string(e.aug_slot_5)); + v.push_back(std::to_string(e.aug_slot_6)); + v.push_back(std::to_string(e.item_sn)); + v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.active_transaction)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -275,12 +354,22 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { Trader e{}; - e.char_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.item_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.serialnumber = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.charges = row[3] ? static_cast(atoi(row[3])) : 0; - e.item_cost = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.slot_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.item_sn = row[9] ? static_cast(atoi(row[9])) : 0; + e.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; + e.item_cost = row[11] ? strtoull(row[11], nullptr, 10) : 0; + e.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.char_entity_id = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.char_zone_id = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.active_transaction = row[15] ? static_cast(atoi(row[15])) : 0; all_entries.push_back(e); } @@ -305,12 +394,22 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { Trader e{}; - e.char_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.item_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.serialnumber = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.charges = row[3] ? static_cast(atoi(row[3])) : 0; - e.item_cost = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.slot_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.item_sn = row[9] ? static_cast(atoi(row[9])) : 0; + e.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; + e.item_cost = row[11] ? strtoull(row[11], nullptr, 10) : 0; + e.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.char_entity_id = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.char_zone_id = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.active_transaction = row[15] ? static_cast(atoi(row[15])) : 0; all_entries.push_back(e); } @@ -385,12 +484,22 @@ public: { std::vector v; + v.push_back(std::to_string(e.id)); v.push_back(std::to_string(e.char_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.serialnumber)); - v.push_back(std::to_string(e.charges)); + v.push_back(std::to_string(e.aug_slot_1)); + v.push_back(std::to_string(e.aug_slot_2)); + v.push_back(std::to_string(e.aug_slot_3)); + v.push_back(std::to_string(e.aug_slot_4)); + v.push_back(std::to_string(e.aug_slot_5)); + v.push_back(std::to_string(e.aug_slot_6)); + v.push_back(std::to_string(e.item_sn)); + v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.active_transaction)); auto results = db.QueryDatabase( fmt::format( @@ -413,12 +522,22 @@ public: for (auto &e: entries) { std::vector v; + v.push_back(std::to_string(e.id)); v.push_back(std::to_string(e.char_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.serialnumber)); - v.push_back(std::to_string(e.charges)); + v.push_back(std::to_string(e.aug_slot_1)); + v.push_back(std::to_string(e.aug_slot_2)); + v.push_back(std::to_string(e.aug_slot_3)); + v.push_back(std::to_string(e.aug_slot_4)); + v.push_back(std::to_string(e.aug_slot_5)); + v.push_back(std::to_string(e.aug_slot_6)); + v.push_back(std::to_string(e.item_sn)); + v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.active_transaction)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/items_repository.h b/common/repositories/items_repository.h index 50f17d8a8..919c11dd3 100644 --- a/common/repositories/items_repository.h +++ b/common/repositories/items_repository.h @@ -7,43 +7,6 @@ class ItemsRepository: public BaseItemsRepository { public: - - /** - * This file was auto generated and can be modified and extended upon - * - * Base repository methods are automatically - * generated in the "base" version of this repository. The base repository - * is immutable and to be left untouched, while methods in this class - * are used as extension methods for more specific persistence-layer - * accessors or mutators. - * - * Base Methods (Subject to be expanded upon in time) - * - * Note: Not all tables are designed appropriately to fit functionality with all base methods - * - * InsertOne - * UpdateOne - * DeleteOne - * FindOne - * GetWhere(std::string where_filter) - * DeleteWhere(std::string where_filter) - * InsertMany - * All - * - * Example custom methods in a repository - * - * ItemsRepository::GetByZoneAndVersion(int zone_id, int zone_version) - * ItemsRepository::GetWhereNeverExpires() - * ItemsRepository::GetWhereXAndY() - * ItemsRepository::DeleteWhereXAndY() - * - * Most of the above could be covered by base methods, but if you as a developer - * find yourself re-using logic for other parts of the code, its best to just make a - * method that can be re-used easily elsewhere especially if it can use a base repository - * method and encapsulate filters there - */ - - // Custom extended repository methods here static std::vector GetItemIDsBySearchCriteria( Database& db, std::string search_string, @@ -73,6 +36,8 @@ public: return item_id_list; } + + }; #endif //EQEMU_ITEMS_REPOSITORY_H diff --git a/common/repositories/trader_repository.h b/common/repositories/trader_repository.h index 31f8fae87..bee208d13 100644 --- a/common/repositories/trader_repository.h +++ b/common/repositories/trader_repository.h @@ -1,50 +1,224 @@ #ifndef EQEMU_TRADER_REPOSITORY_H #define EQEMU_TRADER_REPOSITORY_H -#include "../database.h" +#include "../../common/shareddb.h" #include "../strings.h" #include "base/base_trader_repository.h" +#include "items_repository.h" +#include "../../common/item_data.h" +#include "../../common/races.h" +#include "../cereal/include/cereal/archives/binary.hpp" +#include "../cereal/include/cereal/types/string.hpp" -class TraderRepository: public BaseTraderRepository { +class TraderRepository : public BaseTraderRepository { public: - /** - * This file was auto generated and can be modified and extended upon - * - * Base repository methods are automatically - * generated in the "base" version of this repository. The base repository - * is immutable and to be left untouched, while methods in this class - * are used as extension methods for more specific persistence-layer - * accessors or mutators. - * - * Base Methods (Subject to be expanded upon in time) - * - * Note: Not all tables are designed appropriately to fit functionality with all base methods - * - * InsertOne - * UpdateOne - * DeleteOne - * FindOne - * GetWhere(std::string where_filter) - * DeleteWhere(std::string where_filter) - * InsertMany - * All - * - * Example custom methods in a repository - * - * TraderRepository::GetByZoneAndVersion(int zone_id, int zone_version) - * TraderRepository::GetWhereNeverExpires() - * TraderRepository::GetWhereXAndY() - * TraderRepository::DeleteWhereXAndY() - * - * Most of the above could be covered by base methods, but if you as a developer - * find yourself re-using logic for other parts of the code, its best to just make a - * method that can be re-used easily elsewhere especially if it can use a base repository - * method and encapsulate filters there - */ + struct DistinctTraders_Struct { + uint32 trader_id; + uint32 zone_id; + uint32 entity_id; + std::string trader_name; + }; - // Custom extended repository methods here + struct BulkTraders_Struct { + uint32 count{0}; + uint32 name_length{0}; + std::vector traders{}; + }; + struct WelcomeData_Struct { + uint32 count_of_traders; + uint32 count_of_items; + }; + + static std::vector + GetBazaarSearchResults( + SharedDatabase &db, + BazaarSearchCriteria_Struct search, + uint32 char_zone_id + ); + + static BulkTraders_Struct GetDistinctTraders(Database &db) + { + BulkTraders_Struct all_entries{}; + std::vector distinct_traders; + + auto results = db.QueryDatabase( + "SELECT DISTINCT(t.char_id), t.char_zone_id, t.char_entity_id, c.name " + "FROM trader AS t " + "JOIN character_data AS c ON t.char_id = c.id;" + ); + + distinct_traders.reserve(results.RowCount()); + + for (auto row: results) { + DistinctTraders_Struct e{}; + + e.trader_id = Strings::ToInt(row[0]); + e.zone_id = Strings::ToInt(row[1]); + e.entity_id = Strings::ToInt(row[2]); + e.trader_name = row[3] ? row[3] : ""; + all_entries.name_length += e.trader_name.length() + 1; + + all_entries.traders.push_back(e); + } + all_entries.count = results.RowCount(); + return all_entries; + } + + static WelcomeData_Struct GetWelcomeData(Database &db) + { + WelcomeData_Struct e{}; + + auto results = db.QueryDatabase("SELECT COUNT(DISTINCT char_id), count(char_id) FROM trader;"); + + if (!results.RowCount()) { + return e; + } + + auto r = results.begin(); + e.count_of_traders = Strings::ToInt(r[0]); + e.count_of_items = Strings::ToInt(r[1]); + return e; + } + + static int UpdateItem(Database &db, uint32 char_id, uint32 new_price, uint32 item_id, uint32 item_charges) + { + std::vector items{}; + if (item_charges == 0) { + items = GetWhere( + db, + fmt::format( + "char_id = '{}' AND item_id = '{}'", + char_id, + item_id + ) + ); + } + else { + items = GetWhere( + db, + fmt::format( + "char_id = '{}' AND item_id = '{}' AND item_charges = '{}'", + char_id, + item_id, + item_charges + ) + ); + } + + if (items.empty()) { + return 0; + } + + for (auto &i: items) { + i.item_cost = new_price; + } + + return ReplaceMany(db, items); + } + + static Trader GetTraderItem(Database &db, uint32 trader_id, uint32 item_id, uint32 item_cost) + { + Trader item{}; + + auto query = fmt::format( + "SELECT t.char_id, t.item_id, t.serialnumber, t.charges, t.item_cost, t.slot_id, t.entity_id FROM trader AS t " + "WHERE t.entity_id = {} AND t.item_id = {} AND t.item_cost = {} " + "LIMIT 1;", + trader_id, + item_id, + item_cost + ); + auto results = db.QueryDatabase(query); + + if (results.RowCount() == 0) { + return item; + } + + auto row = results.begin(); + item.char_id = Strings::ToInt(row[0]); + item.item_id = Strings::ToInt(row[1]); + item.item_sn = Strings::ToInt(row[2]); + item.item_charges = Strings::ToInt(row[3]); + item.item_cost = Strings::ToInt(row[4]); + item.slot_id = Strings::ToInt(row[5]); + + return item; + } + + static int UpdateQuantity(Database &db, uint32 char_id, uint32 serial_number, int16 quantity) + { + const auto trader_item = GetWhere( + db, + fmt::format("char_id = '{}' AND item_sn = '{}' ", char_id, serial_number) + ); + + if (trader_item.empty() || trader_item.size() > 1) { + return 0; + } + + auto m = trader_item[0]; + m.item_charges = quantity; + + return UpdateOne(db, m); + } + + static Trader GetItemBySerialNumber(Database &db, uint32 serial_number) + { + Trader e{}; + const auto trader_item = GetWhere( + db, + fmt::format("`item_sn` = '{}' LIMIT 1", serial_number) + ); + + if (trader_item.empty()) { + return e; + } + else { + return trader_item.at(0); + } + } + + static Trader GetItemBySerialNumber(Database &db, std::string serial_number) + { + Trader e{}; + auto sn = Strings::ToUnsignedBigInt(serial_number); + const auto trader_item = GetWhere( + db, + fmt::format("`item_sn` = '{}' LIMIT 1", sn) + ); + + if (trader_item.empty()) { + return e; + } + else { + return trader_item.at(0); + } + } + + static int UpdateActiveTransaction(Database &db, uint32 id, bool status) + { + auto e = FindOne(db, id); + if (!e.id) { + return 0; + } + + e.active_transaction = status == true ? 1 : 0; + + return UpdateOne(db, e); + } + + static int DeleteMany(Database &db, const std::vector &entries) + { + std::vector delete_ids; + + for (auto const &e: entries) { + delete_ids.push_back(std::to_string(e.id)); + } + + return DeleteWhere(db, fmt::format("`id` IN({})", Strings::Implode(",", delete_ids))); + } }; #endif //EQEMU_TRADER_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index 50ff39084..113b582d1 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -783,6 +783,9 @@ RULE_BOOL(Bazaar, AuditTrail, false, "Setting whether a path to the trader shoul RULE_INT(Bazaar, MaxSearchResults, 50, "Maximum number of search results in Bazaar") RULE_BOOL(Bazaar, EnableWarpToTrader, true, "Setting whether teleport to the selected trader should be active") RULE_INT(Bazaar, MaxBarterSearchResults, 200, "The maximum results returned in the /barter search") +RULE_REAL(Bazaar, ParcelDeliveryCostMod, 0.20, "Cost of parcel delivery for a bazaar purchase as a percentage of item cost. Default is 20% of item cost. RoF+ Only.") +RULE_INT(Bazaar, VoucherDeliveryCost, 200, "Number of vouchers for direct delivery for a bazaar purchase. Default is 200 vouchers. RoF+ Only.") +RULE_BOOL(Bazaar, EnableParcelDelivery, true, "Enable bazaar purchases via parcel delivery. Default is True.") RULE_CATEGORY_END() RULE_CATEGORY(Mail) diff --git a/common/servertalk.h b/common/servertalk.h index 7830f0ecf..d39af49b4 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -138,6 +138,9 @@ #define ServerOP_RaidMOTD 0x0113 #define ServerOP_RaidNote 0x0114 +#define ServerOP_TraderMessaging 0x0120 +#define ServerOP_BazaarPurchase 0x0121 + #define ServerOP_InstanceUpdateTime 0x014F #define ServerOP_AdventureRequest 0x0150 #define ServerOP_AdventureRequestAccept 0x0151 @@ -1938,6 +1941,27 @@ struct ServerOP_GuildMessage_Struct { char url[2048]{0}; }; +struct TraderMessaging_Struct { + uint32 action; + uint32 zone_id; + uint32 trader_id; + uint32 entity_id; + char trader_name[64]; +}; + +struct BazaarPurchaseMessaging_Struct { + TraderBuy_Struct trader_buy_struct; + uint32 item_aug_1; + uint32 item_aug_2; + uint32 item_aug_3; + uint32 item_aug_4; + uint32 item_aug_5; + uint32 item_aug_6; + uint32 buyer_id; + uint32 item_quantity_available; + uint32 id; +}; + #pragma pack() #endif diff --git a/common/strings.cpp b/common/strings.cpp index 49bf98560..1f35e30eb 100644 --- a/common/strings.cpp +++ b/common/strings.cpp @@ -745,6 +745,15 @@ bool Strings::Contains(const std::string& subject, const std::string& search) return subject.find(search) != std::string::npos; } +bool Strings::ContainsLower(const std::string& subject, const std::string& search) +{ + if (subject.length() < search.length()) { + return false; + } + + return ToLower(subject).find(ToLower(search)) != std::string::npos; +} + uint32 Strings::TimeToSeconds(std::string time_string) { if (time_string.empty()) { diff --git a/common/strings.h b/common/strings.h index 896e18d1f..7c6f6462a 100644 --- a/common/strings.h +++ b/common/strings.h @@ -86,6 +86,7 @@ class Strings { public: static bool Contains(std::vector container, const std::string& element); static bool Contains(const std::string& subject, const std::string& search); + static bool ContainsLower(const std::string& subject, const std::string& search); static int ToInt(const std::string &s, int fallback = 0); static int64 ToBigInt(const std::string &s, int64 fallback = 0); static uint32 ToUnsignedInt(const std::string &s, uint32 fallback = 0); diff --git a/common/version.h b/common/version.h index 9d7d7d165..9cfe0efdd 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9279 +#define CURRENT_BINARY_DATABASE_VERSION 9280 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9044 #endif diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index c969ced09..518e312de 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -430,6 +430,7 @@ OP_BazaarSearch=0x39d6 OP_TraderDelItem=0x5829 OP_BecomeTrader=0x61b3 OP_TraderShop=0x31df +OP_TraderBulkSend=0x6a96 OP_Trader=0x4ef5 OP_Barter=0x243a OP_TraderBuy=0x0000 diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 4399a9c27..e15aa511f 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1424,6 +1424,7 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { case ServerOP_ReloadLoot: case ServerOP_RezzPlayerAccept: case ServerOP_SpawnStatusChange: + case ServerOP_TraderMessaging: case ServerOP_UpdateSpawn: case ServerOP_WWDialogueWindow: case ServerOP_WWLDoNUpdate: @@ -1744,6 +1745,18 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { break; } + case ServerOP_BazaarPurchase: { + auto in = (BazaarPurchaseMessaging_Struct *)pack->pBuffer; + if (in->trader_buy_struct.trader_id <= 0) { + LogTrading( + "World Message [{}] received with invalid trader_id [{}]", + "ServerOP_BazaarPurchase", + in->trader_buy_struct.trader_id + ); + } + + zoneserver_list.SendPacket(Zones::BAZAAR, pack); + } default: { LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); DumpPacket(pack->pBuffer, pack->size); diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index fcfff16d0..2b82aea4f 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -4157,7 +4157,7 @@ bool Client::CalcItemScale(uint32 slot_x, uint32 slot_y) { continue; // TEST CODE: test for bazaar trader crashing with charm items - if (Trader) + if (IsTrader()) if (i >= EQ::invbag::GENERAL_BAGS_BEGIN && i <= EQ::invbag::GENERAL_BAGS_END) { EQ::ItemInstance* parent_item = m_inv.GetItem(EQ::InventoryProfile::CalcSlotId(i)); if (parent_item && parent_item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) @@ -4249,7 +4249,7 @@ bool Client::DoItemEnterZone(uint32 slot_x, uint32 slot_y) { continue; // TEST CODE: test for bazaar trader crashing with charm items - if (Trader) + if (IsTrader()) if (i >= EQ::invbag::GENERAL_BAGS_BEGIN && i <= EQ::invbag::GENERAL_BAGS_END) { EQ::ItemInstance* parent_item = m_inv.GetItem(EQ::InventoryProfile::CalcSlotId(i)); if (parent_item && parent_item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) diff --git a/zone/client.cpp b/zone/client.cpp index 085e2cef9..aab46daa1 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -202,11 +202,11 @@ Client::Client(EQStreamInterface *ieqs) : Mob( ip = eqs->GetRemoteIP(); port = ntohs(eqs->GetRemotePort()); client_state = CLIENT_CONNECTING; - Trader=false; + SetTrader(false); Buyer = false; Haste = 0; - CustomerID = 0; - TraderID = 0; + SetCustomerID(0); + SetTraderID(0); TrackingID = 0; WID = 0; account_id = 0; @@ -421,8 +421,9 @@ Client::~Client() { if (merc) merc->Depop(); - if(Trader) - database.DeleteTraderItem(CharacterID()); + if(IsTrader()) { + TraderEndTrader(); + } if(Buyer) ToggleBuyerMode(false); @@ -2163,6 +2164,7 @@ void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) ns->spawn.anon = m_pp.anon; ns->spawn.gm = GetGM() ? 1 : 0; ns->spawn.guildID = GuildID(); + ns->spawn.trader = IsTrader(); // ns->spawn.linkdead = IsLD() ? 1 : 0; // ns->spawn.pvp = GetPVP(false) ? 1 : 0; ns->spawn.show_name = true; @@ -11969,7 +11971,7 @@ void Client::SendPath(Mob* target) RuleB(Bazaar, EnableWarpToTrader) && target->IsClient() && ( - target->CastToClient()->Trader || + target->CastToClient()->IsTrader() || target->CastToClient()->Buyer ) ) { diff --git a/zone/client.h b/zone/client.h index da9af847c..3ca55331d 100644 --- a/zone/client.h +++ b/zone/client.h @@ -69,6 +69,7 @@ namespace EQ #include "../common/events/player_events.h" #include "../common/data_verification.h" #include "../common/repositories/character_parcels_repository.h" +#include "../common/repositories/trader_repository.h" #ifdef _WINDOWS // since windows defines these within windef.h (which windows.h include) @@ -280,10 +281,20 @@ public: void AI_Stop(); void AI_Process(); void AI_SpellCast(); - void Trader_ShowItems(); + void TraderShowItems(); void Trader_CustomerBrowsing(Client *Customer); - void Trader_EndTrader(); - void Trader_StartTrader(); + + void TraderEndTrader(); + void TraderPriceUpdate(const EQApplicationPacket *app); + void SendBazaarDone(uint32 trader_id); + void SendBulkBazaarTraders(); + void DoBazaarInspect(const BazaarInspect_Struct &in); + void SendBazaarDeliveryCosts(); + static std::string DetermineMoneyString(uint64 copper); + + void SendTraderMode(BazaarTraderBarterActions status); + void TraderStartTrader(const EQApplicationPacket *app); +// void TraderPriceUpdate(const EQApplicationPacket *app); uint8 WithCustomer(uint16 NewCustomer); void KeyRingLoad(); void KeyRingAdd(uint32 item_id); @@ -315,15 +326,17 @@ public: 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); + void SendTraderItem(uint32 item_id,uint16 quantity, TraderRepository::Trader &trader); + void DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria); uint16 FindTraderItem(int32 SerialNumber,uint16 Quantity); uint32 FindTraderItemSerialNumber(int32 ItemID); EQ::ItemInstance* FindTraderItemBySerialNumber(int32 SerialNumber); - void FindAndNukeTraderItem(int32 item_id,int16 quantity,Client* customer,uint16 traderslot); - void NukeTraderItem(uint16 slot, int16 charges, int16 quantity, Client* customer, uint16 traderslot, int32 uniqueid, int32 itemid = 0); + void FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client* customer, uint16 trader_slot); + void NukeTraderItem(uint16 slot, int16 charges, int16 quantity, Client* customer, uint16 trader_slot, int32 serial_number, int32 item_id = 0); void ReturnTraderReq(const EQApplicationPacket* app,int16 traderitemcharges, uint32 itemid = 0); void TradeRequestFailed(const EQApplicationPacket* app); - void BuyTraderItem(TraderBuy_Struct* tbs,Client* trader,const EQApplicationPacket* app); + void BuyTraderItem(TraderBuy_Struct* tbs, Client* trader, const EQApplicationPacket* app); + void BuyTraderItemOutsideBazaar(TraderBuy_Struct* tbs, const EQApplicationPacket* app); void FinishTrade( Mob *with, bool finalizer = false, @@ -335,7 +348,7 @@ public: void DoParcelCancel(); void DoParcelSend(const Parcel_Struct *parcel_in); void DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in); - void SendParcel(const Parcel_Struct &parcel); + void SendParcel(Parcel_Struct &parcel); void SendParcelStatus(); void SendParcelAck(); void SendParcelRetrieveAck(); @@ -355,6 +368,13 @@ public: int32 FindNextFreeParcelSlot(uint32 char_id); void SendParcelIconStatus(); + void SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions action); + void SendBecomeTrader(BazaarTraderBarterActions action, uint32 trader_id); + + bool IsThereACustomer() const { return customer_id ? true : false; } + uint32 GetCustomerID() { return customer_id; } + void SetCustomerID(uint32 id) { customer_id = id; } + void SendBuyerResults(char *SearchQuery, uint32 SearchID); void ShowBuyLines(const EQApplicationPacket *app); void SellToBuyer(const EQApplicationPacket *app); @@ -485,7 +505,7 @@ public: void ServerFilter(SetServerFilter_Struct* filter); void BulkSendTraderInventory(uint32 char_id); - void SendSingleTraderItem(uint32 char_id, int uniqueid); + void SendSingleTraderItem(uint32 char_id, int serial_number); void BulkSendMerchantInventory(int merchant_id, int npcid); inline uint8 GetLanguageSkill(uint8 language_id) const { return m_pp.languages[language_id]; } @@ -1067,7 +1087,11 @@ public: bool IsValidSlot(uint32 slot); bool IsBankSlot(uint32 slot); - inline bool IsTrader() const { return(Trader); } + bool IsTrader() const { return trader; } + void SetTrader(bool status) { trader = status; } + uint16 GetTraderID() { return trader_id; } + void SetTraderID(uint16 id) { trader_id = id; } + inline bool IsBuyer() const { return(Buyer); } eqFilterMode GetFilter(eqFilterType filter_id) const { return ClientFilters[filter_id]; } void SetFilter(eqFilterType filter_id, eqFilterMode filter_mode) { ClientFilters[filter_id] = filter_mode; } @@ -1800,8 +1824,6 @@ private: void RemoveBandolier(const EQApplicationPacket *app); void SetBandolier(const EQApplicationPacket *app); - void HandleTraderPriceUpdate(const EQApplicationPacket *app); - int32 CalcItemATKCap() final; int32 CalcHaste(); @@ -1880,13 +1902,13 @@ private: uint16 controlling_boat_id; uint16 controlled_mob_id; uint16 TrackingID; - uint16 CustomerID; - uint16 TraderID; + bool trader; + uint16 trader_id; + uint16 customer_id; uint32 account_creation; uint8 firstlogon; uint32 mercid; // current merc uint8 mercSlot; // selected merc slot - bool Trader; bool Buyer; std::string BuyerWelcomeMessage; int32 m_parcel_platinum; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index bf36cd6b9..884504ba7 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -918,6 +918,10 @@ void Client::CompleteConnect() CastToClient()->FastQueuePacket(&outapp); } + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + SendBulkBazaarTraders(); + } + // TODO: load these states // We at least will set them to the correct state for now if (m_ClientVersionBit & EQ::versions::maskUFAndLater && GetPet()) { @@ -1074,9 +1078,6 @@ void Client::Handle_Connect_OP_ReqClientSpawn(const EQApplicationPacket *app) QueuePacket(outapp); safe_delete(outapp); - if (strncasecmp(zone->GetShortName(), "bazaar", 6) == 0) - SendBazaarWelcome(); - conn_state = ZoneContentsSent; return; @@ -3787,7 +3788,7 @@ void Client::Handle_OP_Barter(const EQApplicationPacket *app) case Barter_BuyerModeOn: { - if (!Trader) { + if (!IsTrader()) { ToggleBuyerMode(true); } else { @@ -3864,7 +3865,7 @@ void Client::Handle_OP_Barter(const EQApplicationPacket *app) case Barter_Welcome: { - SendBazaarWelcome(); + //SendBazaarWelcome(); break; } @@ -3918,7 +3919,7 @@ void Client::Handle_OP_BazaarInspect(const EQApplicationPacket *app) BazaarInspect_Struct* bis = (BazaarInspect_Struct*)app->pBuffer; - const EQ::ItemData* item = database.GetItem(bis->ItemID); + const EQ::ItemData* item = database.GetItem(bis->item_id); if (!item) { Message(Chat::Red, "Error: This item does not exist!"); @@ -3937,39 +3938,47 @@ void Client::Handle_OP_BazaarInspect(const EQApplicationPacket *app) void Client::Handle_OP_BazaarSearch(const EQApplicationPacket *app) { + uint32 action = *(uint32 *) app->pBuffer; - if (app->size == sizeof(BazaarSearch_Struct)) { + switch (action) { + case BazaarSearch: { + BazaarSearchCriteria_Struct *bss = (BazaarSearchCriteria_Struct *) app->pBuffer; + BazaarSearchCriteria_Struct search_details{}; - BazaarSearch_Struct* bss = (BazaarSearch_Struct*)app->pBuffer; + search_details.action = BazaarSearchResults; + search_details.augment = bss->augment; + search_details._class = bss->_class; + search_details.item_stat = bss->item_stat; + search_details.min_cost = bss->min_cost; + search_details.max_cost = bss->max_cost; + search_details.min_level = bss->min_level; + search_details.max_level = bss->max_level; + search_details.max_results = bss->max_results; + search_details.prestige = bss->prestige; + search_details.race = bss->race; + search_details.search_scope = bss->search_scope; + search_details.slot = bss->slot; + search_details.trader_entity_id = bss->trader_entity_id; + search_details.trader_id = bss->trader_id; + search_details.type = bss->type; + strn0cpy(search_details.item_name, bss->item_name, sizeof(search_details.item_name)); - SendBazaarResults(bss->TraderID, bss->Class_, bss->Race, bss->ItemStat, bss->Slot, bss->Type, - bss->Name, bss->MinPrice * 1000, bss->MaxPrice * 1000); - } - else if (app->size == sizeof(BazaarWelcome_Struct)) { - - BazaarWelcome_Struct* bws = (BazaarWelcome_Struct*)app->pBuffer; - - if (bws->Beginning.Action == BazaarWelcome) - SendBazaarWelcome(); - } - else if (app->size == sizeof(NewBazaarInspect_Struct)) { - - NewBazaarInspect_Struct *nbis = (NewBazaarInspect_Struct*)app->pBuffer; - - Client *c = entity_list.GetClientByName(nbis->Name); - if (c) { - EQ::ItemInstance* inst = c->FindTraderItemBySerialNumber(nbis->SerialNumber); - if (inst) - SendItemPacket(0, inst, ItemPacketViewLink); + DoBazaarSearch(search_details); + break; + } + case BazaarInspect: { + auto in = (BazaarInspect_Struct *) app->pBuffer; + DoBazaarInspect(*in); + break; + } + case WelcomeMessage: { + SendBazaarWelcome(); + break; + } + default: { + LogError("Malformed BazaarSearch_Struct packet received, ignoring\n"); } - return; } - else { - LogTrading("Malformed BazaarSearch_Struct packet received, ignoring"); - LogError("Malformed BazaarSearch_Struct packet received, ignoring\n"); - } - - return; } void Client::Handle_OP_Begging(const EQApplicationPacket *app) @@ -4973,8 +4982,8 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { if (cy != m_Position.y || cx != m_Position.x) { // End trader mode if we move - if (Trader) { - Trader_EndTrader(); + if (IsTrader()) { + TraderEndTrader(); } /* Break Hide if moving without sneaking and set rewind timer if moved */ @@ -15494,174 +15503,49 @@ void Client::Handle_OP_Trader(const EQApplicationPacket *app) // // SoF sends 1 or more unhandled OP_Trader packets of size 96 when a trade has completed. // I don't know what they are for (yet), but it doesn't seem to matter that we ignore them. + auto action = *(uint32 *)app->pBuffer; - - uint32 max_items = 80; - - /* - if (GetClientVersion() >= EQClientRoF) - max_items = 200; - */ - - //Show Items - if (app->size == sizeof(Trader_ShowItems_Struct)) - { - Trader_ShowItems_Struct* sis = (Trader_ShowItems_Struct*)app->pBuffer; - - switch (sis->Code) - { - case BazaarTrader_EndTraderMode: { - Trader_EndTrader(); + switch (action) { + case TraderOff: { + TraderEndTrader(); LogTrading("End Trader Session"); break; } - case BazaarTrader_EndTransaction: { - - Client* c = entity_list.GetClientByID(sis->TraderID); - if (c) - { - c->WithCustomer(0); - LogTrading("End Transaction"); - } - else - LogTrading("Null Client Pointer"); - - break; - } - case BazaarTrader_ShowItems: { - Trader_ShowItems(); - LogTrading("Show Trader Items"); - break; - } - default: { - LogTrading("Unhandled action code in OP_Trader ShowItems_Struct"); - break; - } - } - } - else if (app->size == sizeof(ClickTrader_Struct)) - { - if (Buyer) { - Trader_EndTrader(); - Message(Chat::Red, "You cannot be a Trader and Buyer at the same time."); - return; - } - - ClickTrader_Struct* ints = (ClickTrader_Struct*)app->pBuffer; - - if (ints->Code == BazaarTrader_StartTraderMode) - { - GetItems_Struct* gis = GetTraderItems(); - - LogTrading("Start Trader Mode"); - // Verify there are no NODROP or items with a zero price - bool TradeItemsValid = true; - - for (uint32 i = 0; i < max_items; i++) { - - if (gis->Items[i] == 0) break; - - if (ints->ItemCost[i] == 0) { - Message(Chat::Red, "Item in Trader Satchel with no price. Unable to start trader mode"); - TradeItemsValid = false; - break; - } - const EQ::ItemData *Item = database.GetItem(gis->Items[i]); - - if (!Item) { - Message(Chat::Red, "Unexpected error. Unable to start trader mode"); - TradeItemsValid = false; - break; - } - - if (Item->NoDrop == 0) { - Message(Chat::Red, "NODROP Item in Trader Satchel. Unable to start trader mode"); - TradeItemsValid = false; - break; - } - } - - if (!TradeItemsValid) { - Trader_EndTrader(); - safe_delete(gis); + case TraderOn: { + if (Buyer) { + TraderEndTrader(); + Message(Chat::Red, "You cannot be a Trader and Buyer at the same time."); return; } - for (uint32 i = 0; i < max_items; i++) { - if (database.GetItem(gis->Items[i])) { - database.SaveTraderItem(CharacterID(), gis->Items[i], gis->SerialNumber[i], - gis->Charges[i], ints->ItemCost[i], i); - - auto inst = FindTraderItemBySerialNumber(gis->SerialNumber[i]); - if (inst) - inst->SetPrice(ints->ItemCost[i]); - } - else { - //return; //sony doesnt memset so assume done on first bad item - break; - } - - } - safe_delete(gis); - - Trader_StartTrader(); - - // This refreshes the Trader window to display the End Trader button - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - auto outapp = new EQApplicationPacket(OP_Trader, sizeof(TraderStatus_Struct)); - TraderStatus_Struct* tss = (TraderStatus_Struct*)outapp->pBuffer; - tss->Code = BazaarTrader_StartTraderMode2; - QueuePacket(outapp); - safe_delete(outapp); - } - } - else { - LogTrading("Unknown TraderStruct code of: [{}]\n", - ints->Code); - - LogError("Unknown TraderStruct code of: [{}]\n", ints->Code); - } - } - else if (app->size == sizeof(TraderStatus_Struct)) - { - TraderStatus_Struct* tss = (TraderStatus_Struct*)app->pBuffer; - - LogTrading("Trader Status Code: [{}]", tss->Code); - - switch (tss->Code) - { - case BazaarTrader_EndTraderMode: { - Trader_EndTrader(); - LogTrading("End Trader Session"); + TraderStartTrader(app); break; } - case BazaarTrader_ShowItems: { - Trader_ShowItems(); + case PriceUpdate: + case ItemMove: { + LogTrading("Trader Price Update"); + TraderPriceUpdate(app); + break; + } + case EndTransaction: { + auto sis = (Trader_ShowItems_Struct *) app->pBuffer; + Client *c = entity_list.GetClientByID(sis->entity_id); + if (c) { + c->WithCustomer(0); + LogTrading("End Transaction"); + } + + break; + } + case ListTraderItems: { + TraderShowItems(); LogTrading("Show Trader Items"); break; } default: { - LogTrading("Unhandled action code in OP_Trader ShowItems_Struct"); - break; + LogError("Unknown size for OP_Trader: [{}]\n", app->size); } - } - - } - else if (app->size == sizeof(TraderPriceUpdate_Struct)) - { - LogTrading("Trader Price Update"); - HandleTraderPriceUpdate(app); - } - else { - LogTrading("Unknown size for OP_Trader: [{}]\n", app->size); - LogError("Unknown size for OP_Trader: [{}]\n", app->size); - DumpPacket(app); - return; - } - - return; } void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) @@ -15670,23 +15554,80 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) // // Client has elected to buy an item from a Trader // - if (app->size != sizeof(TraderBuy_Struct)) { - LogError("Wrong size: OP_TraderBuy, size=[{}], expected [{}]", app->size, sizeof(TraderBuy_Struct)); - return; + auto in = (TraderBuy_Struct *) app->pBuffer; + auto trader = entity_list.GetClientByID(in->trader_id); + + switch (in->method) { + case ByVendor: { + if (trader) { + LogTrading("Buy item directly from vendor id [{}] item_id [{}] quantity [{}] " + "serial_number [{}]", + in->trader_id, + in->item_id, + in->quantity, + in->serial_number + ); + BuyTraderItem(in, trader, app); + } + break; + } + case ByParcel: { + if (!RuleB(Parcel, EnableParcelMerchants) || !RuleB(Bazaar, EnableParcelDelivery)) { + LogTrading( + "Bazaar purchase attempt by parcel delivery though 'Parcel:EnableParcelMerchants' or " + "'Bazaar::EnableParcelDelivery' not enabled." + ); + Message( + Chat::Yellow, + "The bazaar parcel delivey system is not enabled on this server. Please visit the vendor directly in the Bazaar." + ); + in->method = ByParcel; + in->sub_action = Failed; + TradeRequestFailed(app); + return; + } + LogTrading("Buy item by parcel delivery [{}] item_id [{}] quantity [{}] " + "serial_number [{}]", + in->trader_id, + in->item_id, + in->quantity, + in->serial_number + ); + BuyTraderItemOutsideBazaar(in, app); + break; + } + case ByDirectToInventory: { + if (!RuleB(Parcel, EnableDirectToInventoryDelivery)) { + LogTrading("Bazaar purchase attempt by direct inventory delivery though " + "'Parcel:EnableDirectToInventoryDelivery' not enabled." + ); + Message( + Chat::Yellow, + "Direct inventory delivey is not enabled on this server. Please visit the vendor directly." + ); + in->method = ByDirectToInventory; + in->sub_action = Failed; + TradeRequestFailed(app); + return; + } + trader = entity_list.GetClientByCharID(in->trader_id); + LogTrading("Buy item by direct inventory delivery [{}] item_id [{}] quantity [{}] " + "serial_number [{}]", + in->trader_id, + in->item_id, + in->quantity, + in->serial_number + ); + Message( + Chat::Yellow, + "Direct inventory delivey is not yet implemented. Please visit the vendor directly or purchase via parcel delivery." + ); + in->method = ByDirectToInventory; + in->sub_action = Failed; + TradeRequestFailed(app); + break; + } } - - TraderBuy_Struct* tbs = (TraderBuy_Struct*)app->pBuffer; - - if (Client* Trader = entity_list.GetClientByID(tbs->TraderID)) { - BuyTraderItem(tbs, Trader, app); - LogTrading("Client::Handle_OP_TraderBuy: Buy Trader Item "); - } - else { - LogTrading("Client::Handle_OP_TraderBuy: Null Client Pointer"); - } - - - return; } void Client::Handle_OP_TradeRequest(const EQApplicationPacket *app) @@ -15749,126 +15690,76 @@ void Client::Handle_OP_TradeRequestAck(const EQApplicationPacket *app) void Client::Handle_OP_TraderShop(const EQApplicationPacket *app) { - // Bazaar Trader: + auto in = (TraderClick_Struct *) app->pBuffer; + LogTrading("Handle_OP_TraderShop: TraderClick_Struct TraderID [{}], Code [{}], Unknown008 [{}], Approval [{}]", + in->TraderID, + in->Code, + in->Unknown008, + in->Approval + ); - if (app->size == sizeof(TraderClick_Struct)) - { + switch (in->Code) { + case ClickTrader: { + LogTrading("Handle_OP_TraderShop case ClickTrader [{}]", in->Code); + auto outapp = std::make_unique(OP_TraderShop, sizeof(TraderClick_Struct)); + auto data = (TraderClick_Struct *) outapp->pBuffer; + auto trader_client = entity_list.GetClientByID(in->TraderID); - TraderClick_Struct* tcs = (TraderClick_Struct*)app->pBuffer; - - LogTrading("Handle_OP_TraderShop: TraderClick_Struct TraderID [{}], Code [{}], Unknown008 [{}], Approval [{}]", - tcs->TraderID, tcs->Code, tcs->Unknown008, tcs->Approval); - - if (tcs->Code == BazaarWelcome) - { - LogTrading("Client::Handle_OP_TraderShop: Sent Bazaar Welcome Info"); - SendBazaarWelcome(); - } - else - { - // This is when a potential purchaser right clicks on this client who is in Trader mode to - // browse their goods. - auto outapp = new EQApplicationPacket(OP_TraderShop, sizeof(TraderClick_Struct)); - - TraderClick_Struct* outtcs = (TraderClick_Struct*)outapp->pBuffer; - - Client* Trader = entity_list.GetClientByID(tcs->TraderID); - - if (Trader) - { - outtcs->Approval = Trader->WithCustomer(GetID()); - LogTrading("Client::Handle_OP_TraderShop: Shop Request ([{}]) to ([{}]) with Approval: [{}]", GetCleanName(), Trader->GetCleanName(), outtcs->Approval); + if (trader_client) { + data->Approval = trader_client->WithCustomer(GetID()); + LogTrading("Client::Handle_OP_TraderShop: Shop Request ([{}]) to ([{}]) with Approval: [{}]", + GetCleanName(), + trader_client->GetCleanName(), + data->Approval + ); } else { LogTrading("Client::Handle_OP_TraderShop: entity_list.GetClientByID(tcs->traderid)" - " returned a nullptr pointer"); - safe_delete(outapp); + " returned a nullptr pointer" + ); return; } - outtcs->TraderID = tcs->TraderID; + data->Code = ClickTrader; + data->TraderID = in->TraderID; + data->Unknown008 = 0x3f800000; + QueuePacket(outapp.get()); - outtcs->Unknown008 = 0x3f800000; - - QueuePacket(outapp); - - - if (outtcs->Approval) { - BulkSendTraderInventory(Trader->CharacterID()); - Trader->Trader_CustomerBrowsing(this); - TraderID = tcs->TraderID; - LogTrading("Client::Handle_OP_TraderShop: Trader Inventory Sent"); + if (data->Approval) { + BulkSendTraderInventory(trader_client->CharacterID()); + trader_client->Trader_CustomerBrowsing(this); + SetTraderID(in->TraderID); + LogTrading("Client::Handle_OP_TraderShop: Trader Inventory Sent to [{}] from [{}]", + GetID(), + in->TraderID + ); } - else - { + else { MessageString(Chat::Yellow, TRADER_BUSY); LogTrading("Client::Handle_OP_TraderShop: Trader Busy"); } - safe_delete(outapp); - return; + break; } - - } - else if (app->size == sizeof(BazaarWelcome_Struct)) - { - // RoF+ - // Client requested Bazaar Welcome Info (Trader and Item Total Counts) - SendBazaarWelcome(); - LogTrading("Client::Handle_OP_TraderShop: Sent Bazaar Welcome Info"); - } - else if (app->size == sizeof(TraderBuy_Struct)) - { - // RoF+ - // Customer has purchased an item from the Trader - - TraderBuy_Struct* tbs = (TraderBuy_Struct*)app->pBuffer; - - if (Client* Trader = entity_list.GetClientByID(tbs->TraderID)) - { - BuyTraderItem(tbs, Trader, app); - LogTrading("Handle_OP_TraderShop: Buy Action [{}], Price [{}], Trader [{}], ItemID [{}], Quantity [{}], ItemName, [{}]", - tbs->Action, tbs->Price, tbs->TraderID, tbs->ItemID, tbs->Quantity, tbs->ItemName); - } - else - { - LogTrading("OP_TraderShop: Null Client Pointer"); - } - } - else if (app->size == 4) - { - // RoF+ - // Customer has closed the trade window - uint32 Command = *((uint32 *)app->pBuffer); - - if (Command == 4) - { - Client* c = entity_list.GetClientByID(TraderID); - TraderID = 0; - if (c) - { + case EndTransaction: { + Client *c = entity_list.GetClientByID(GetTraderID()); + SetTraderID(0); + if (c) { c->WithCustomer(0); - LogTrading("End Transaction - Code [{}]", Command); + LogTrading("End Transaction - Code [{}]", in->Code); } - else - { - LogTrading("Null Client Pointer for Trader - Code [{}]", Command); + else { + LogTrading("Null Client Pointer for Trader - Code [{}]", in->Code); } - EQApplicationPacket empty(OP_ShopEndConfirm); - QueuePacket(&empty); + + auto outapp = new EQApplicationPacket(OP_ShopEndConfirm); + QueuePacket(outapp); + safe_delete(outapp); + break; } - else - { - LogTrading("Unhandled Code [{}]", Command); + default: { } } - else - { - LogTrading("Unknown size for OP_TraderShop: [{}]\n", app->size); - LogError("Unknown size for OP_TraderShop: [{}]\n", app->size); - DumpPacket(app); - return; - } } void Client::Handle_OP_TradeSkillCombine(const EQApplicationPacket *app) diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 782932e2e..17e2abafa 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -994,22 +994,22 @@ void Client::BulkSendMerchantInventory(int merchant_id, int npcid) { uint8 Client::WithCustomer(uint16 NewCustomer){ if(NewCustomer == 0) { - CustomerID = 0; + SetCustomerID(0); return 0; } - if(CustomerID == 0) { - CustomerID = NewCustomer; + if(GetCustomerID() == 0) { + SetCustomerID(NewCustomer); return 1; } // Check that the player browsing our wares hasn't gone away. - Client* c = entity_list.GetClientByID(CustomerID); + Client* c = entity_list.GetClientByID(GetCustomerID()); if(!c) { LogTrading("Previous customer has gone away"); - CustomerID = NewCustomer; + SetCustomerID(NewCustomer); return 1; } diff --git a/zone/gm_commands/parcels.cpp b/zone/gm_commands/parcels.cpp index 770b9ff00..686d2a95a 100644 --- a/zone/gm_commands/parcels.cpp +++ b/zone/gm_commands/parcels.cpp @@ -41,7 +41,7 @@ void command_parcels(Client *c, const Seperator *sep) return; } - auto results = CharacterParcelsRepository::GetWhere( + auto results = CharacterParcelsRepository::GetWhere( database, fmt::format("char_id = '{}' ORDER BY slot_id ASC", player_id.at(0).char_id) ); @@ -120,8 +120,8 @@ void command_parcels(Client *c, const Seperator *sep) auto note = std::string(sep->argplus[5]); auto send_to_client = CharacterParcelsRepository::GetParcelCountAndCharacterName( - database, - to_name + database, + to_name ); if (send_to_client.at(0).character_name.empty()) { c->MessageString(Chat::Yellow, CANT_FIND_PLAYER, to_name.c_str()); @@ -164,14 +164,14 @@ void command_parcels(Client *c, const Seperator *sep) } CharacterParcelsRepository::CharacterParcels parcel_out; - parcel_out.from_name = c->GetName(); - parcel_out.note = note; - parcel_out.sent_date = time(nullptr); - parcel_out.quantity = quantity == 0 ? 1 : quantity; - parcel_out.item_id = PARCEL_MONEY_ITEM_ID; - parcel_out.char_id = send_to_client.at(0).char_id; - parcel_out.slot_id = next_slot; - parcel_out.id = 0; + parcel_out.from_name = c->GetName(); + parcel_out.note = note; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = quantity == 0 ? 1 : quantity; + parcel_out.item_id = PARCEL_MONEY_ITEM_ID; + parcel_out.char_id = send_to_client.at(0).char_id; + parcel_out.slot_id = next_slot; + parcel_out.id = 0; auto result = CharacterParcelsRepository::InsertOne(database, parcel_out); if (!result.id) { @@ -205,7 +205,7 @@ void command_parcels(Client *c, const Seperator *sep) e.quantity = parcel_out.quantity; e.sent_date = parcel_out.sent_date; - RecordPlayerEventLogWithClient(c, PlayerEvent::PARCEL_SEND, e); + RecordPlayerEventLogWithClient (c, PlayerEvent::PARCEL_SEND, e); } Parcel_Struct ps{}; @@ -242,14 +242,14 @@ void command_parcels(Client *c, const Seperator *sep) } CharacterParcelsRepository::CharacterParcels parcel_out; - parcel_out.from_name = c->GetName(); - parcel_out.note = note.empty() ? "" : note; - parcel_out.sent_date = time(nullptr); - parcel_out.quantity = quantity; - parcel_out.item_id = item_id; - parcel_out.char_id = send_to_client.at(0).char_id; - parcel_out.slot_id = next_slot; - parcel_out.id = 0; + parcel_out.from_name = c->GetName(); + parcel_out.note = note.empty() ? "" : note; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = quantity; + parcel_out.item_id = item_id; + parcel_out.char_id = send_to_client.at(0).char_id; + parcel_out.slot_id = next_slot; + parcel_out.id = 0; auto result = CharacterParcelsRepository::InsertOne(database, parcel_out); if (!result.id) { @@ -283,7 +283,7 @@ void command_parcels(Client *c, const Seperator *sep) e.quantity = parcel_out.quantity; e.sent_date = parcel_out.sent_date; - RecordPlayerEventLogWithClient(c, PlayerEvent::PARCEL_SEND, e); + RecordPlayerEventLogWithClient (c, PlayerEvent::PARCEL_SEND, e); } Parcel_Struct ps{}; @@ -300,8 +300,8 @@ void SendParcelsSubCommands(Client *c) c->Message(Chat::White, "#parcels listdb [Character Name]"); c->Message(Chat::White, "#parcels listmemory [Character Name] (Must be in the same zone)"); c->Message( - Chat::White, - "#parcels add [Character Name] [item id] [quantity] [note]. To send money use item id of 99990. Quantity is valid for stackable items, charges on an item, or amount of copper." + Chat::White, + "#parcels add [Character Name] [item id] [quantity] [note]. To send money use item id of 99990. Quantity is valid for stackable items, charges on an item, or amount of copper." ); c->Message(Chat::White, "#parcels details [Character Name]"); } diff --git a/zone/gm_commands/show/inventory.cpp b/zone/gm_commands/show/inventory.cpp index cbb5a03fc..affcd4750 100644 --- a/zone/gm_commands/show/inventory.cpp +++ b/zone/gm_commands/show/inventory.cpp @@ -164,10 +164,11 @@ void ShowInventory(Client *c, const Seperator *sep) c->Message( Chat::White, fmt::format( - "Slot {} | {} ({}){}", + "Slot {} | {} ({}/{}){}", ((scope_bit & peekWorld) ? (EQ::invslot::WORLD_BEGIN + index_main) : index_main), linker.GenerateLink(), item_data->ID, + c->GetInv().GetItem(((scope_bit &peekWorld) ? (EQ::invslot::WORLD_BEGIN + index_main) : index_main))->GetSerialNumber(), ( inst_main->IsStackable() && inst_main->GetCharges() > 0 ? fmt::format( @@ -228,7 +229,7 @@ void ShowInventory(Client *c, const Seperator *sep) c->Message( Chat::White, fmt::format( - "Slot {} Bag Slot {} | {} ({}){}", + "Slot {} Bag Slot {}/{} | {} ({}/{}){}", ( (scope_bit & peekWorld) ? INVALID_INDEX : @@ -238,6 +239,7 @@ void ShowInventory(Client *c, const Seperator *sep) sub_index, linker.GenerateLink(), item_data->ID, + c->GetInv().GetItem(EQ::InventoryProfile::CalcSlotId(index_main, sub_index))->GetSerialNumber(), ( inst_sub->IsStackable() && inst_sub->GetCharges() > 0 ? fmt::format( diff --git a/zone/inventory.cpp b/zone/inventory.cpp index b1377b3ac..374fa3aa7 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -1864,19 +1864,19 @@ bool Client::SwapItem(MoveItem_Struct* move_in) { LogInventory("Dest slot [{}] has item [{}] ([{}]) with [{}] charges in it", dst_slot_id, dst_inst->GetItem()->Name, dst_inst->GetItem()->ID, dst_inst->GetCharges()); dstitemid = dst_inst->GetItem()->ID; } - if (Trader && srcitemid>0){ + if (IsTrader() && srcitemid>0){ EQ::ItemInstance* srcbag; EQ::ItemInstance* dstbag; uint32 srcbagid =0; uint32 dstbagid = 0; if (src_slot_id >= EQ::invbag::GENERAL_BAGS_BEGIN && src_slot_id <= EQ::invbag::GENERAL_BAGS_END) { - srcbag = m_inv.GetItem(((int)(src_slot_id / 10)) - 3); + srcbag = m_inv.GetItem(EQ::InventoryProfile::CalcSlotId(src_slot_id)); if (srcbag) srcbagid = srcbag->GetItem()->ID; } if (dst_slot_id >= EQ::invbag::GENERAL_BAGS_BEGIN && dst_slot_id <= EQ::invbag::GENERAL_BAGS_END) { - dstbag = m_inv.GetItem(((int)(dst_slot_id / 10)) - 3); + dstbag = m_inv.GetItem(EQ::InventoryProfile::CalcSlotId(dst_slot_id)); if (dstbag) dstbagid = dstbag->GetItem()->ID; } @@ -1884,7 +1884,7 @@ bool Client::SwapItem(MoveItem_Struct* move_in) { (dstbagid && dstbag->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) || (srcitemid && src_inst && src_inst->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) || (dstitemid && dst_inst && dst_inst->GetItem()->BagType == EQ::item::BagTypeTradersSatchel)) { - Trader_EndTrader(); + TraderEndTrader(); Message(Chat::Red,"You cannot move your Trader Satchels, or items inside them, while Trading."); } } @@ -2180,6 +2180,27 @@ bool Client::SwapItem(MoveItem_Struct* move_in) { } LogInventory("Moving entire item from slot [{}] to slot [{}]", src_slot_id, dst_slot_id); + if (src_inst->IsStackable() && + dst_slot_id >= EQ::invbag::GENERAL_BAGS_BEGIN && + dst_slot_id <= EQ::invbag::GENERAL_BAGS_END + ) { + EQ::ItemInstance *bag = nullptr; + bag = m_inv.GetItem(EQ::InventoryProfile::CalcSlotId(dst_slot_id)); + if (bag) { + if (bag->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) { + PutItemInInventory(dst_slot_id, *src_inst, true); + //This resets the UF client to recognize the new serial item of the placed item + //if it came from a stack without having to close the trader window and re-open. + //It is not required for the RoF2 client. + if (ClientVersion() < EQ::versions::ClientVersion::RoF2) { + auto outapp = new EQApplicationPacket(OP_Trader, sizeof(TraderBuy_Struct)); + auto data = (TraderBuy_Struct *) outapp->pBuffer; + data->action = BazaarBuyItem; + FastQueuePacket(&outapp); + } + } + } + } if (src_slot_id <= EQ::invslot::EQUIPMENT_END) { if(src_inst) { diff --git a/zone/parcels.cpp b/zone/parcels.cpp index 5d69e0e59..259e495ea 100644 --- a/zone/parcels.cpp +++ b/zone/parcels.cpp @@ -107,7 +107,7 @@ void Client::SendBulkParcels() } } -void Client::SendParcel(const Parcel_Struct &parcel_in) +void Client::SendParcel(Parcel_Struct &parcel_in) { auto results = CharacterParcelsRepository::GetWhere( database, diff --git a/zone/string_ids.h b/zone/string_ids.h index 9b64ecbc1..c4aed6fdb 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -195,6 +195,7 @@ #define PARCEL_DELAY 734 //%1 tells you, 'You must give me a chance to send the last parcel before I can send another!' #define PARCEL_DUPLICATE_DELETE 737 //Duplicate lore items are not allowed! Your duplicate %1 has been deleted! #define PARCEL_DELIVER_3 741 //%1 told you, 'I will deliver the stack of %2 %3 to %4 as soon as possible!' +#define TRADER_MODE_FAILED_ROF2 785 //Your attempt to become a trader has failed. #define PARCEL_INV_FULL 790 //%1 tells you, 'Your inventory appears full! Unable to retrieve parceled item.' #define AA_CAP 1000 //You have reached the AA point cap, and cannot gain any further experience until some of your stored AA point pool is used. #define GM_GAINXP 1002 //[GM] You have gained %1 AXP and %2 EXP (%3). @@ -427,6 +428,9 @@ #define GENERIC_STRING 6688 //%1 (used to any basic message) #define SENTINEL_TRIG_YOU 6724 //You have triggered your sentinel. #define SENTINEL_TRIG_OTHER 6725 //%1 has triggered your sentinel. +#define TRADER_MODE_OFF 6741 //Bazaar Trader Mode *OFF* +#define TRADER_MODE_ON 6742 //Bazaar Trader Mode *ON* +#define TRADER_SET_PRICE 6754 //To become a merchant you must assign a price to an item in your list. Do this by selecting an item, then selecting a money amount, and then clicking set price. #define IDENTIFY_SPELL 6765 //Item Lore: %1. #define PET_NOW_HOLDING 6834 //Now holding, Master. I will not start attacks until ordered. #define PET_ON_GHOLD 6843 //Pet greater hold has been set to on. diff --git a/zone/trading.cpp b/zone/trading.cpp index 697c9556b..5ff292823 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -20,8 +20,10 @@ #include "../common/eqemu_logsys.h" #include "../common/rulesys.h" #include "../common/strings.h" +#include "../common/eq_packet_structs.h" #include "../common/misc_functions.h" #include "../common/events/player_event_logs.h" +#include "../common/repositories/trader_repository.h" #include "client.h" #include "entity.h" @@ -30,6 +32,7 @@ #include "quest_parser_collection.h" #include "string_ids.h" #include "worldserver.h" +#include "../common/bazaar.h" class QueryServ; @@ -980,21 +983,26 @@ bool Client::CheckTradeNonDroppable() return false; } -void Client::Trader_ShowItems(){ +void Client::TraderShowItems() +{ auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_Struct)); + auto data = (Trader_Struct *) outapp->pBuffer; - Trader_Struct* outints = (Trader_Struct*)outapp->pBuffer; - Trader_Struct* TraderItems = database.LoadTraderItem(CharacterID()); + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ? + GetInv().GetLookup()->InventoryTypeSize.Bazaar : + trader_items.size(); - for(int i = 0; i < 80; i++){ - outints->ItemCost[i] = TraderItems->ItemCost[i]; - outints->Items[i] = TraderItems->Items[i]; + for (int i = 0; i < item_limit; i++) { + data->item_cost[i] = trader_items.at(i).item_cost; + data->items[i] = ClientVersion() == EQ::versions::ClientVersion::RoF2 ? trader_items.at(i).item_sn + : trader_items.at(i).item_id; } - outints->Code = BazaarTrader_ShowItems; + + data->action = ListTraderItems; QueuePacket(outapp); safe_delete(outapp); - safe_delete(TraderItems); } void Client::SendTraderPacket(Client* Trader, uint32 Unknown72) @@ -1006,13 +1014,11 @@ void Client::SendTraderPacket(Client* Trader, uint32 Unknown72) BecomeTrader_Struct* bts = (BecomeTrader_Struct*)outapp->pBuffer; - bts->Code = BazaarTrader_StartTraderMode; + bts->action = BazaarTrader_StartTraderMode; - bts->ID = Trader->GetID(); - - strn0cpy(bts->Name, Trader->GetName(), sizeof(bts->Name)); - - bts->Unknown072 = Unknown72; + bts->trader_id = Trader->CharacterID(); + bts->entity_id = Trader->GetID(); + strn0cpy(bts->trader_name, Trader->GetName(), sizeof(bts->trader_name)); QueuePacket(outapp); @@ -1020,132 +1026,137 @@ void Client::SendTraderPacket(Client* Trader, uint32 Unknown72) safe_delete(outapp); } -void Client::Trader_CustomerBrowsing(Client *Customer) { +void Client::Trader_CustomerBrowsing(Client *Customer) +{ auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct)); + auto sis = (Trader_ShowItems_Struct *) outapp->pBuffer; - Trader_ShowItems_Struct* sis = (Trader_ShowItems_Struct*)outapp->pBuffer; - - sis->Code = BazaarTrader_CustomerBrowsing; - - sis->TraderID = Customer->GetID(); + sis->action = CustomerBrowsing; + sis->entity_id = Customer->GetID(); QueuePacket(outapp); safe_delete(outapp); } +void Client::TraderStartTrader(const EQApplicationPacket *app) +{ + uint32 max_items = GetInv().GetLookup()->InventoryTypeSize.Bazaar; + auto in = (ClickTrader_Struct *) app->pBuffer; + auto inv = GetTraderItems(); + bool trade_items_valid = true; + std::vector trader_items{}; -void Client::Trader_StartTrader() { - - Trader=true; - - auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct)); - - Trader_ShowItems_Struct* sis = (Trader_ShowItems_Struct*)outapp->pBuffer; - - sis->Code = BazaarTrader_StartTraderMode; - - sis->TraderID = GetID(); - - QueuePacket(outapp); - - - safe_delete(outapp); - - // Notify other clients we are now in trader mode - - outapp= new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct)); - - BecomeTrader_Struct* bts = (BecomeTrader_Struct*)outapp->pBuffer; - - bts->Code = 1; - - bts->ID = GetID(); - - strn0cpy(bts->Name, GetName(), sizeof(bts->Name)); - - entity_list.QueueClients(this, outapp, false); - - - safe_delete(outapp); -} - -void Client::Trader_EndTrader() { - - // If someone is looking at our wares, remove all the items from the window. - // - if(CustomerID) { - Client* Customer = entity_list.GetClientByID(CustomerID); - GetItems_Struct* gis=GetTraderItems(); - - if(Customer && gis) { - auto outapp = new EQApplicationPacket(OP_TraderDelItem, sizeof(TraderDelItem_Struct)); - TraderDelItem_Struct* tdis = (TraderDelItem_Struct*)outapp->pBuffer; - - tdis->Unknown000 = 0; - tdis->TraderID = Customer->GetID(); - tdis->Unknown012 = 0; - Customer->Message(Chat::Red, "The Trader is no longer open for business"); - - for(int i = 0; i < 80; i++) { - if(gis->Items[i] != 0) { - - if (Customer->ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - // RoF+ use Item IDs for now - tdis->ItemID = gis->Items[i]; - } - else - { - tdis->ItemID = gis->SerialNumber[i]; - } - - Customer->QueuePacket(outapp); - } + //Check inventory for no-trade items + for (auto const &i: inv->serial_number) { + auto inst = FindTraderItemBySerialNumber(i); + if (inst) { + if (inst->GetItem() && inst->GetItem()->NoDrop == 0) { + Message( + Chat::Red, + fmt::format( + "Item: {} is NODROP and found in a Trader's Satchel. Please remove and restart trader mode", + inst->GetItem()->Name + ).c_str() + ); + TraderEndTrader(); + safe_delete(inv); + return; } - - safe_delete(outapp); } - safe_delete(gis); } - database.DeleteTraderItem(CharacterID()); + for (uint32 i = 0; i < max_items; i++) { + auto inst = FindTraderItemBySerialNumber(inv->serial_number[i]); + auto it = std::find(std::begin(in->serial_number), std::end(in->serial_number), inv->serial_number[i]); + if (inst && it != std::end(in->serial_number)) { + inst->SetPrice(in->item_cost[i]); + TraderRepository::Trader trader_item{}; - // Notify other clients we are no longer in trader mode. - // - auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct)); + trader_item.id = 0; + trader_item.char_entity_id = GetID(); + trader_item.char_id = CharacterID(); + trader_item.char_zone_id = GetZoneID(); + trader_item.item_charges = inst->GetCharges() == 0 ? 1 : inst->GetCharges(); + trader_item.item_cost = inst->GetPrice(); + trader_item.item_id = inst->GetID(); + trader_item.item_sn = in->serial_number[i]; + trader_item.slot_id = i; + if (inst->IsAugmented()) { + auto augs = inst->GetAugmentIDs(); + trader_item.aug_slot_1 = augs.at(0); + trader_item.aug_slot_2 = augs.at(1); + trader_item.aug_slot_3 = augs.at(2); + trader_item.aug_slot_4 = augs.at(3); + trader_item.aug_slot_5 = augs.at(4); + trader_item.aug_slot_6 = augs.at(5); + } - BecomeTrader_Struct* bts = (BecomeTrader_Struct*)outapp->pBuffer; + trader_items.emplace_back(trader_item); + continue; + } + else if (inst) { + Message( + Chat::Red, + fmt::format( + "Item: {} has no price set. Please set a price and try again.", + inst->GetItem()->Name + ).c_str() + ); + trade_items_valid = false; + continue; + } + else if (!in->serial_number[i]) { + break; + } + } - bts->Code = 0; + if (!trade_items_valid) { + Message(Chat::Red, "You are not able to become a trader at this time."); + TraderEndTrader(); + safe_delete(inv); + return; + } - bts->ID = GetID(); + TraderRepository::ReplaceMany(database, trader_items); + safe_delete(inv); - strn0cpy(bts->Name, GetName(), sizeof(bts->Name)); + // This refreshes the Trader window to display the End Trader button + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + auto outapp = new EQApplicationPacket(OP_Trader, sizeof(TraderStatus_Struct)); + auto data = (TraderStatus_Struct *) outapp->pBuffer; + data->Code = TraderAck2; + QueuePacket(outapp); + safe_delete(outapp); + } - entity_list.QueueClients(this, outapp, false); - - - safe_delete(outapp); - - outapp= new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct)); - - Trader_ShowItems_Struct* sis = (Trader_ShowItems_Struct*)outapp->pBuffer; - - sis->Code = BazaarTrader_EndTraderMode; - - sis->TraderID = BazaarTrader_EndTraderMode; - - QueuePacket(outapp); - - safe_delete(outapp); - - WithCustomer(0); - - Trader = false; + MessageString(Chat::Yellow, TRADER_MODE_ON); + SetTrader(true); + SendTraderMode(TraderOn); + SendBecomeTraderToWorld(this, TraderOn); + LogTrading("Trader Mode ON for Player [{}] with client version {}.", GetCleanName(), (uint32) ClientVersion()); } -void Client::SendTraderItem(uint32 ItemID, uint16 Quantity) { +void Client::TraderEndTrader() +{ + if (IsThereACustomer()) { + auto customer = entity_list.GetClientByID(GetCustomerID()); + if (customer) { + auto end_session = new EQApplicationPacket(OP_ShopEnd); + customer->FastQueuePacket(&end_session); + } + } + + TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + + SendBecomeTraderToWorld(this, TraderOff); + SendTraderMode(TraderOff); + + WithCustomer(0); + SetTrader(false); +} + +void Client::SendTraderItem(uint32 ItemID, uint16 Quantity, TraderRepository::Trader &t) { std::string Packet; int16 FreeSlotID=0; @@ -1157,65 +1168,91 @@ void Client::SendTraderItem(uint32 ItemID, uint16 Quantity) { return; } - EQ::ItemInstance* inst = database.CreateItem(item, Quantity); + std::unique_ptr inst( + database.CreateItem( + item, + Quantity, + t.aug_slot_1, + t.aug_slot_2, + t.aug_slot_3, + t.aug_slot_4, + t.aug_slot_5, + t.aug_slot_6 + ) + ); if (inst) { bool is_arrow = (inst->GetItem()->ItemType == EQ::item::ItemTypeArrow) ? true : false; FreeSlotID = m_inv.FindFreeSlot(false, true, inst->GetItem()->Size, is_arrow); - PutItemInInventory(FreeSlotID, *inst); + if (TryStacking(inst.get(), ItemPacketTrade, true, false)) { + } + else { + PutItemInInventory(FreeSlotID, *inst); + SendItemPacket(FreeSlotID, inst.get(), ItemPacketTrade); + } Save(); - - SendItemPacket(FreeSlotID, inst, ItemPacketTrade); - - safe_delete(inst); } } -void Client::SendSingleTraderItem(uint32 CharID, int SerialNumber) { - - EQ::ItemInstance* inst= database.LoadSingleTraderItem(CharID, SerialNumber); - if(inst) { - SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketMerchant); // MainCursor? - safe_delete(inst); +void Client::SendSingleTraderItem(uint32 char_id, int serial_number) +{ + auto inst = database.LoadSingleTraderItem(char_id, serial_number); + if (inst) { + SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor? } - } -void Client::BulkSendTraderInventory(uint32 char_id) { +void Client::BulkSendTraderInventory(uint32 char_id) +{ const EQ::ItemData *item; - TraderCharges_Struct* TraderItems = database.LoadTraderItemWithCharges(char_id); + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", char_id)); + uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ? + GetInv().GetLookup()->InventoryTypeSize.Bazaar : + trader_items.size(); - for (uint8 i = 0;i < 80; i++) { // need to transition away from 'magic number' - if((TraderItems->ItemID[i] == 0) || (TraderItems->ItemCost[i] <= 0)) { + for (uint32 i = 0; i < item_limit; i++) { + if ((trader_items.at(i).item_id == 0) || (trader_items.at(i).item_cost == 0)) { continue; } - else - item=database.GetItem(TraderItems->ItemID[i]); + else { + item = database.GetItem(trader_items.at(i).item_id); + } - if (item && (item->NoDrop!=0)) { - EQ::ItemInstance* inst = database.CreateItem(item); + if (item && (item->NoDrop != 0)) { + std::unique_ptr inst( + database.CreateItem( + trader_items.at(i).item_id, + trader_items.at(i).item_charges, + trader_items.at(i).aug_slot_1, + trader_items.at(i).aug_slot_2, + trader_items.at(i).aug_slot_3, + trader_items.at(i).aug_slot_4, + trader_items.at(i).aug_slot_5, + trader_items.at(i).aug_slot_6 + ) + ); if (inst) { - inst->SetSerialNumber(TraderItems->SerialNumber[i]); - if(TraderItems->Charges[i] > 0) - inst->SetCharges(TraderItems->Charges[i]); - - if(inst->IsStackable()) { - inst->SetMerchantCount(TraderItems->Charges[i]); - inst->SetMerchantSlot(TraderItems->SerialNumber[i]); + inst->SetSerialNumber(trader_items.at(i).item_sn); + if (trader_items.at(i).item_charges > 0) { + inst->SetCharges(trader_items.at(i).item_charges); } - inst->SetPrice(TraderItems->ItemCost[i]); - SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketMerchant); // MainCursor? - safe_delete(inst); + if (inst->IsStackable()) { + inst->SetMerchantCount(trader_items.at(i).item_charges); + inst->SetMerchantSlot(trader_items.at(i).item_sn); + } + + inst->SetPrice(trader_items.at(i).item_cost); + SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); +// safe_delete(inst); } else LogTrading("Client::BulkSendTraderInventory nullptr inst pointer"); } } - safe_delete(TraderItems); } uint32 Client::FindTraderItemSerialNumber(int32 ItemID) { @@ -1241,58 +1278,58 @@ uint32 Client::FindTraderItemSerialNumber(int32 ItemID) { return 0; } -EQ::ItemInstance* Client::FindTraderItemBySerialNumber(int32 SerialNumber){ +EQ::ItemInstance *Client::FindTraderItemBySerialNumber(int32 SerialNumber) +{ + EQ::ItemInstance *item = nullptr; + int16 slot_id = 0; - EQ::ItemInstance* item = nullptr; - uint16 SlotID = 0; - for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++){ + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { item = GetInv().GetItem(i); - if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel){ - for (int x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) { + if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) { + for (int16 x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) { // we already have the parent bag and a contents iterator..why not just iterate the bag!?? - SlotID = EQ::InventoryProfile::CalcSlotId(i, x); - item = GetInv().GetItem(SlotID); - if(item) { - if(item->GetSerialNumber() == SerialNumber) + slot_id = EQ::InventoryProfile::CalcSlotId(i, x); + item = GetInv().GetItem(slot_id); + if (item) { + if (item->GetSerialNumber() == SerialNumber) { return item; + } } } } } - LogTrading("Client::FindTraderItemBySerialNumber Couldn't find item! Serial No. was [{}]", SerialNumber); + + LogTrading("Couldn't find item! Serial No. was [{}]", SerialNumber); return nullptr; } -GetItems_Struct* Client::GetTraderItems(){ +GetItems_Struct *Client::GetTraderItems() +{ + const EQ::ItemInstance *item = nullptr; + int16 slot_id = INVALID_INDEX; + auto gis = new GetItems_Struct; + uint8 ndx = 0; - const EQ::ItemInstance* item = nullptr; - uint16 SlotID = INVALID_INDEX; - - auto gis = new GetItems_Struct; - - memset(gis,0,sizeof(GetItems_Struct)); - - uint8 ndx = 0; - - for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { - if (ndx >= 80) + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + if (ndx >= GetInv().GetLookup()->InventoryTypeSize.Bazaar) { break; + } item = GetInv().GetItem(i); - if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel){ + if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) { for (int x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) { - if (ndx >= 80) + if (ndx >= GetInv().GetLookup()->InventoryTypeSize.Bazaar) { break; + } - SlotID = EQ::InventoryProfile::CalcSlotId(i, x); + slot_id = EQ::InventoryProfile::CalcSlotId(i, x); + item = GetInv().GetItem(slot_id); - item = GetInv().GetItem(SlotID); - - if(item){ - gis->Items[ndx] = item->GetItem()->ID; - gis->SerialNumber[ndx] = item->GetSerialNumber(); - gis->Charges[ndx] = item->GetCharges(); + if (item) { + gis->items[ndx] = item->GetItem()->ID; + gis->serial_number[ndx] = item->GetSerialNumber(); + gis->charges[ndx] = item->GetCharges() == 0 ? 1 : item->GetCharges(); ndx++; } } @@ -1327,202 +1364,181 @@ uint16 Client::FindTraderItem(int32 SerialNumber, uint16 Quantity){ return 0; } -void Client::NukeTraderItem(uint16 Slot,int16 Charges,int16 Quantity,Client* Customer,uint16 TraderSlot, int32 SerialNumber, int32 itemid) { - - if(!Customer) +void Client::NukeTraderItem( + uint16 slot, + int16 charges, + int16 quantity, + Client *customer, + uint16 trader_slot, + int32 serial_number, + int32 item_id +) +{ + if (!customer) { return; - - LogTrading("NukeTraderItem(Slot [{}], Charges [{}], Quantity [{}]", Slot, Charges, Quantity); - - if(Quantity < Charges) - { - Customer->SendSingleTraderItem(CharacterID(), SerialNumber); - m_inv.DeleteItem(Slot, Quantity); } - else - { + + LogTrading("NukeTraderItem(Slot [{}] Charges [{}] Quantity [{}]", slot, charges, quantity); + if (quantity < charges) { + customer->SendSingleTraderItem(CharacterID(), serial_number); + m_inv.DeleteItem(slot, quantity); + } + else { auto outapp = new EQApplicationPacket(OP_TraderDelItem, sizeof(TraderDelItem_Struct)); - TraderDelItem_Struct* tdis = (TraderDelItem_Struct*)outapp->pBuffer; + auto tdis = (TraderDelItem_Struct *) outapp->pBuffer; - tdis->Unknown000 = 0; - tdis->TraderID = Customer->GetID(); - if (Customer->ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - // RoF+ use Item IDs for now - tdis->ItemID = itemid; - } - else - { - tdis->ItemID = SerialNumber; - } - tdis->Unknown012 = 0; - - - Customer->QueuePacket(outapp); + tdis->unknown_000 = 0; + tdis->trader_id = customer->GetID(); + tdis->item_id = serial_number; + tdis->unknown_012 = 0; + customer->QueuePacket(outapp); safe_delete(outapp); - m_inv.DeleteItem(Slot); + m_inv.DeleteItem(slot); } // This updates the trader. Removes it from his trading bags. // - const EQ::ItemInstance* Inst = m_inv[Slot]; + const EQ::ItemInstance *Inst = m_inv[slot]; + database.SaveInventory(CharacterID(), Inst, slot); - database.SaveInventory(CharacterID(), Inst, Slot); + EQApplicationPacket *outapp2; - EQApplicationPacket* outapp2; + if (quantity < charges) { + outapp2 = new EQApplicationPacket(OP_DeleteItem, sizeof(MoveItem_Struct)); + } + else { + outapp2 = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); + } - if(Quantity < Charges) - outapp2 = new EQApplicationPacket(OP_DeleteItem,sizeof(MoveItem_Struct)); - else - outapp2 = new EQApplicationPacket(OP_MoveItem,sizeof(MoveItem_Struct)); - - MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer; - mis->from_slot = Slot; - mis->to_slot = 0xFFFFFFFF; + auto mis = (MoveItem_Struct *) outapp2->pBuffer; + mis->from_slot = slot; + mis->to_slot = 0xFFFFFFFF; mis->number_in_stack = 0xFFFFFFFF; - if(Quantity >= Charges) - Quantity = 1; - - for(int i = 0; i < Quantity; i++) { + if (quantity >= charges) { + quantity = 1; + } + for (int i = 0; i < quantity; i++) { QueuePacket(outapp2); } + safe_delete(outapp2); - } -void Client::FindAndNukeTraderItem(int32 SerialNumber, int16 Quantity, Client* Customer, uint16 TraderSlot){ +void Client::FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client *customer, uint16 trader_slot) +{ + const EQ::ItemInstance *item = nullptr; + bool stackable = false; + int16 charges = 0; + uint16 slot_id = FindTraderItem(serial_number, quantity); - const EQ::ItemInstance* item= nullptr; - bool Stackable = false; - int16 Charges=0; - - uint16 SlotID = FindTraderItem(SerialNumber, Quantity); - - if(SlotID > 0) { - - item = GetInv().GetItem(SlotID); - - if (!item) - { - LogTrading("Could not find Item: [{}] on Trader: [{}]", SerialNumber, Quantity, GetName()); + if (slot_id > 0) { + item = GetInv().GetItem(slot_id); + if (!item) { + LogTrading("Could not find Item: [{}] on Trader: [{}]", serial_number, quantity, GetName()); return; } - Charges = GetInv().GetItem(SlotID)->GetCharges(); + charges = GetInv().GetItem(slot_id)->GetCharges(); + stackable = item->IsStackable(); + if (!stackable) { + quantity = (charges > 0) ? charges : 1; + } - Stackable = item->IsStackable(); + LogTrading("FindAndNuke [{}] charges [{}] quantity [{}]", + item->GetItem()->Name, + charges, + quantity + ); - if (!Stackable) - Quantity = (Charges > 0) ? Charges : 1; + if (charges <= quantity || (charges <= 0 && quantity == 1) || !stackable) { + DeleteItemInInventory(slot_id, quantity); + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ? + GetInv().GetLookup()->InventoryTypeSize.Bazaar : + trader_items.size(); + uint8 count = 0; + bool test_slot = true; - LogTrading("FindAndNuke [{}], Charges [{}], Quantity [{}]", item->GetItem()->Name, Charges, Quantity); - - if (Charges <= Quantity || (Charges <= 0 && Quantity==1) || !Stackable) - { - DeleteItemInInventory(SlotID, Quantity); - - TraderCharges_Struct* TraderItems = database.LoadTraderItemWithCharges(CharacterID()); - - uint8 Count = 0; - - bool TestSlot = true; - - for(int i = 0;i < 80;i++){ - - if(TestSlot && TraderItems->SerialNumber[i] == SerialNumber) - { - database.DeleteTraderItem(CharacterID(),i); - NukeTraderItem(SlotID, Charges, Quantity, Customer, TraderSlot, TraderItems->SerialNumber[i], TraderItems->ItemID[i]); - TestSlot=false; + std::vector delete_queue{}; + for (int i = 0; i < item_limit; i++) { + if (test_slot && trader_items.at(i).item_sn == serial_number) { + delete_queue.push_back(trader_items.at(i)); + NukeTraderItem( + slot_id, + charges, + quantity, + customer, + trader_slot, + trader_items.at(i).item_sn, + trader_items.at(i).item_id + ); + test_slot = false; } - else if (TraderItems->ItemID[i] > 0) - { - Count++; + else if (trader_items.at(i).item_id > 0) { + count++; } } - if (Count == 0) - { - Trader_EndTrader(); + + TraderRepository::DeleteMany(database, delete_queue); + if (count == 0) { + TraderEndTrader(); } - safe_delete(TraderItems); - return; } - else - { - database.UpdateTraderItemCharges(CharacterID(), item->GetSerialNumber(), Charges-Quantity); - - NukeTraderItem(SlotID, Charges, Quantity, Customer, TraderSlot, item->GetSerialNumber(), item->GetID()); - + else { + TraderRepository::UpdateQuantity(database, CharacterID(), item->GetSerialNumber(), charges - quantity); + NukeTraderItem(slot_id, charges, quantity, customer, trader_slot, item->GetSerialNumber(), item->GetID()); return; - } } - LogTrading("Could NOT find a match for Item: [{}] with a quantity of: [{}] on Trader: [{}]\n",SerialNumber, - Quantity,GetName()); + LogTrading("Could NOT find a match for Item: [{}] with a quantity of: [{}] on Trader: [{}]\n", + serial_number, + quantity, + GetName() + ); } -void Client::ReturnTraderReq(const EQApplicationPacket* app, int16 TraderItemCharges, uint32 itemid){ - - TraderBuy_Struct* tbs = (TraderBuy_Struct*)app->pBuffer; - - EQApplicationPacket* outapp = nullptr; - - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - outapp = new EQApplicationPacket(OP_TraderShop, sizeof(TraderBuy_Struct)); - } - else - { - outapp = new EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct)); - } - - TraderBuy_Struct* outtbs = (TraderBuy_Struct*)outapp->pBuffer; - memcpy(outtbs, tbs, app->size); - - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - // Convert Serial Number back to Item ID for RoF+ - outtbs->ItemID = itemid; - } - else - { - // RoF+ requires individual price, but older clients require total price - outtbs->Price = (tbs->Price * static_cast(TraderItemCharges)); - } - - outtbs->Quantity = TraderItemCharges; - // This should probably be trader ID, not customer ID as it is below. - outtbs->TraderID = GetID(); - outtbs->AlreadySold = 0; - - QueuePacket(outapp); - - safe_delete(outapp); -} - -void Client::TradeRequestFailed(const EQApplicationPacket* app) { - - TraderBuy_Struct* tbs = (TraderBuy_Struct*)app->pBuffer; - +void Client::ReturnTraderReq(const EQApplicationPacket *app, int16 trader_item_charges, uint32 item_id) +{ + auto tbs = (TraderBuy_Struct *) app->pBuffer; auto outapp = new EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct)); - - TraderBuy_Struct* outtbs = (TraderBuy_Struct*)outapp->pBuffer; + auto outtbs = (TraderBuy_Struct *) outapp->pBuffer; memcpy(outtbs, tbs, app->size); + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + // Convert Serial Number back to Item ID for RoF+ + outtbs->item_id = item_id; + } + else { + // RoF+ requires individual price, but older clients require total price + outtbs->price = (tbs->price * static_cast(trader_item_charges)); + } - outtbs->AlreadySold = 0xFFFFFFFF; - - outtbs->TraderID = 0xFFFFFFFF;; + outtbs->quantity = trader_item_charges; + // This should probably be trader ID, not customer ID as it is below. + outtbs->trader_id = GetID(); + outtbs->already_sold = 0; QueuePacket(outapp); - safe_delete(outapp); } +void Client::TradeRequestFailed(const EQApplicationPacket *app) +{ + auto tbs = (TraderBuy_Struct *) app->pBuffer; + auto outapp = new EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct)); + auto outtbs = (TraderBuy_Struct *) outapp->pBuffer; + + memcpy(outtbs, tbs, app->size); + outtbs->already_sold = 0xFFFFFFFF; + outtbs->trader_id = 0xFFFFFFFF; + + QueuePacket(outapp); + safe_delete(outapp); +} static void BazaarAuditTrail(const char *seller, const char *buyer, const char *itemName, int quantity, int totalCost, int tranType) { @@ -1540,221 +1556,242 @@ static void BazaarAuditTrail(const char *seller, const char *buyer, const char * database.QueryDatabase(query); } -void Client::BuyTraderItem(TraderBuy_Struct* tbs, Client* Trader, const EQApplicationPacket* app){ +void Client::BuyTraderItem(TraderBuy_Struct *tbs, Client *Trader, const EQApplicationPacket *app) +{ + if (!Trader) { + return; + } - if(!Trader) return; - - if(!Trader->IsTrader()) { + if (!Trader->IsTrader()) { TradeRequestFailed(app); return; } - auto outapp = new EQApplicationPacket(OP_Trader, sizeof(TraderBuy_Struct)); + auto in = (TraderBuy_Struct *) app->pBuffer; + auto outapp = std::make_unique(OP_Trader, sizeof(TraderBuy_Struct)); + auto outtbs = (TraderBuy_Struct *) outapp->pBuffer; + outtbs->item_id = tbs->item_id; + const EQ::ItemInstance *buy_item = nullptr; + uint32 item_id = 0; - TraderBuy_Struct* outtbs = (TraderBuy_Struct*)outapp->pBuffer; - - outtbs->ItemID = tbs->ItemID; - - const EQ::ItemInstance* BuyItem = nullptr; - uint32 ItemID = 0; - - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - // Convert Item ID to Serial Number for RoF+ - ItemID = tbs->ItemID; - tbs->ItemID = Trader->FindTraderItemSerialNumber(tbs->ItemID); + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + tbs->item_id = Strings::ToUnsignedBigInt(tbs->serial_number); } - BuyItem = Trader->FindTraderItemBySerialNumber(tbs->ItemID); + buy_item = Trader->FindTraderItemBySerialNumber(tbs->item_id); - if(!BuyItem) { - LogTrading("Unable to find item on trader"); + if (!buy_item) { + LogTrading("Unable to find item id [{}] item_sn [{}] on trader", tbs->item_id, tbs->serial_number); TradeRequestFailed(app); - safe_delete(outapp); return; } - tbs->Price = BuyItem->GetPrice(); - - LogTrading("Buyitem: Name: [{}], IsStackable: [{}], Requested Quantity: [{}], Charges on Item [{}]", - BuyItem->GetItem()->Name, BuyItem->IsStackable(), tbs->Quantity, BuyItem->GetCharges()); + LogTrading( + "Name: [{}] IsStackable: [{}] Requested Quantity: [{}] Charges on Item [{}]", + buy_item->GetItem()->Name, + buy_item->IsStackable(), + tbs->quantity, + buy_item->GetCharges() + ); // If the item is not stackable, then we can only be buying one of them. - if(!BuyItem->IsStackable()) - outtbs->Quantity = 1; // normally you can't send more than 1 here + if (!buy_item->IsStackable()) { + outtbs->quantity = 1; // normally you can't send more than 1 here + } else { // Stackable items, arrows, diamonds, etc - int32 ItemCharges = BuyItem->GetCharges(); + int32 item_charges = buy_item->GetCharges(); // ItemCharges for stackables should not be <= 0 - if(ItemCharges <= 0) - outtbs->Quantity = 1; - // If the purchaser requested more than is in the stack, just sell them how many are actually in the stack. - else if(static_cast(ItemCharges) < tbs->Quantity) - outtbs->Quantity = ItemCharges; - else - outtbs->Quantity = tbs->Quantity; + if (item_charges <= 0) { + outtbs->quantity = 1; + // If the purchaser requested more than is in the stack, just sell them how many are actually in the stack. + } + else if (static_cast(item_charges) < tbs->quantity) { + outtbs->quantity = item_charges; + } + else { + outtbs->quantity = tbs->quantity; + } } - LogTrading("Actual quantity that will be traded is [{}]", outtbs->Quantity); + LogTrading("Actual quantity that will be traded is [{}]", outtbs->quantity); - if((tbs->Price * outtbs->Quantity) <= 0) { - Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1"); - Trader->Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1"); - LogError("Bazaar: Zero price transaction between [{}] and [{}] aborted. Item: [{}], Charges: [{}], TBS: Qty [{}], Price: [{}]", - GetName(), Trader->GetName(), - BuyItem->GetItem()->Name, BuyItem->GetCharges(), tbs->Quantity, tbs->Price); + if ((tbs->price * outtbs->quantity) <= 0) { + Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1"); + Trader->Message( + Chat::Red, + "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1" + ); + LogError( + "Bazaar: Zero price transaction between [{}] and [{}] aborted. Item: [{}] Charges: " + "[{}] Qty [{}] Price: [{}]", + GetName(), + Trader->GetName(), + buy_item->GetItem()->Name, + buy_item->GetCharges(), + tbs->quantity, + tbs->price + ); + TradeRequestFailed(app); + return; + } + + uint64 total_transaction_value = static_cast(tbs->price) * static_cast(outtbs->quantity); + + if (total_transaction_value > MAX_TRANSACTION_VALUE) { + Message( + Chat::Red, + "That would exceed the single transaction limit of %u platinum.", + MAX_TRANSACTION_VALUE / 1000 + ); TradeRequestFailed(app); - safe_delete(outapp); - return; - } - - uint64 TotalTransactionValue = static_cast(tbs->Price) * static_cast(outtbs->Quantity); - - if(TotalTransactionValue > MAX_TRANSACTION_VALUE) { - Message(Chat::Red, "That would exceed the single transaction limit of %u platinum.", MAX_TRANSACTION_VALUE / 1000); - TradeRequestFailed(app); - safe_delete(outapp); return; } // This cannot overflow assuming MAX_TRANSACTION_VALUE, checked above, is the default of 2000000000 - uint32 TotalCost = tbs->Price * outtbs->Quantity; + uint32 total_cost = tbs->price * outtbs->quantity; - if (Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) - { + if (Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) { // RoF+ uses individual item price where older clients use total price - outtbs->Price = tbs->Price; + outtbs->price = tbs->price; } - else - { - outtbs->Price = TotalCost; + else { + outtbs->price = total_cost; } - if(!TakeMoneyFromPP(TotalCost)) { - RecordPlayerEventLog(PlayerEvent::POSSIBLE_HACK, PlayerEvent::PossibleHackEvent{.message = "Attempted to buy something in bazaar but did not have enough money."}); - TradeRequestFailed(app); - safe_delete(outapp); - return; - } + if (!TakeMoneyFromPP(total_cost)) { + RecordPlayerEventLog( + PlayerEvent::POSSIBLE_HACK, + PlayerEvent::PossibleHackEvent{ + .message = "Attempted to buy something in bazaar but did not have enough money." + } + ); + TradeRequestFailed(app); + return; + } - LogTrading("Customer Paid: [{}] in Copper", TotalCost); + LogTrading("Customer Paid: [{}] in Copper", total_cost); - uint32 platinum = TotalCost / 1000; - TotalCost -= (platinum * 1000); - uint32 gold = TotalCost / 100; - TotalCost -= (gold * 100); - uint32 silver = TotalCost / 10; - TotalCost -= (silver * 10); - uint32 copper = TotalCost; + uint32 platinum = total_cost / 1000; + total_cost -= (platinum * 1000); + uint32 gold = total_cost / 100; + total_cost -= (gold * 100); + uint32 silver = total_cost / 10; + total_cost -= (silver * 10); + uint32 copper = total_cost; Trader->AddMoneyToPP(copper, silver, gold, platinum, true); - if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { - auto e = PlayerEvent::TraderPurchaseEvent{ - .item_id = BuyItem->GetID(), - .item_name = BuyItem->GetItem()->Name, - .trader_id = Trader->CharacterID(), - .trader_name = Trader->GetCleanName(), - .price = tbs->Price, - .charges = outtbs->Quantity, - .total_cost = (tbs->Price * outtbs->Quantity), - .player_money_balance = GetCarriedMoney(), - }; + auto e = PlayerEvent::TraderPurchaseEvent{ + .item_id = buy_item->GetID(), + .item_name = buy_item->GetItem()->Name, + .trader_id = Trader->CharacterID(), + .trader_name = Trader->GetCleanName(), + .price = tbs->price, + .charges = outtbs->quantity, + .total_cost = (tbs->price * outtbs->quantity), + .player_money_balance = GetCarriedMoney(), + }; - RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); - } + RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); + } if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) { - auto e = PlayerEvent::TraderSellEvent{ - .item_id = BuyItem->GetID(), - .item_name = BuyItem->GetItem()->Name, - .buyer_id = CharacterID(), - .buyer_name = GetCleanName(), - .price = tbs->Price, - .charges = outtbs->Quantity, - .total_cost = (tbs->Price * outtbs->Quantity), - .player_money_balance = Trader->GetCarriedMoney(), - }; + auto e = PlayerEvent::TraderSellEvent{ + .item_id = buy_item->GetID(), + .item_name = buy_item->GetItem()->Name, + .buyer_id = CharacterID(), + .buyer_name = GetCleanName(), + .price = tbs->price, + .charges = outtbs->quantity, + .total_cost = (tbs->price * outtbs->quantity), + .player_money_balance = Trader->GetCarriedMoney(), + }; - RecordPlayerEventLogWithClient(Trader, PlayerEvent::TRADER_SELL, e); - } + RecordPlayerEventLogWithClient(Trader, PlayerEvent::TRADER_SELL, e); + } LogTrading("Trader Received: [{}] Platinum, [{}] Gold, [{}] Silver, [{}] Copper", platinum, gold, silver, copper); + ReturnTraderReq(app, outtbs->quantity, item_id); - ReturnTraderReq(app, outtbs->Quantity, ItemID); + outtbs->trader_id = GetID(); + outtbs->action = BazaarBuyItem; + strn0cpy(outtbs->seller_name, Trader->GetCleanName(), sizeof(outtbs->seller_name)); + strn0cpy(outtbs->buyer_name, GetCleanName(), sizeof(outtbs->buyer_name)); + strn0cpy(outtbs->item_name, buy_item->GetItem()->Name, sizeof(outtbs->item_name)); + strn0cpy( + outtbs->serial_number, + fmt::format("{:016}", buy_item->GetSerialNumber()).c_str(), + sizeof(outtbs->serial_number) + ); - outtbs->TraderID = GetID(); - outtbs->Action = BazaarBuyItem; - strn0cpy(outtbs->ItemName, BuyItem->GetItem()->Name, 64); + TraderRepository::Trader t{}; + t.item_charges = buy_item->IsStackable() ? outtbs->quantity : buy_item->GetCharges(); + t.item_id = buy_item->GetItem()->ID; + t.aug_slot_1 = buy_item->GetAugmentItemID(0); + t.aug_slot_2 = buy_item->GetAugmentItemID(1); + t.aug_slot_3 = buy_item->GetAugmentItemID(2); + t.aug_slot_4 = buy_item->GetAugmentItemID(3); + t.aug_slot_5 = buy_item->GetAugmentItemID(4); + t.aug_slot_6 = buy_item->GetAugmentItemID(5); + t.char_id = CharacterID(); + t.slot_id = FindNextFreeParcelSlot(CharacterID()); - int TraderSlot = 0; + SendTraderItem( + buy_item->GetItem()->ID, + buy_item->IsStackable() ? outtbs->quantity : buy_item->GetCharges(), + t + ); - if(BuyItem->IsStackable()) - SendTraderItem(BuyItem->GetItem()->ID, outtbs->Quantity); - else - SendTraderItem(BuyItem->GetItem()->ID, BuyItem->GetCharges()); - - TraderSlot = Trader->FindTraderItem(tbs->ItemID, outtbs->Quantity); - - if(RuleB(Bazaar, AuditTrail)) - BazaarAuditTrail(Trader->GetName(), GetName(), BuyItem->GetItem()->Name, outtbs->Quantity, outtbs->Price, 0); - - Trader->FindAndNukeTraderItem(tbs->ItemID, outtbs->Quantity, this, 0); - - if (ItemID > 0 && Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - // Convert Serial Number back to ItemID for RoF+ - outtbs->ItemID = ItemID; + if (RuleB(Bazaar, AuditTrail)) { + BazaarAuditTrail(Trader->GetName(), GetName(), buy_item->GetItem()->Name, outtbs->quantity, outtbs->price, 0); } - Trader->QueuePacket(outapp); - safe_delete(outapp); + Trader->FindAndNukeTraderItem(tbs->item_id, outtbs->quantity, this, 0); + + if (item_id > 0 && Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) { + // Convert Serial Number back to ItemID for RoF+ + outtbs->item_id = item_id; + } + + Trader->QueuePacket(outapp.get()); } void Client::SendBazaarWelcome() { - const std::string query = "SELECT COUNT(DISTINCT char_id), count(char_id) FROM trader"; - auto results = database.QueryDatabase(query); - if (results.Success() && results.RowCount() == 1){ - auto row = results.begin(); + const auto results = TraderRepository::GetWelcomeData(database); + auto outapp = std::make_unique(OP_BazaarSearch, sizeof(BazaarWelcome_Struct)); + auto data = (BazaarWelcome_Struct *) outapp->pBuffer; - EQApplicationPacket* outapp = nullptr; - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - outapp = new EQApplicationPacket(OP_TraderShop, sizeof(BazaarWelcome_Struct)); - } - else - { - outapp = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarWelcome_Struct)); - } + data->action = BazaarWelcome; + data->traders = results.count_of_traders; + data->items = results.count_of_items; - memset(outapp->pBuffer,0,outapp->size); + QueuePacket(outapp.get()); +} - BazaarWelcome_Struct* bws = (BazaarWelcome_Struct*)outapp->pBuffer; - - bws->Beginning.Action = BazaarWelcome; - - bws->Traders = Strings::ToInt(row[0]); - bws->Items = Strings::ToInt(row[1]); - - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) - { - bws->Unknown012 = GetID(); - } - - QueuePacket(outapp); - - safe_delete(outapp); +void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria) +{ + auto results = Bazaar::GetSearchResults(database, search_criteria, GetZoneID()); + if (results.empty()) { + SendBazaarDone(GetID()); + return; } - const std::string buyerCountQuery = "SELECT COUNT(DISTINCT charid) FROM buyer"; - results = database.QueryDatabase(buyerCountQuery); - if (!results.Success() || results.RowCount() != 1) - return; + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + ar(results); - auto row = results.begin(); - Message(Chat::NPCQuestSay, "There are %i Buyers waiting to purchase your loot. Type /barter to search for them, " - "or use /buyer to set up your own Buy Lines.", Strings::ToInt(row[0])); + uint32 packet_size = ss.str().length() + sizeof(BazaarSearchMessaging_Struct); + auto out = new EQApplicationPacket(OP_BazaarSearch, packet_size); + auto data = (BazaarSearchMessaging_Struct *) out->pBuffer; + + data->action = BazaarSearch; + memcpy(data->payload, ss.str().data(), ss.str().length()); + FastQueuePacket(&out); + + SendBazaarDone(GetID()); + SendBazaarDeliveryCosts(); } void Client::SendBazaarResults( @@ -2068,91 +2105,107 @@ void Client::SendBazaarResults( safe_delete(outapp2); } -static void UpdateTraderCustomerItemsAdded(uint32 CustomerID, TraderCharges_Struct* gis, uint32 ItemID) { - +static void UpdateTraderCustomerItemsAdded( + uint32 customer_id, + std::vector trader_items, + uint32 item_id, + uint32 item_limit +) +{ // Send Item packets to the customer to update the Merchant window with the // new items for sale, and give them a message in their chat window. - - Client* Customer = entity_list.GetClientByID(CustomerID); - - if(!Customer) return; - - const EQ::ItemData *item = database.GetItem(ItemID); - - if(!item) return; - - EQ::ItemInstance* inst = database.CreateItem(item); - - if(!inst) return; - - Customer->Message(Chat::Red, "The Trader has put up %s for sale.", item->Name); - - for(int i = 0; i < 80; i++) { - - if(gis->ItemID[i] == ItemID) { - - inst->SetCharges(gis->Charges[i]); - - inst->SetPrice(gis->ItemCost[i]); - - inst->SetSerialNumber(gis->SerialNumber[i]); - - inst->SetMerchantSlot(gis->SerialNumber[i]); - - if(inst->IsStackable()) - inst->SetMerchantCount(gis->Charges[i]); - - LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges", - item->Name, gis->SerialNumber[i], gis->Charges[i]); - - Customer->SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketMerchant); // MainCursor? - } + auto customer = entity_list.GetClientByID(customer_id); + if (!customer) { + return; } - safe_delete(inst); + const EQ::ItemData *item = database.GetItem(item_id); + if (!item) { + return; + } + + customer->Message(Chat::Red, "The Trader has put up %s for sale.", item->Name); + + for (auto const &i: trader_items) { + if (i.item_id == item_id) { + std::unique_ptr inst( + database.CreateItem( + i.item_id, + i.item_charges, + i.aug_slot_1, + i.aug_slot_2, + i.aug_slot_3, + i.aug_slot_4, + i.aug_slot_5, + i.aug_slot_6 + ) + ); + if (!inst) { + return; + } + + inst->SetCharges(i.item_charges); + inst->SetPrice(i.item_cost); + inst->SetSerialNumber(i.item_sn); + inst->SetMerchantSlot(i.item_sn); + if (inst->IsStackable()) { + inst->SetMerchantCount(i.item_charges); + } + + customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor? + LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges", + item->Name, i.item_sn, i.item_charges); + } + } } -static void UpdateTraderCustomerPriceChanged(uint32 CustomerID, TraderCharges_Struct* gis, uint32 ItemID, int32 Charges, uint32 NewPrice) { - +static void UpdateTraderCustomerPriceChanged( + uint32 customer_id, + std::vector trader_items, + uint32 item_id, + int32 charges, + uint32 new_price, + uint32 item_limit +) +{ // Send ItemPackets to update the customer's Merchant window with the new price (or remove the item if // the new price is 0) and inform them with a chat message. + auto customer = entity_list.GetClientByID(customer_id); - Client* Customer = entity_list.GetClientByID(CustomerID); + if (!customer) { + return; + } - if(!Customer) return; + const EQ::ItemData *item = database.GetItem(item_id); - const EQ::ItemData *item = database.GetItem(ItemID); + if (!item) { + return; + } - if(!item) return; - - if(NewPrice == 0) { + if (new_price == 0) { // If the new price is 0, remove the item(s) from the window. auto outapp = new EQApplicationPacket(OP_TraderDelItem, sizeof(TraderDelItem_Struct)); - TraderDelItem_Struct* tdis = (TraderDelItem_Struct*)outapp->pBuffer; + auto tdis = (TraderDelItem_Struct *) outapp->pBuffer; - tdis->Unknown000 = 0; - tdis->TraderID = Customer->GetID(); - tdis->Unknown012 = 0; - Customer->Message(Chat::Red, "The Trader has withdrawn the %s from sale.", item->Name); + tdis->unknown_000 = 0; + tdis->trader_id = customer->GetID(); + tdis->unknown_012 = 0; + customer->Message(Chat::Red, "The Trader has withdrawn the %s from sale.", item->Name); - for(int i = 0; i < 80; i++) { - - if(gis->ItemID[i] == ItemID) { - if (Customer->ClientVersion() >= EQ::versions::ClientVersion::RoF) - { + for (int i = 0; i < item_limit; i++) { + if (trader_items.at(i).item_id == item_id) { + if (customer->ClientVersion() >= EQ::versions::ClientVersion::RoF) { // RoF+ use Item IDs for now - tdis->ItemID = gis->ItemID[i]; + tdis->item_id = trader_items.at(i).item_id; } - else - { - tdis->ItemID = gis->SerialNumber[i]; + else { + tdis->item_id = trader_items.at(i).item_sn; } - tdis->ItemID = gis->SerialNumber[i]; + tdis->item_id = trader_items.at(i).item_sn; LogTrading("Telling customer to remove item [{}] with [{}] charges and S/N [{}]", - ItemID, Charges, gis->SerialNumber[i]); + item_id, charges, trader_items.at(i).item_sn); - - Customer->QueuePacket(outapp); + customer->QueuePacket(outapp); } } @@ -2160,239 +2213,52 @@ static void UpdateTraderCustomerPriceChanged(uint32 CustomerID, TraderCharges_St return; } - LogTrading("Sending price updates to customer [{}]", Customer->GetName()); + LogTrading("Sending price updates to customer [{}]", customer->GetName()); - EQ::ItemInstance* inst = database.CreateItem(item); + auto it = std::find_if(trader_items.begin(), trader_items.end(), [&](TraderRepository::Trader x){ return x.item_id == item->ID;}); + std::unique_ptr inst( + database.CreateItem( + it->item_id, + it->item_charges, + it->aug_slot_1, + it->aug_slot_2, + it->aug_slot_3, + it->aug_slot_4, + it->aug_slot_5, + it->aug_slot_6 + ) + ); + if (!inst) { + return; + } - if(!inst) return; + if (charges > 0) { + inst->SetCharges(charges); + } - if(Charges > 0) - inst->SetCharges(Charges); - - inst->SetPrice(NewPrice); - - if(inst->IsStackable()) - inst->SetMerchantCount(Charges); + inst->SetPrice(new_price); + if (inst->IsStackable()) { + inst->SetMerchantCount(charges); + } // Let the customer know the price in the window has suddenly just changed on them. - Customer->Message(Chat::Red, "The Trader has changed the price of %s.", item->Name); + customer->Message(Chat::Red, "The Trader has changed the price of %s.", item->Name); - for(int i = 0; i < 80; i++) { - if((gis->ItemID[i] != ItemID) || - ((!item->Stackable) && (gis->Charges[i] != Charges))) + for (int i = 0; i < item_limit; i++) { + if ((trader_items.at(i).item_id != item_id) || + ((!item->Stackable) && (trader_items.at(i).item_charges != charges))) { continue; + } - inst->SetSerialNumber(gis->SerialNumber[i]); - - inst->SetMerchantSlot(gis->SerialNumber[i]); + inst->SetSerialNumber(trader_items.at(i).item_sn); + inst->SetMerchantSlot(trader_items.at(i).item_sn); LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges", - item->Name, gis->SerialNumber[i], gis->Charges[i]); + item->Name, trader_items.at(i).item_sn, trader_items.at(i).item_charges); - Customer->SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketMerchant); // MainCursor?? + customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor?? } - safe_delete(inst); -} - -void Client::HandleTraderPriceUpdate(const EQApplicationPacket *app) { - - // Handle price updates from the Trader and update a customer browsing our stuff if necessary - // This method also handles removing items from sale and adding them back up whilst still in - // Trader mode. - // - TraderPriceUpdate_Struct* tpus = (TraderPriceUpdate_Struct*)app->pBuffer; - - LogTrading("Received Price Update for [{}], Item Serial No. [{}], New Price [{}]", - GetName(), tpus->SerialNumber, tpus->NewPrice); - - // Pull the items this Trader currently has for sale from the trader table. - // - TraderCharges_Struct* gis = database.LoadTraderItemWithCharges(CharacterID()); - - if(!gis) { - LogDebug("[CLIENT] Error retrieving Trader items details to update price"); - return; - } - - // The client only sends a single update with the Serial Number of the item whose price has been updated. - // We must update the price for all the Trader's items that are identical to that one item, i.e. - // if it is a stackable item like arrows, update the price for all stacks. If it is not stackable, then - // update the prices for all items that have the same number of charges. - // - uint32 IDOfItemToUpdate = 0; - - int32 ChargesOnItemToUpdate = 0; - - uint32 OldPrice = 0; - - for(int i = 0; i < 80; i++) { - - if((gis->ItemID[i] > 0) && (gis->SerialNumber[i] == tpus->SerialNumber)) { - // We found the item that the Trader wants to change the price of (or add back up for sale). - // - LogTrading("ItemID is [{}], Charges is [{}]", gis->ItemID[i], gis->Charges[i]); - - IDOfItemToUpdate = gis->ItemID[i]; - - ChargesOnItemToUpdate = gis->Charges[i]; - - OldPrice = gis->ItemCost[i]; - - break; - } - } - - if(IDOfItemToUpdate == 0) { - - // If the item is not currently in the trader table for this Trader, then they must have removed it from sale while - // still in Trader mode. Check if the item is in their Trader Satchels, and if so, put it back up. - - // Quick Sanity check. If the item is not currently up for sale, and the new price is zero, just ack the packet - // and do nothing. - if(tpus->NewPrice == 0) { - tpus->SubAction = BazaarPriceChange_RemoveItem; - QueuePacket(app); - safe_delete(gis); - return ; - } - - LogTrading("Unable to find item to update price for. Rechecking trader satchels"); - - // Find what is in their Trader Satchels - GetItems_Struct* newgis=GetTraderItems(); - - uint32 IDOfItemToAdd = 0; - - int32 ChargesOnItemToAdd = 0; - - for(int i = 0; i < 80; i++) { - - if((newgis->Items[i] > 0) && (newgis->SerialNumber[i] == tpus->SerialNumber)) { - - LogTrading("Found new Item to Add, ItemID is [{}], Charges is [{}]", newgis->Items[i], - newgis->Charges[i]); - - IDOfItemToAdd = newgis->Items[i]; - ChargesOnItemToAdd = newgis->Charges[i]; - - break; - } - } - - - const EQ::ItemData *item = 0; - - if(IDOfItemToAdd) - item = database.GetItem(IDOfItemToAdd); - - if(!IDOfItemToAdd || !item) { - - LogTrading("Item not found in Trader Satchels either"); - tpus->SubAction = BazaarPriceChange_Fail; - QueuePacket(app); - Trader_EndTrader(); - safe_delete(gis); - safe_delete(newgis); - return; - } - - // It is a limitation of the client that if you have multiple of the same item, but with different charges, - // although you can set different prices for them before entering Trader mode. If you Remove them and then - // add them back whilst still in Trader mode, they all go up for the same price. We check for this situation - // and give the Trader a warning message. - // - if(!item->Stackable) { - - bool SameItemWithDifferingCharges = false; - - for(int i = 0; i < 80; i++) { - if((newgis->Items[i] == IDOfItemToAdd) && (newgis->Charges[i] != ChargesOnItemToAdd)) { - - SameItemWithDifferingCharges = true; - break; - } - } - - if(SameItemWithDifferingCharges) - Message(Chat::Red, "Warning: You have more than one %s with different charges. They have all been added for sale " - "at the same price.", item->Name); - } - - // Now put all Items with a matching ItemID up for trade. - // - for(int i = 0; i < 80; i++) { - - if(newgis->Items[i] == IDOfItemToAdd) { - - database.SaveTraderItem(CharacterID(), newgis->Items[i], newgis->SerialNumber[i], newgis->Charges[i], - tpus->NewPrice, i); - - gis->ItemID[i] = newgis->Items[i]; - gis->Charges[i] = newgis->Charges[i]; - gis->SerialNumber[i] = newgis->SerialNumber[i]; - gis->ItemCost[i] = tpus->NewPrice; - - LogTrading("Adding new item for [{}]. ItemID [{}], SerialNumber [{}], Charges [{}], Price: [{}], Slot [{}]", - GetName(), newgis->Items[i], newgis->SerialNumber[i], newgis->Charges[i], - tpus->NewPrice, i); - } - } - - // If we have a customer currently browsing, update them with the new items. - // - if(CustomerID) - UpdateTraderCustomerItemsAdded(CustomerID, gis, IDOfItemToAdd); - - safe_delete(gis); - safe_delete(newgis); - - // Acknowledge to the client. - tpus->SubAction = BazaarPriceChange_AddItem; - QueuePacket(app); - - return; - } - - // This is a safeguard against a Trader increasing the price of an item while a customer is browsing and - // unwittingly buying it at a higher price than they were expecting to. - // - if((OldPrice != 0) && (tpus->NewPrice > OldPrice) && CustomerID) { - - tpus->SubAction = BazaarPriceChange_Fail; - QueuePacket(app); - Trader_EndTrader(); - Message(Chat::Red, "You must remove the item from sale before you can increase the price while a customer is browsing."); - Message(Chat::Red, "Click 'Begin Trader' to restart Trader mode with the increased price for this item."); - safe_delete(gis); - return; - } - - // Send Acknowledgement back to the client. - if(OldPrice == 0) - tpus->SubAction = BazaarPriceChange_AddItem; - else if(tpus->NewPrice != 0) - tpus->SubAction = BazaarPriceChange_UpdatePrice; - else - tpus->SubAction = BazaarPriceChange_RemoveItem; - - QueuePacket(app); - - if(OldPrice == tpus->NewPrice) { - LogTrading("The new price is the same as the old one"); - safe_delete(gis); - return; - } - // Update the price for all items we have for sale that have this ItemID and number of charges, or remove - // them from the trader table if the new price is zero. - // - database.UpdateTraderItemPrice(CharacterID(), IDOfItemToUpdate, ChargesOnItemToUpdate, tpus->NewPrice); - - // If a customer is browsing our goods, send them the updated prices / remove the items from the Merchant window - if(CustomerID) - UpdateTraderCustomerPriceChanged(CustomerID, gis, IDOfItemToUpdate, ChargesOnItemToUpdate, tpus->NewPrice); - - safe_delete(gis); - +// safe_delete(inst); } void Client::SendBuyerResults(char* searchString, uint32 searchID) { @@ -2948,7 +2814,7 @@ void Client::ToggleBuyerMode(bool TurnOn) { else { VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00); database.DeleteBuyLines(CharacterID()); - CustomerID = 0; + SetCustomerID(0); } VARSTRUCT_ENCODE_STRING(Buf, GetName()); @@ -3098,3 +2964,617 @@ const std::string &Client::GetMailKey() const { return m_mail_key; } + +void Client::SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions action) +{ + auto outapp = new ServerPacket(ServerOP_TraderMessaging, sizeof(TraderMessaging_Struct)); + auto data = (TraderMessaging_Struct *) outapp->pBuffer; + + data->action = action; + data->entity_id = trader->GetID(); + data->trader_id = trader->CharacterID(); + data->zone_id = trader->GetZoneID(); + strn0cpy(data->trader_name, trader->GetName(), sizeof(data->trader_name)); + + worldserver.SendPacket(outapp); + safe_delete(outapp); +} + +void Client::SendBecomeTrader(BazaarTraderBarterActions action, uint32 entity_id) +{ + if (entity_id <= 0) { + return; + } + + auto trader = entity_list.GetClientByID(entity_id); + if (!trader) { + return; + } + + auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct)); + auto data = (BecomeTrader_Struct *) outapp->pBuffer; + + data->action = action; + data->entity_id = trader->GetID(); + data->trader_id = trader->CharacterID(); + strn0cpy(data->trader_name, trader->GetCleanName(), sizeof(data->trader_name)); + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::SendTraderMode(BazaarTraderBarterActions status) +{ + auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct)); + auto data = (Trader_ShowItems_Struct *) outapp->pBuffer; + + data->action = status; + data->entity_id = GetID(); + + QueuePacket(outapp); + safe_delete(outapp); +} + +void Client::TraderPriceUpdate(const EQApplicationPacket *app) +{ + // Handle price updates from the Trader and update a customer browsing our stuff if necessary + // This method also handles removing items from sale and adding them back up whilst still in + // Trader mode. + // + auto tpus = (TraderPriceUpdate_Struct *) app->pBuffer; + + LogTrading( + "Received Price Update for [{}] Item Serial No. [{}] New Price [{}]", + GetName(), + tpus->SerialNumber, + tpus->NewPrice + ); + + // Pull the items this Trader currently has for sale from the trader table. + // + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar + ? GetInv().GetLookup()->InventoryTypeSize.Bazaar + : trader_items.size(); + + // The client only sends a single update with the Serial Number of the item whose price has been updated. + // We must update the price for all the Trader's items that are identical to that one item, i.e. + // if it is a stackable item like arrows, update the price for all stacks. If it is not stackable, then + // update the prices for all items that have the same number of charges. + // + uint32 id_of_item_to_update = 0; + int32 charges_on_item_to_update = 0; + uint32 old_price = 0; + + for (int i = 0; i < item_limit; i++) { + if ((trader_items.at(i).item_id > 0) && (trader_items.at(i).item_sn == tpus->SerialNumber)) { + // We found the item that the Trader wants to change the price of (or add back up for sale). + // + id_of_item_to_update = trader_items.at(i).item_id; + charges_on_item_to_update = trader_items.at(i).item_charges; + old_price = trader_items.at(i).item_cost; + + LogTrading( + "ItemID is [{}] Charges is [{}]", + trader_items.at(i).item_id, + trader_items.at(i).item_charges + ); + break; + } + } + + if (id_of_item_to_update == 0) { + // If the item is not currently in the trader table for this Trader, then they must have removed it from sale while + // still in Trader mode. Check if the item is in their Trader Satchels, and if so, put it back up. + // Quick Sanity check. If the item is not currently up for sale, and the new price is zero, just ack the packet + // and do nothing. + if (tpus->NewPrice == 0) { + tpus->SubAction = BazaarPriceChange_RemoveItem; + QueuePacket(app); + return; + } + + LogTrading("Unable to find item to update price for. Rechecking trader satchels"); + + // Find what is in their Trader Satchels + auto newgis = GetTraderItems(); + uint32 id_of_item_to_add = 0; + int32 charges_on_item_to_add = 0; + + for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) { + if ((newgis->items[i] > 0) && (newgis->serial_number[i] == tpus->SerialNumber)) { + id_of_item_to_add = newgis->items[i]; + charges_on_item_to_add = newgis->charges[i]; + + LogTrading( + "Found new Item to Add, ItemID is [{}] Charges is [{}]", + newgis->items[i], + newgis->charges[i] + ); + break; + } + } + + const EQ::ItemData *item = nullptr; + if (id_of_item_to_add) { + item = database.GetItem(id_of_item_to_add); + } + + if (!id_of_item_to_add || !item) { + tpus->SubAction = BazaarPriceChange_Fail; + QueuePacket(app); + TraderEndTrader(); + safe_delete(newgis); + + LogTrading("Item not found in Trader Satchels either"); + return; + } + + // It is a limitation of the client that if you have multiple of the same item, but with different charges, + // although you can set different prices for them before entering Trader mode. If you Remove them and then + // add them back whilst still in Trader mode, they all go up for the same price. We check for this situation + // and give the Trader a warning message. + // + if (!item->Stackable) { + bool same_item_with_differing_charges = false; + + for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) { + if ((newgis->items[i] == id_of_item_to_add) && (newgis->charges[i] != charges_on_item_to_add)) { + same_item_with_differing_charges = true; + break; + } + } + + if (same_item_with_differing_charges) { + Message( + Chat::Red, + "Warning: You have more than one %s with different charges. They have all been added for sale " + "at the same price.", + item->Name + ); + } + } + + // Now put all Items with a matching ItemID up for trade. + // + for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) { + if (newgis->items[i] == id_of_item_to_add) { + auto item_detail = FindTraderItemBySerialNumber(newgis->serial_number[i]); + + TraderRepository::Trader trader_item{}; + trader_item.id = 0; + trader_item.char_entity_id = GetID(); + trader_item.char_id = CharacterID(); + trader_item.char_zone_id = GetZoneID(); + trader_item.item_charges = newgis->charges[i]; + trader_item.item_cost = tpus->NewPrice; + trader_item.item_id = newgis->items[i]; + trader_item.item_sn = newgis->serial_number[i]; + if (item_detail->IsAugmented()) { + auto augs = item_detail->GetAugmentIDs(); + trader_item.aug_slot_1 = augs.at(0); + trader_item.aug_slot_2 = augs.at(1); + trader_item.aug_slot_3 = augs.at(2); + trader_item.aug_slot_4 = augs.at(3); + trader_item.aug_slot_5 = augs.at(4); + trader_item.aug_slot_6 = augs.at(5); + } + trader_item.slot_id = i; + + TraderRepository::ReplaceOne(database, trader_item); + + trader_items.push_back(trader_item); + + LogTrading( + "Adding new item for [{}] ItemID [{}] SerialNumber [{}] Charges [{}] " + "Price: [{}] Slot [{}]", + GetName(), + newgis->items[i], + newgis->serial_number[i], + newgis->charges[i], + tpus->NewPrice, + i + ); + } + } + + // If we have a customer currently browsing, update them with the new items. + // + if (GetCustomerID()) { + UpdateTraderCustomerItemsAdded( + GetCustomerID(), + trader_items, + id_of_item_to_add, + GetInv().GetLookup()->InventoryTypeSize.Bazaar + ); + } + + safe_delete(newgis); + + // Acknowledge to the client. + tpus->SubAction = BazaarPriceChange_AddItem; + QueuePacket(app); + + return; + } + + // This is a safeguard against a Trader increasing the price of an item while a customer is browsing and + // unwittingly buying it at a higher price than they were expecting to. + // + if ((old_price != 0) && (tpus->NewPrice > old_price) && GetCustomerID()) { + tpus->SubAction = BazaarPriceChange_Fail; + QueuePacket(app); + TraderEndTrader(); + Message( + Chat::Red, + "You must remove the item from sale before you can increase the price while a customer is browsing." + ); + Message(Chat::Red, "Click 'Begin Trader' to restart Trader mode with the increased price for this item."); + return; + } + + // Send Acknowledgement back to the client. + if (old_price == 0) { + tpus->SubAction = BazaarPriceChange_AddItem; + } + else if (tpus->NewPrice != 0) { + tpus->SubAction = BazaarPriceChange_UpdatePrice; + } + else { + tpus->SubAction = BazaarPriceChange_RemoveItem; + } + + QueuePacket(app); + + if (old_price == tpus->NewPrice) { + LogTrading("The new price is the same as the old one"); + return; + } + // Update the price for all items we have for sale that have this ItemID and number of charges, or remove + // them from the trader table if the new price is zero. + // + database.UpdateTraderItemPrice(CharacterID(), id_of_item_to_update, charges_on_item_to_update, tpus->NewPrice); + + // If a customer is browsing our goods, send them the updated prices / remove the items from the Merchant window + if (GetCustomerID()) { + UpdateTraderCustomerPriceChanged( + GetCustomerID(), + trader_items, + id_of_item_to_update, + charges_on_item_to_update, + tpus->NewPrice, + item_limit + ); + } +} + +void Client::SendBazaarDone(uint32 trader_id) +{ + auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); + auto brds = (BazaarReturnDone_Struct *) outapp2->pBuffer; + + brds->TraderID = trader_id; + brds->Type = BazaarSearchDone; + brds->Unknown008 = 0xFFFFFFFF; + brds->Unknown012 = 0xFFFFFFFF; + brds->Unknown016 = 0xFFFFFFFF; + + QueuePacket(outapp2); + safe_delete(outapp2); +} + +void Client::SendBulkBazaarTraders() +{ + if (ClientVersion() < EQ::versions::ClientVersion::RoF2) { + return; + } + + auto results = TraderRepository::GetDistinctTraders(database); + auto p_size = 4 + 12 * results.count + results.name_length; + auto buffer = std::make_unique(p_size); + memset(buffer.get(), 0, p_size); + char *bufptr = buffer.get(); + + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, results.count); + + for (auto t : results.traders) { + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.zone_id); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.trader_id); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.entity_id); + VARSTRUCT_ENCODE_STRING(bufptr, t.trader_name.c_str()); + } + + auto outapp = std::make_unique(OP_TraderBulkSend, p_size); + memcpy(outapp->pBuffer, buffer.get(), p_size); + + QueuePacket(outapp.get()); +} + +void Client::DoBazaarInspect(const BazaarInspect_Struct &in) +{ + auto items = TraderRepository::GetWhere(database, fmt::format("item_sn = {}", in.serial_number)); + if (items.empty()) { + LogInfo("Failed to find item with serial number [{}]", in.serial_number); + return; + } + + auto item = items.front(); + + std::unique_ptr inst( + database.CreateItem( + item.item_id, + item.item_charges, + item.aug_slot_1, + item.aug_slot_2, + item.aug_slot_3, + item.aug_slot_4, + item.aug_slot_5, + item.aug_slot_6 + ) + ); + + if (inst) { + SendItemPacket(0, inst.get(), ItemPacketViewLink); + } +} + +void Client::SendBazaarDeliveryCosts() +{ + auto outapp = std::make_unique(OP_BazaarSearch, sizeof(BazaarDeliveryCost_Struct)); + auto data = (BazaarDeliveryCost_Struct *) outapp->pBuffer; + + data->action = DeliveryCostUpdate; + data->voucher_delivery_cost = RuleI(Bazaar, VoucherDeliveryCost); + data->parcel_deliver_cost = RuleR(Bazaar, ParcelDeliveryCostMod); + + QueuePacket(outapp.get()); +} + +std::string Client::DetermineMoneyString(uint64 cp) +{ + uint32 plat = cp / 1000; + uint32 gold = (cp - plat * 1000) / 100; + uint32 silver = (cp - plat * 1000 - gold * 100) / 10; + uint32 copper = (cp - plat * 1000 - gold * 100 - silver * 10); + + if (!plat && !gold && !silver && !copper) { + return std::string("No Money"); + } + + std::string money {}; + if (plat) { + money += fmt::format("{}p ", plat); + } + if (gold) { + money += fmt::format("{}g ", gold); + } + if (silver) { + money += fmt::format("{}s ", silver); + } + if (copper) { + money += fmt::format("{}c", copper); + } + + return fmt::format("{}", money); +} + +void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicationPacket *app) +{ + auto in = (TraderBuy_Struct *) app->pBuffer; + auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number); + if (!trader_item.id) { + LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " + "[{}] The Traders data was outdated.", + tbs->trader_id, + tbs->serial_number + ); + in->method = ByParcel; + in->sub_action = DataOutDated; + TradeRequestFailed(app); + return; + } + + if (trader_item.active_transaction) { + LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " + "[{}] The item is already within an active transaction.", + tbs->trader_id, + tbs->serial_number + ); + in->method = ByParcel; + in->sub_action = DataOutDated; + TradeRequestFailed(app); + return; + } + + TraderRepository::UpdateActiveTransaction(database, trader_item.id, true); + + std::unique_ptr buy_item( + database.CreateItem( + trader_item.item_id, + trader_item.item_charges, + trader_item.aug_slot_1, + trader_item.aug_slot_2, + trader_item.aug_slot_3, + trader_item.aug_slot_4, + trader_item.aug_slot_5, + trader_item.aug_slot_6 + ) + ); + + if (!buy_item) { + LogTrading("Unable to find item id [{}] item_sn [{}] on trader", + trader_item.item_id, + trader_item.item_sn + ); + in->method = ByParcel; + in->sub_action = Failed; + TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); + TradeRequestFailed(app); + return; + } + + LogTrading( + "Name: [{}] IsStackable: [{}] Requested Quantity: [{}] Charges on Item [{}]", + buy_item->GetItem()->Name, + buy_item->IsStackable(), + tbs->quantity, + buy_item->GetCharges() + ); + + // Determine the actual quantity for the purchase + if (!buy_item->IsStackable()) { + tbs->quantity = 1; + } + else { + int32 item_charges = buy_item->GetCharges(); + if (item_charges <= 0) { + tbs->quantity = 1; + } + else if (static_cast(item_charges) < tbs->quantity) { + tbs->quantity = item_charges; + } + } + + LogTrading("Actual quantity that will be traded is [{}]", tbs->quantity); + + uint64 total_transaction_value = static_cast(tbs->price) * static_cast(tbs->quantity); + if (total_transaction_value > MAX_TRANSACTION_VALUE) { + Message( + Chat::Red, + "That would exceed the single transaction limit of %u platinum.", + MAX_TRANSACTION_VALUE / 1000 + ); + TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); + TradeRequestFailed(app); + return; + } + + uint32 total_cost = tbs->price * tbs->quantity; + uint32 fee = static_cast(std::round((uint32) total_cost * RuleR(Bazaar, ParcelDeliveryCostMod))); + if (!TakeMoneyFromPP(total_cost + fee)) { + RecordPlayerEventLog( + PlayerEvent::POSSIBLE_HACK, + PlayerEvent::PossibleHackEvent{ + .message = fmt::format( + "{} attempted to buy {} in bazaar but did not have enough money.", + GetCleanName(), + buy_item->GetItem()->Name + ) + } + ); + in->method = ByParcel; + in->sub_action = InsufficientFunds; + TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); + TradeRequestFailed(app); + return; + } + + Message(Chat::Red, fmt::format("You paid {} for the parcel delivery.", DetermineMoneyString(fee)).c_str()); + LogTrading("Customer [{}] Paid: [{}] in Copper", CharacterID(), total_cost); + + if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { + auto e = PlayerEvent::TraderPurchaseEvent{ + .item_id = buy_item->GetID(), + .item_name = buy_item->GetItem()->Name, + .trader_id = tbs->trader_id, + .trader_name = tbs->seller_name, + .price = tbs->price, + .charges = tbs->quantity, + .total_cost = total_cost, + .player_money_balance = GetCarriedMoney(), + }; + + RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); + } + + CharacterParcelsRepository::CharacterParcels parcel_out{}; + auto next_slot = FindNextFreeParcelSlot(CharacterID()); + if (next_slot == INVALID_INDEX) { + LogTrading( + "{} attempted to purchase {} from the bazaar with parcel delivery. Unfortunately their parcel limit was reached. " + "Purchase unsuccessful.", + GetCleanName(), + buy_item->GetItem()->Name + ); + in->method = ByParcel; + in->sub_action = TooManyParcels; + TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); + TradeRequestFailed(app); + return; + } + parcel_out.from_name = tbs->seller_name; + parcel_out.note = "Delivered from a Bazaar Purchase"; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = buy_item->IsStackable() ? tbs->quantity : buy_item->GetCharges(); + parcel_out.item_id = buy_item->GetItem()->ID; + parcel_out.aug_slot_1 = buy_item->GetAugmentItemID(0); + parcel_out.aug_slot_2 = buy_item->GetAugmentItemID(1); + parcel_out.aug_slot_3 = buy_item->GetAugmentItemID(2); + parcel_out.aug_slot_4 = buy_item->GetAugmentItemID(3); + parcel_out.aug_slot_5 = buy_item->GetAugmentItemID(4); + parcel_out.aug_slot_6 = buy_item->GetAugmentItemID(5); + parcel_out.char_id = CharacterID(); + parcel_out.slot_id = next_slot; + parcel_out.id = 0; + + auto result = CharacterParcelsRepository::InsertOne(database, parcel_out); + if (!result.id) { + LogError("Failed to add parcel to database. From {} to {} item {} quantity {}", + parcel_out.from_name, + GetCleanName(), + parcel_out.item_id, + parcel_out.quantity + ); + Message(Chat::Yellow, "Unable to save parcel to the database. Please contact an administrator."); + in->method = ByParcel; + in->sub_action = Failed; + TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); + TradeRequestFailed(app); + return; + } + + ReturnTraderReq(app, tbs->quantity, buy_item->GetID()); + if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) { + PlayerEvent::ParcelSend e{}; + e.from_player_name = parcel_out.from_name; + e.to_player_name = GetCleanName(); + e.item_id = parcel_out.item_id; + e.aug_slot_1 = parcel_out.aug_slot_1; + e.aug_slot_2 = parcel_out.aug_slot_2; + e.aug_slot_3 = parcel_out.aug_slot_3; + e.aug_slot_4 = parcel_out.aug_slot_4; + e.aug_slot_5 = parcel_out.aug_slot_5; + e.aug_slot_6 = parcel_out.aug_slot_6; + e.quantity = parcel_out.quantity; + e.sent_date = parcel_out.sent_date; + + RecordPlayerEventLog(PlayerEvent::PARCEL_SEND, e); + } + + Parcel_Struct ps{}; + ps.item_slot = parcel_out.slot_id; + strn0cpy(ps.send_to, GetCleanName(), sizeof(ps.send_to)); + + SendParcelDeliveryToWorld(ps); + + if (RuleB(Bazaar, AuditTrail)) { + BazaarAuditTrail(tbs->seller_name, GetName(), buy_item->GetItem()->Name, tbs->quantity, tbs->price, 0); + } + + auto out_server = std::make_unique(ServerOP_BazaarPurchase, sizeof(BazaarPurchaseMessaging_Struct)); + auto out_data = (BazaarPurchaseMessaging_Struct *) out_server->pBuffer; + + out_data->trader_buy_struct = *tbs; + out_data->buyer_id = CharacterID(); + out_data->item_aug_1 = buy_item->GetAugmentItemID(0); + out_data->item_aug_2 = buy_item->GetAugmentItemID(1); + out_data->item_aug_3 = buy_item->GetAugmentItemID(2); + out_data->item_aug_4 = buy_item->GetAugmentItemID(3); + out_data->item_aug_5 = buy_item->GetAugmentItemID(4); + out_data->item_aug_6 = buy_item->GetAugmentItemID(5); + out_data->item_quantity_available = trader_item.item_charges; + 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()); +} diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 358fc4e75..4316eaf1b 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -3915,10 +3915,98 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) } break; } - default: { - LogInfo("Unknown ZS Opcode [{}] size [{}]", (int)pack->opcode, pack->size); - break; - } + case ServerOP_TraderMessaging: { + auto in = (TraderMessaging_Struct *) pack->pBuffer; + for (auto const &c: entity_list.GetClientList()) { + if (c.second->ClientVersion() >= EQ::versions::ClientVersion::RoF2) { + auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct)); + auto out = (BecomeTrader_Struct *) outapp->pBuffer; + switch (in->action) { + case TraderOn: { + out->action = AddTraderToBazaarWindow; + break; + } + case TraderOff: { + out->action = RemoveTraderFromBazaarWindow; + break; + } + default: { + out->action = 0; + } + } + out->entity_id = in->entity_id; + out->zone_id = in->zone_id; + out->trader_id = in->trader_id; + strn0cpy(out->trader_name, in->trader_name, sizeof(out->trader_name)); + + c.second->QueuePacket(outapp); + safe_delete(outapp); + } + if (zone && zone->GetZoneID() == Zones::BAZAAR) { + if (in->action == TraderOn) { + c.second->SendBecomeTrader(TraderOn, in->entity_id); + } + else { + c.second->SendBecomeTrader(TraderOff, in->entity_id); + } + } + } + break; + } + case ServerOP_BazaarPurchase: { + auto in = (BazaarPurchaseMessaging_Struct *) pack->pBuffer; + auto trader_pc = entity_list.GetClientByCharID(in->trader_buy_struct.trader_id); + if (!trader_pc) { + LogTrading("Request trader_id [{}] could not be found in zone_id [{}]", + in->trader_buy_struct.trader_id, + zone->GetZoneID() + ); + return; + } + + auto item_sn = Strings::ToUnsignedBigInt(in->trader_buy_struct.serial_number); + auto outapp = std::make_unique(OP_Trader, sizeof(TraderBuy_Struct)); + auto data = (TraderBuy_Struct *) outapp->pBuffer; + + memcpy(data, &in->trader_buy_struct, sizeof(TraderBuy_Struct)); + + if (trader_pc->ClientVersion() < EQ::versions::ClientVersion::RoF) { + data->price = in->trader_buy_struct.price * in->trader_buy_struct.quantity; + } + + TraderRepository::UpdateQuantity( + database, + trader_pc->CharacterID(), + item_sn, + in->item_quantity_available - in->trader_buy_struct.quantity + ); + TraderRepository::UpdateActiveTransaction(database, in->id, false); + + trader_pc->RemoveItemBySerialNumber(item_sn, in->trader_buy_struct.quantity); + trader_pc->AddMoneyToPP(in->trader_buy_struct.price * in->trader_buy_struct.quantity, true); + trader_pc->QueuePacket(outapp.get()); + + if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) { + auto e = PlayerEvent::TraderSellEvent{ + .item_id = in->trader_buy_struct.item_id, + .item_name = in->trader_buy_struct.item_name, + .buyer_id = in->buyer_id, + .buyer_name = in->trader_buy_struct.buyer_name, + .price = in->trader_buy_struct.price, + .charges = in->trader_buy_struct.quantity, + .total_cost = (in->trader_buy_struct.price * in->trader_buy_struct.quantity), + .player_money_balance = trader_pc->GetCarriedMoney(), + }; + + RecordPlayerEventLogWithClient(trader_pc, PlayerEvent::TRADER_SELL, e); + } + + break; + } + default: { + LogInfo("Unknown ZS Opcode [{}] size [{}]", (int) pack->opcode, pack->size); + break; + } } } diff --git a/zone/zone.cpp b/zone/zone.cpp index 01fbf1234..69c401176 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -68,6 +68,7 @@ #include "../common/repositories/merc_stance_entries_repository.h" #include "../common/repositories/alternate_currency_repository.h" #include "../common/repositories/graveyard_repository.h" +#include "../common/repositories/trader_repository.h" #include @@ -1197,7 +1198,7 @@ bool Zone::Init(bool is_static) { //clear trader items if we are loading the bazaar if (strncasecmp(short_name, "bazaar", 6) == 0) { - database.DeleteTraderItem(0); + TraderRepository::Truncate(database); database.DeleteBuyLines(0); } diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 6729becb6..39b968b5f 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -51,6 +51,9 @@ #include "../common/repositories/character_corpse_items_repository.h" #include "../common/repositories/zone_repository.h" +#include "../common/repositories/trader_repository.h" + + #include #include #include @@ -302,187 +305,114 @@ void ZoneDatabase::DeleteWorldContainer(uint32 parent_id, uint32 zone_id) ); } -Trader_Struct* ZoneDatabase::LoadTraderItem(uint32 char_id) +std::unique_ptr ZoneDatabase::LoadSingleTraderItem(uint32 char_id, int serial_number) { - auto loadti = new Trader_Struct; - memset(loadti,0,sizeof(Trader_Struct)); + auto results = TraderRepository::GetWhere( + database, + fmt::format( + "`char_id` = '{}' AND `item_sn` = '{}' ORDER BY slot_id", + char_id, + serial_number + ) + ); - std::string query = StringFormat("SELECT * FROM trader WHERE char_id = %i ORDER BY slot_id LIMIT 80", char_id); - auto results = QueryDatabase(query); - if (!results.Success()) { - LogTrading("Failed to load trader information!\n"); - return loadti; + if (results.empty()) { + LogTrading("Could not find item serial number {} for character id {}", serial_number, char_id); } - loadti->Code = BazaarTrader_ShowItems; - for (auto& row = results.begin(); row != results.end(); ++row) { - if (Strings::ToInt(row[5]) >= 80 || Strings::ToInt(row[4]) < 0) { - LogTrading("Bad Slot number when trying to load trader information!\n"); - continue; - } + int item_id = results.at(0).item_id; + int charges = results.at(0).item_charges; + int cost = results.at(0).item_cost; - loadti->Items[Strings::ToInt(row[5])] = Strings::ToInt(row[1]); - loadti->ItemCost[Strings::ToInt(row[5])] = Strings::ToInt(row[4]); + const EQ::ItemData *item = database.GetItem(item_id); + if (!item) { + LogTrading("Unable to create item."); + return nullptr; } - return loadti; + + if (item->NoDrop == 0) { + return nullptr; + } + + std::unique_ptr inst( + database.CreateItem( + item_id, + charges, + results.at(0).aug_slot_1, + results.at(0).aug_slot_2, + results.at(0).aug_slot_3, + results.at(0).aug_slot_4, + results.at(0).aug_slot_5, + results.at(0).aug_slot_6 + ) + ); + if (!inst) { + LogTrading("Unable to create item instance."); + return nullptr; + } + + inst->SetCharges(charges); + inst->SetSerialNumber(serial_number); + inst->SetMerchantSlot(serial_number); + inst->SetPrice(cost); + + if (inst->IsStackable()) { + inst->SetMerchantCount(charges); + } + + return std::move(inst); } -TraderCharges_Struct* ZoneDatabase::LoadTraderItemWithCharges(uint32 char_id) -{ - auto loadti = new TraderCharges_Struct; - memset(loadti,0,sizeof(TraderCharges_Struct)); +void ZoneDatabase::UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 charges, uint32 new_price) { - std::string query = StringFormat("SELECT * FROM trader WHERE char_id=%i ORDER BY slot_id LIMIT 80", char_id); - auto results = QueryDatabase(query); - if (!results.Success()) { - LogTrading("Failed to load trader information!\n"); - return loadti; - } - - for (auto& row = results.begin(); row != results.end(); ++row) { - if (Strings::ToInt(row[5]) >= 80 || Strings::ToInt(row[5]) < 0) { - LogTrading("Bad Slot number when trying to load trader information!\n"); - continue; - } - - loadti->ItemID[Strings::ToInt(row[5])] = Strings::ToInt(row[1]); - loadti->SerialNumber[Strings::ToInt(row[5])] = Strings::ToInt(row[2]); - loadti->Charges[Strings::ToInt(row[5])] = Strings::ToInt(row[3]); - loadti->ItemCost[Strings::ToInt(row[5])] = Strings::ToInt(row[4]); - } - return loadti; -} - -EQ::ItemInstance* ZoneDatabase::LoadSingleTraderItem(uint32 CharID, int SerialNumber) { - std::string query = StringFormat("SELECT * FROM trader WHERE char_id = %i AND serialnumber = %i " - "ORDER BY slot_id LIMIT 80", CharID, SerialNumber); - auto results = QueryDatabase(query); - if (!results.Success()) - return nullptr; - - if (results.RowCount() == 0) { - LogTrading("Bad result from query\n"); fflush(stdout); - return nullptr; - } - - auto& row = results.begin(); - - int ItemID = Strings::ToInt(row[1]); - int Charges = Strings::ToInt(row[3]); - int Cost = Strings::ToInt(row[4]); - - const EQ::ItemData *item = database.GetItem(ItemID); + LogTrading("ZoneDatabase::UpdateTraderPrice([{}], [{}], [{}], [{}])", char_id, item_id, charges, new_price); + const EQ::ItemData *item = database.GetItem(item_id); if(!item) { - LogTrading("Unable to create item\n"); - fflush(stdout); - return nullptr; - } - - if (item->NoDrop == 0) - return nullptr; - - EQ::ItemInstance* inst = database.CreateItem(item); - if(!inst) { - LogTrading("Unable to create item instance\n"); - fflush(stdout); - return nullptr; - } - - inst->SetCharges(Charges); - inst->SetSerialNumber(SerialNumber); - inst->SetMerchantSlot(SerialNumber); - inst->SetPrice(Cost); - - if(inst->IsStackable()) - inst->SetMerchantCount(Charges); - - return inst; -} - -void ZoneDatabase::SaveTraderItem(uint32 CharID, uint32 ItemID, uint32 SerialNumber, int32 Charges, uint32 ItemCost, uint8 Slot){ - - std::string query = StringFormat("REPLACE INTO trader VALUES(%i, %i, %i, %i, %i, %i)", - CharID, ItemID, SerialNumber, Charges, ItemCost, Slot); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to save trader item: [{}] for char_id: [{}], the error was: [{}]\n", ItemID, CharID, results.ErrorMessage().c_str()); - -} - -void ZoneDatabase::UpdateTraderItemCharges(int CharID, uint32 SerialNumber, int32 Charges) { - LogTrading("ZoneDatabase::UpdateTraderItemCharges([{}], [{}], [{}])", CharID, SerialNumber, Charges); - - std::string query = StringFormat("UPDATE trader SET charges = %i WHERE char_id = %i AND serialnumber = %i", - Charges, CharID, SerialNumber); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to update charges for trader item: [{}] for char_id: [{}], the error was: [{}]\n", SerialNumber, CharID, results.ErrorMessage().c_str()); - -} - -void ZoneDatabase::UpdateTraderItemPrice(int CharID, uint32 ItemID, uint32 Charges, uint32 NewPrice) { - - LogTrading("ZoneDatabase::UpdateTraderPrice([{}], [{}], [{}], [{}])", CharID, ItemID, Charges, NewPrice); - - const EQ::ItemData *item = database.GetItem(ItemID); - - if(!item) return; + } - if(NewPrice == 0) { - LogTrading("Removing Trader items from the DB for CharID [{}], ItemID [{}]", CharID, ItemID); + if (new_price == 0) { + LogTrading("Removing Trader items from the DB for char_id [{}], item_id [{}]", char_id, item_id); - std::string query = StringFormat("DELETE FROM trader WHERE char_id = %i AND item_id = %i",CharID, ItemID); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to remove trader item(s): [{}] for char_id: [{}], the error was: [{}]\n", ItemID, CharID, results.ErrorMessage().c_str()); + auto results = TraderRepository::DeleteWhere( + database, + fmt::format( + "`char_id` = '{}' AND `item_id` = {}", + char_id, + item_id + ) + ); + if (!results) { + LogDebug("[CLIENT] Failed to remove trader item(s): [{}] for char_id: [{}]", + item_id, + char_id + ); + } return; } - if(!item->Stackable) { - std::string query = StringFormat("UPDATE trader SET item_cost = %i " - "WHERE char_id = %i AND item_id = %i AND charges=%i", - NewPrice, CharID, ItemID, Charges); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to update price for trader item: [{}] for char_id: [{}], the error was: [{}]\n", ItemID, CharID, results.ErrorMessage().c_str()); - - return; - } - - std::string query = StringFormat("UPDATE trader SET item_cost = %i " - "WHERE char_id = %i AND item_id = %i", - NewPrice, CharID, ItemID); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to update price for trader item: [{}] for char_id: [{}], the error was: [{}]\n", ItemID, CharID, results.ErrorMessage().c_str()); -} - -void ZoneDatabase::DeleteTraderItem(uint32 char_id){ - - if(char_id==0) { - const std::string query = "DELETE FROM trader"; - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to delete all trader items data, the error was: [{}]\n", results.ErrorMessage().c_str()); - - return; + if (!item->Stackable) { + auto results = TraderRepository::UpdateItem(database, char_id, new_price, item_id, charges); + if (!results) { + LogTrading( + "Failed to update price for trader item [{}] for char_id: [{}]", + item_id, + char_id + ); + } + return; } - std::string query = StringFormat("DELETE FROM trader WHERE char_id = %i", char_id); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to delete trader item data for char_id: [{}], the error was: [{}]\n", char_id, results.ErrorMessage().c_str()); - -} -void ZoneDatabase::DeleteTraderItem(uint32 CharID,uint16 SlotID) { - - std::string query = StringFormat("DELETE FROM trader WHERE char_id = %i And slot_id = %i", CharID, SlotID); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to delete trader item data for char_id: [{}], the error was: [{}]\n",CharID, results.ErrorMessage().c_str()); + auto results = TraderRepository::UpdateItem(database, char_id, new_price, item_id, 0); + if (!results) { + LogTrading( + "Failed to update price for trader item [{}] for char_id: [{}]", + item_id, + char_id + ); + } } void ZoneDatabase::DeleteBuyLines(uint32 CharID) { @@ -526,10 +456,22 @@ void ZoneDatabase::UpdateBuyLine(uint32 CharID, uint32 BuySlot, uint32 Quantity) return; } - std::string query = StringFormat("UPDATE buyer SET quantity = %i WHERE charid = %i AND buyslot = %i", Quantity, CharID, BuySlot); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to update quantity in buyslot [{}] for charid: [{}], the error was: [{}]\n", BuySlot, CharID, results.ErrorMessage().c_str()); + std::string query = StringFormat( + "UPDATE buyer SET quantity = %i WHERE charid = %i AND buyslot = %i", + Quantity, + CharID, + BuySlot + ); + + auto results = QueryDatabase(query); + if (!results.Success()) { + LogTrading( + "Failed to update quantity in buyslot [{}] for charid [{}], the error was [{}]\n", + BuySlot, + CharID, + results.ErrorMessage().c_str() + ); + } } @@ -630,6 +572,7 @@ bool ZoneDatabase::LoadCharacterData(uint32 character_id, PlayerProfile_Struct* pp->raidAutoconsent = e.raid_auto_consent; pp->guildAutoconsent = e.guild_auto_consent; pp->RestTimer = e.RestTimer; + pp->char_id = e.id; m_epp->aa_effects = e.e_aa_effects; m_epp->perAA = e.e_percent_to_aa; m_epp->expended_aa = e.e_expended_aa_spent; diff --git a/zone/zonedb.h b/zone/zonedb.h index 10e8a44ed..52b6c8504 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -389,11 +389,11 @@ public: /* Traders */ void SaveTraderItem(uint32 char_id,uint32 itemid,uint32 uniqueid, int32 charges,uint32 itemcost,uint8 slot); void UpdateTraderItemCharges(int char_id, uint32 ItemInstID, int32 charges); - void UpdateTraderItemPrice(int CharID, uint32 ItemID, uint32 Charges, uint32 NewPrice); + void UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 charges, uint32 new_price); void DeleteTraderItem(uint32 char_id); void DeleteTraderItem(uint32 char_id,uint16 slot_id); - EQ::ItemInstance* LoadSingleTraderItem(uint32 char_id, int uniqueid); + std::unique_ptr LoadSingleTraderItem(uint32 char_id, int serial_number); Trader_Struct* LoadTraderItem(uint32 char_id); TraderCharges_Struct* LoadTraderItemWithCharges(uint32 char_id);