diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 84b3921d4..3b06c0e1f 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -158,6 +158,7 @@ SET(repositories repositories/base/base_bugs_repository.h repositories/base/base_bug_reports_repository.h repositories/base/base_buyer_repository.h + repositories/base/base_buyer_trade_items_repository.h repositories/base/base_character_activities_repository.h repositories/base/base_character_alternate_abilities_repository.h repositories/base/base_character_alt_currency_repository.h @@ -339,7 +340,8 @@ SET(repositories repositories/books_repository.h repositories/bugs_repository.h repositories/bug_reports_repository.h - repositories/buyer_repository.h + repositories/buyer_buy_lines_repository.h + repositories/buyer_trade_items_repository.h repositories/character_activities_repository.h repositories/character_alternate_abilities_repository.h repositories/character_alt_currency_repository.h diff --git a/common/database.cpp b/common/database.cpp index 5a3e9ff66..187638fcc 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -78,6 +78,7 @@ #include "repositories/merchantlist_temp_repository.h" #include "repositories/bot_data_repository.h" #include "repositories/trader_repository.h" +#include "repositories/buyer_repository.h" extern Client client; @@ -2123,3 +2124,8 @@ void Database::ClearTraderDetails() { TraderRepository::Truncate(*this); } + +void Database::ClearBuyerDetails() +{ + BuyerRepository::DeleteBuyer(*this, 0); +} diff --git a/common/database.h b/common/database.h index 0e0f488aa..b33a6acc9 100644 --- a/common/database.h +++ b/common/database.h @@ -245,6 +245,7 @@ public: void PurgeAllDeletedDataBuckets(); void ClearGuildOnlineStatus(); void ClearTraderDetails(); + void ClearBuyerDetails(); /* Database Variables */ diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 9dbd4b1ad..df77a0179 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5660,6 +5660,63 @@ ALTER TABLE `trader` DROP PRIMARY KEY, ADD PRIMARY KEY (`id`), ADD INDEX `charid_slotid` (`char_id`, `slot_id`); +)" + }, + ManifestEntry{ + .version = 9281, + .description = "2024_06_24_update_buyer_support.sql", + .check = "SHOW COLUMNS FROM `buyer` LIKE 'id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `buyer` + ADD COLUMN `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + CHANGE COLUMN `charid` `char_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `id`, + ADD COLUMN `char_entity_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `char_id`, + ADD COLUMN `char_name` VARCHAR(64) NULL DEFAULT NULL AFTER `char_entity_id`, + ADD COLUMN `char_zone_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `char_name`, + ADD COLUMN `char_zone_instance_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `char_zone_id`, + ADD COLUMN `transaction_date` DATETIME NULL DEFAULT NULL AFTER `char_zone_instance_id`, + ADD COLUMN `welcome_message` VARCHAR(256) NULL DEFAULT NULL AFTER `transaction_date`, + DROP COLUMN `buyslot`, + DROP COLUMN `itemid`, + DROP COLUMN `itemname`, + DROP COLUMN `quantity`, + DROP COLUMN `price`, + DROP PRIMARY KEY, + ADD PRIMARY KEY (`id`) USING BTREE, + ADD INDEX `charid` (`char_id`); + +CREATE TABLE `buyer_buy_lines` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `buyer_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0', + `char_id` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `buy_slot_id` INT(11) NOT NULL DEFAULT '0', + `item_id` INT(11) NOT NULL DEFAULT '0', + `item_qty` INT(11) NOT NULL DEFAULT '0', + `item_price` INT(11) NOT NULL DEFAULT '0', + `item_icon` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `item_name` VARCHAR(64) NOT NULL DEFAULT '' COLLATE 'latin1_swedish_ci', + PRIMARY KEY (`id`) USING BTREE, + INDEX `buyerid_charid_buyslotid` (`buyer_id`, `char_id`, `buy_slot_id`) USING BTREE +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +AUTO_INCREMENT=1; + +CREATE TABLE `buyer_trade_items` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `buyer_buy_lines_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0', + `item_id` INT(11) NOT NULL DEFAULT '0', + `item_qty` INT(11) NOT NULL DEFAULT '0', + `item_icon` INT(11) NOT NULL DEFAULT '0', + `item_name` VARCHAR(64) NOT NULL DEFAULT '0' COLLATE 'latin1_swedish_ci', + PRIMARY KEY (`id`) USING BTREE, + INDEX `buyerbuylinesid` (`buyer_buy_lines_id`) USING BTREE +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +AUTO_INCREMENT=1; )" } // -- template; copy/paste this when you need to create a new entry diff --git a/common/database_schema.h b/common/database_schema.h index 61bdb5bea..e0b87b94d 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -36,7 +36,6 @@ namespace DatabaseSchema { { return { {"adventure_stats", "player_id"}, - {"buyer", "charid"}, {"char_recipe_list", "char_id"}, {"character_activities", "charid"}, {"character_alt_currency", "char_id"}, @@ -107,6 +106,8 @@ namespace DatabaseSchema { "adventure_details", "adventure_stats", "buyer", + "buyer_buy_lines", + "buyer_trade_items", "char_recipe_list", "character_activities", "character_alt_currency", @@ -325,6 +326,9 @@ namespace DatabaseSchema { "banned_ips", "bug_reports", "bugs", + "buyer", + "buyer_buy_lines", + "buyer_trade_items", "completed_shared_task_activity_state", "completed_shared_task_members", "completed_shared_tasks", diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 0d09c6bdf..b58e45827 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -67,6 +67,7 @@ N(OP_Buff), N(OP_BuffCreate), N(OP_BuffRemoveRequest), N(OP_Bug), +N(OP_BuyerItems), N(OP_CameraEffect), N(OP_Camp), N(OP_CancelSneakHide), diff --git a/common/eq_constants.h b/common/eq_constants.h index cc9099aa7..6bd4beef5 100644 --- a/common/eq_constants.h +++ b/common/eq_constants.h @@ -1129,4 +1129,7 @@ enum ExpSource #define PARCEL_LIMIT 5 #define PARCEL_BEGIN_SLOT 1 +namespace DoorType { + constexpr uint32 BuyerStall = 155; +} #endif /*COMMON_EQ_CONSTANTS_H*/ diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 19d89b9fe..e9b80aaf0 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -323,6 +323,7 @@ union bool show_name; bool guild_show; bool trader; + bool buyer; }; struct PlayerState_Struct { @@ -3139,60 +3140,369 @@ struct BazaarSearchResults_Struct { // Barter/Buyer // // -enum { - Barter_BuyerSearch = 0, - Barter_SellerSearch = 1, - Barter_BuyerModeOn = 2, - Barter_BuyerModeOff = 3, - Barter_BuyerItemUpdate = 5, - Barter_BuyerItemRemove = 6, - Barter_SellItem = 7, +#define MAX_BUYER_COMPENSATION_ITEMS 10 + +enum BarterBuyerActions { + Barter_BuyerSearch = 0, + Barter_SellerSearch = 1, + Barter_BuyerModeOn = 2, + Barter_BuyerModeOff = 3, + Barter_BuyerItemStart = 4, + Barter_BuyerItemUpdate = 5, + Barter_BuyerItemRemove = 6, + Barter_SellItem = 7, Barter_SellerTransactionComplete = 8, - Barter_BuyerTransactionComplete = 9, - Barter_BuyerInspectBegin = 10, - Barter_BuyerInspectEnd = 11, - Barter_BuyerAppearance = 12, - Barter_BuyerInspectWindow = 13, - Barter_BarterItemInspect = 14, - Barter_SellerBrowsing = 15, - Barter_BuyerSearchResults = 16, - Barter_Welcome = 17, - Barter_WelcomeMessageUpdate = 19, - Barter_BuyerItemInspect = 21, - Barter_Unknown23 = 23 + Barter_BuyerTransactionComplete = 9, + Barter_BuyerInspectBegin = 10, + Barter_BuyerInspectEnd = 11, + Barter_BuyerAppearance = 12, + Barter_BuyerInspectWindow = 13, + Barter_BarterItemInspect = 14, + Barter_SellerBrowsing = 15, + Barter_BuyerSearchResults = 16, + Barter_Welcome = 17, + Barter_WelcomeMessageUpdate = 19, + Barter_Greeting = 20, + Barter_BuyerItemInspect = 21, + Barter_OpenBarterWindow = 23, + Barter_AddToBarterWindow = 26, + Barter_RemoveFromBarterWindow = 27, + Barter_RemoveFromMerchantWindow = 50, //Not a client item. Used for internal communications. + Barter_FailedTransaction = 51, + Barter_BuyerCouldNotBeFound = 52, + Barter_FailedBuyerChecks = 53, + Barter_SellerCouldNotBeFound = 54, + Barter_FailedSellerChecks = 55 +}; + +enum BarterBuyerSubActions { + Barter_Success = 0, + Barter_Failure = 1, + Barter_DataOutOfDate = 4, + Barter_SellerDoesNotHaveItem = 6, + Barter_SameZone = 8 +}; + +enum BuyerBarter { + Off = 0, + On = 1 +}; + +struct BuyerRemoveItem_Struct { + uint32 action; + uint32 buy_slot_id; +}; + +struct BuyerRemoveItemFromMerchantWindow_Struct { + uint32 action; + uint32 unknown_004; + uint32 buy_slot_id; + uint32 unknown_012; +}; + +struct BuyerGeneric_Struct { + uint32 action; + char payload[]; +}; + +struct BuyerMessaging_Struct { + uint32 action; + uint32 sub_action; + uint32 zone_id; + uint32 buyer_id; + uint32 buyer_entity_id; + char buyer_name[64]; + uint32 buy_item_id; + uint32 buy_item_qty; + uint64 buy_item_cost; + uint32 buy_item_icon; + uint32 seller_entity_id; + char seller_name[64]; + char item_name[64]; + uint32 slot; + uint32 seller_quantity; +}; + +struct BuyerAddBuyertoBarterWindow_Struct { + uint32 action; + uint32 zone_id; + uint32 buyer_id; + uint32 buyer_entity_id; + char buyer_name[64]; +}; + +struct BuyerRemoveBuyerFromBarterWindow_Struct { + uint32 action; + uint32 buyer_id; +}; + +struct BuyerBrowsing_Struct { + uint32 action; + char char_name[64]; +}; + +struct BuyerGreeting_Struct { + uint32 action; + uint32 buyer_id; }; struct BuyerWelcomeMessageUpdate_Struct { -/*000*/ uint32 Action; -/*004*/ char WelcomeMessage[256]; + uint32 action; + char welcome_message[256]; }; -struct BuyerItemSearch_Struct { -/*000*/ uint32 Unknown000; -/*004*/ char SearchString[64]; +struct BuyerLineTradeItems_Struct { + uint32 item_id; + uint32 item_quantity; + uint32 item_icon; + std::string item_name; + + void operator*=(uint32 multiplier) + { + this->item_quantity *= multiplier; + } + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(item_id), + CEREAL_NVP(item_quantity), + CEREAL_NVP(item_icon), + CEREAL_NVP(item_name) + ); + } }; -struct BuyerItemSearchResultEntry_Struct { -/*000*/ char ItemName[64]; -/*064*/ uint32 ItemID; -/*068*/ uint32 Unknown068; -/*072*/ uint32 Unknown072; +struct BuyerLineItems_Struct { + uint32 slot; + uint8 enabled; + uint32 item_id; + std::string item_name; + uint32 item_icon; + uint32 item_quantity; + uint8 item_toggle; + uint32 item_cost; + std::vector trade_items; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(slot), + CEREAL_NVP(enabled), + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(item_icon), + CEREAL_NVP(item_quantity), + CEREAL_NVP(item_toggle), + CEREAL_NVP(item_cost), + CEREAL_NVP(trade_items) + ); + } }; -#define MAX_BUYER_ITEMSEARCH_RESULTS 200 +struct BuyerBuyLines_Struct { + uint32 action; + union { + uint32 no_items; + uint32 string_length; + }; + std::vector buy_lines; -struct BuyerItemSearchResults_Struct { - uint32 Action; - uint32 ResultCount; - BuyerItemSearchResultEntry_Struct Results[MAX_BUYER_ITEMSEARCH_RESULTS]; + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(no_items), + CEREAL_NVP(buy_lines) + ); + } +}; + +struct BuyerLineSellItem_Struct { + uint32 action; + uint32 sub_action; + uint32 error_code; + uint32 purchase_method; // 0 direct merchant, 1 via /barter window + uint32 buyer_entity_id; + uint32 buyer_id; + std::string buyer_name; + uint32 seller_entity_id; + std::string seller_name; + uint32 slot; + uint8 enabled; + uint32 item_id; + char item_name[64]; + uint32 item_icon; + uint32 item_quantity; + uint8 item_toggle; + uint32 item_cost; + uint32 no_trade_items; + std::vector trade_items; + uint32 seller_quantity; + uint32 zone_id; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(sub_action), + CEREAL_NVP(error_code), + CEREAL_NVP(purchase_method), + CEREAL_NVP(buyer_entity_id), + CEREAL_NVP(buyer_id), + CEREAL_NVP(buyer_name), + CEREAL_NVP(seller_entity_id), + CEREAL_NVP(seller_name), + CEREAL_NVP(slot), + CEREAL_NVP(enabled), + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(item_icon), + CEREAL_NVP(item_quantity), + CEREAL_NVP(item_toggle), + CEREAL_NVP(item_cost), + CEREAL_NVP(no_trade_items), + CEREAL_NVP(trade_items), + CEREAL_NVP(seller_quantity), + CEREAL_NVP(zone_id) + ); + } +}; + +struct BuyerLineItemsSearch_Struct { + uint32 slot; + uint8 enabled; + uint32 item_id; + char item_name[64]; + uint32 item_icon; + uint32 item_quantity; + uint8 item_toggle; + uint32 item_cost; + uint32 buyer_id; + uint32 buyer_entity_id; + uint32 buyer_zone_id; + uint32 buyer_zone_instance_id; + std::string buyer_name; + std::vector trade_items; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(slot), + CEREAL_NVP(enabled), + CEREAL_NVP(item_id), + CEREAL_NVP(item_name), + CEREAL_NVP(item_icon), + CEREAL_NVP(item_quantity), + CEREAL_NVP(item_toggle), + CEREAL_NVP(item_cost), + CEREAL_NVP(buyer_id), + CEREAL_NVP(buyer_entity_id), + CEREAL_NVP(buyer_zone_id), + CEREAL_NVP(buyer_zone_instance_id), + CEREAL_NVP(buyer_name), + CEREAL_NVP(trade_items) + ); + } +}; + +struct BuyerLineSearch_Struct { + uint32 action; + uint32 no_items; + std::string search_string; + uint32 transaction_id; + std::vector buy_line; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(no_items), + CEREAL_NVP(search_string), + CEREAL_NVP(transaction_id), + CEREAL_NVP(buy_line) + ); + } +}; + +struct BuyerSetAppearance_Struct { + uint32 action; + uint32 entity_id; + uint32 status; // 0 off 1 on + char buyer_name[64]; +}; + +struct BarterItemSearchLinkRequest_Struct { + uint32 action; + uint32 searcher_id; + uint32 unknown_008; + uint32 unknown_012; + uint32 item_id; + uint32 unknown_020; +}; + +struct BuyerInspectRequest_Struct { + uint32 action; + uint32 buyer_id; + uint32 approval; }; struct BarterSearchRequest_Struct { - uint32 Action; - char SearchString[64]; - uint32 SearchID; + uint32 action; + char search_string[64]; + uint32 transaction_id; + uint32 unknown_072; + uint32 buyer_id; + uint8 search_scope; //0 All Buyers, 1 Local Buyers + uint16 zone_id; }; +struct BuyerItemSearch_Struct { + uint32 action; + char search_string[64]; +}; + +struct BuyerItemSearchResultEntry_Struct { + char item_name[64]; + uint32 item_id; + uint32 item_icon; + uint32 unknown_072; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(item_name), + CEREAL_NVP(item_id), + CEREAL_NVP(item_icon), + CEREAL_NVP(unknown_072) + ); + } +}; + +struct BuyerItemSearchResults_Struct { + uint32 action; + uint32 result_count; + std::vector results; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(result_count), + CEREAL_NVP(results) + ); + } +}; + +//old below here struct BuyerItemSearchLinkRequest_Struct { /*000*/ uint32 Action; // 0x00000015 /*004*/ uint32 ItemID; @@ -3200,31 +3510,6 @@ struct BuyerItemSearchLinkRequest_Struct { /*012*/ uint32 Unknown012; }; -struct BarterItemSearchLinkRequest_Struct { -/*000*/ uint32 Action; // 0x0000000E -/*004*/ uint32 SearcherID; -/*008*/ uint32 Unknown008; -/*012*/ uint32 Unknown012; -/*016*/ uint32 ItemID; -/*020*/ uint32 Unknown020; -}; - -struct BuyerInspectRequest_Struct { - uint32 Action; - uint32 BuyerID; - uint32 Approval; -}; - -struct BuyerBrowsing_Struct { - uint32 Action; - char PlayerName[64]; -}; - -struct BuyerRemoveItem_Struct { - uint32 Action; - uint32 BuySlot; -}; - struct ServerSideFilters_Struct { uint8 clientattackfilters; // 0) No, 1) All (players) but self, 2) All (players) but group uint8 npcattackfilters; // 0) No, 1) Ignore NPC misses (all), 2) Ignore NPC Misses + Attacks (all but self), 3) Ignores NPC Misses + Attacks (all but group) @@ -6066,9 +6351,12 @@ enum BazaarTraderBarterActions { }; enum BazaarPurchaseActions { - ByVendor = 0, - ByParcel = 1, - ByDirectToInventory = 2 + BazaarByVendor = 0, + BazaarByParcel = 1, + BazaarByDirectToInventory = 2, + BarterByVendor = 0, + BarterInBazaar = 1, + BarterOutsideBazaar = 2 }; enum BazaarPurchaseSubActions { @@ -6142,6 +6430,11 @@ struct BazaarSearchMessaging_Struct { } }; +struct BuylineItemDetails_Struct { + uint64 item_cost; + uint32 item_quantity; +}; + // Restore structure packing to default #pragma pack() diff --git a/common/events/player_event_logs.cpp b/common/events/player_event_logs.cpp index f526fff05..8f095d79f 100644 --- a/common/events/player_event_logs.cpp +++ b/common/events/player_event_logs.cpp @@ -706,6 +706,7 @@ void PlayerEventLogs::SetSettingsDefaults() m_settings[PlayerEvent::PARCEL_SEND].event_enabled = 1; m_settings[PlayerEvent::PARCEL_RETRIEVE].event_enabled = 1; m_settings[PlayerEvent::PARCEL_DELETE].event_enabled = 1; + m_settings[PlayerEvent::BARTER_TRANSACTION].event_enabled = 1; for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { m_settings[i].retention_days = RETENTION_DAYS_DEFAULT; diff --git a/common/events/player_events.h b/common/events/player_events.h index 16440ad9a..da518a458 100644 --- a/common/events/player_events.h +++ b/common/events/player_events.h @@ -60,7 +60,8 @@ namespace PlayerEvent { GUILD_TRIBUTE_DONATE_PLAT, PARCEL_SEND, PARCEL_RETRIEVE, - PARCEL_DELETE, + PARCEL_DELETE, + BARTER_TRANSACTION, MAX // dont remove }; @@ -122,7 +123,8 @@ namespace PlayerEvent { "Guild Tribute Donate Platinum", "Parcel Item Sent", "Parcel Item Retrieved", - "Parcel Prune Routine" + "Parcel Prune Routine", + "Barter Transaction" }; // Generic struct used by all events @@ -1081,6 +1083,32 @@ namespace PlayerEvent { ); } }; + + struct BarterTransaction { + std::string status; + uint32 item_id; + uint32 item_quantity; + std::string item_name; + std::vector trade_items; + std::string buyer_name; + std::string seller_name; + uint64 total_cost; + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(status), + CEREAL_NVP(item_id), + CEREAL_NVP(item_quantity), + CEREAL_NVP(item_name), + CEREAL_NVP(trade_items), + CEREAL_NVP(buyer_name), + CEREAL_NVP(seller_name), + CEREAL_NVP(total_cost) + ); + } + }; } #endif //EQEMU_PLAYER_EVENTS_H diff --git a/common/inventory_profile.cpp b/common/inventory_profile.cpp index b07ce8f74..df81e516c 100644 --- a/common/inventory_profile.cpp +++ b/common/inventory_profile.cpp @@ -1743,3 +1743,68 @@ std::vector EQ::InventoryProfile::GetAugmentIDsBySlotID(int16 slot_id) return augments; } + +std::vector EQ::InventoryProfile::FindAllFreeSlotsThatFitItem(const EQ::ItemData *item_data) +{ + std::vector free_slots{}; + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + if ((((uint64) 1 << i) & GetLookup()->PossessionsBitmask) == 0) { + continue; + } + + EQ::ItemInstance *inv_item = GetItem(i); + + if (!inv_item) { + // Found available slot in personal inventory + free_slots.push_back(i); + } + + if (inv_item->IsClassBag() && + EQ::InventoryProfile::CanItemFitInContainer(item_data, inv_item->GetItem())) { + + int16 base_slot_id = EQ::InventoryProfile::CalcSlotId(i, EQ::invbag::SLOT_BEGIN); + uint8 bag_size = inv_item->GetItem()->BagSlots; + + for (uint8 bag_slot = EQ::invbag::SLOT_BEGIN; bag_slot < bag_size; bag_slot++) { + auto bag_item = GetItem(base_slot_id + bag_slot); + if (!bag_item) { + // Found available slot within bag + free_slots.push_back(i); + } + } + } + } + return free_slots; +} + +int16 EQ::InventoryProfile::FindFirstFreeSlotThatFitsItem(const EQ::ItemData *item_data) +{ + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + if ((((uint64) 1 << i) & GetLookup()->PossessionsBitmask) == 0) { + continue; + } + + EQ::ItemInstance *inv_item = GetItem(i); + + if (!inv_item) { + // Found available slot in personal inventory + return i; + } + + if (inv_item->IsClassBag() && + EQ::InventoryProfile::CanItemFitInContainer(item_data, inv_item->GetItem())) { + + int16 base_slot_id = EQ::InventoryProfile::CalcSlotId(i, EQ::invbag::SLOT_BEGIN); + uint8 bag_size = inv_item->GetItem()->BagSlots; + + for (uint8 bag_slot = EQ::invbag::SLOT_BEGIN; bag_slot < bag_size; bag_slot++) { + auto bag_item = GetItem(base_slot_id + bag_slot); + if (!bag_item) { + // Found available slot within bag + return base_slot_id + bag_slot; + } + } + } + } + return 0; +} \ No newline at end of file diff --git a/common/inventory_profile.h b/common/inventory_profile.h index 42dba9619..100d5ebd8 100644 --- a/common/inventory_profile.h +++ b/common/inventory_profile.h @@ -176,6 +176,8 @@ namespace EQ // Locate an available inventory slot int16 FindFreeSlot(bool for_bag, bool try_cursor, uint8 min_size = 0, bool is_arrow = false); int16 FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start = invslot::GENERAL_BEGIN, uint8 bag_start = invbag::SLOT_BEGIN); + std::vector FindAllFreeSlotsThatFitItem(const EQ::ItemData *inst); + int16 FindFirstFreeSlotThatFitsItem(const EQ::ItemData *inst); // Calculate slot_id for an item within a bag static int16 CalcSlotId(int16 slot_id); // Calc parent bag's slot_id diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 5d8ebcd3e..9024f93b9 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -356,41 +356,91 @@ namespace RoF2 ENCODE(OP_Barter) { - EQApplicationPacket *in = *p; + EQApplicationPacket *in = *p; *p = nullptr; - char *Buffer = (char *)in->pBuffer; + char *buffer = (char *) in->pBuffer; + uint32 sub_action = VARSTRUCT_DECODE_TYPE(uint32, buffer); - uint32 SubAction = VARSTRUCT_DECODE_TYPE(uint32, Buffer); + switch (sub_action) { + case Barter_BuyerAppearance: { + auto emu = (BuyerInspectRequest_Struct *) in->pBuffer; - if (SubAction != Barter_BuyerAppearance) - { - dest->FastQueuePacket(&in, ack_req); + auto outapp = new EQApplicationPacket(OP_Barter, sizeof(structs::Buyer_SetAppearance_Struct)); + auto eq = (structs::Buyer_SetAppearance_Struct *) outapp->pBuffer; - return; + eq->action = structs::RoF2BuyerActions::BuyerAppearance; + eq->entity_id = emu->buyer_id; + eq->enabled = emu->approval; + + dest->FastQueuePacket(&outapp); + safe_delete(in); + + break; + } + case Barter_BuyerItemRemove: { + auto emu = (BuyerRemoveItem_Struct *) in->pBuffer; + + auto outapp = new EQApplicationPacket(OP_BuyerItems, sizeof(structs::BuyerRemoveItem_Struct)); + auto eq = (structs::BuyerRemoveItem_Struct *) outapp->pBuffer; + + eq->action = structs::RoF2BuyerActions::BuyerModifyBuyLine; + eq->slot_id = emu->buy_slot_id; + eq->toggle = 0; + + dest->FastQueuePacket(&outapp); + safe_delete(in); + + break; + } + case Barter_BuyerInspectBegin: { + *(uint32 *) in->pBuffer = structs::RoF2BuyerActions::BuyerInspectBegin; + dest->FastQueuePacket(&in); + break; + } + case Barter_BuyerInspectEnd: { + *(uint32 *) in->pBuffer = structs::RoF2BuyerActions::BuyerInspectEnd; + dest->FastQueuePacket(&in); + break; + } + case Barter_SellerBrowsing: { + *(uint32 *) in->pBuffer = structs::RoF2BuyerActions::BuyerBrowsingBuyLine; + dest->FastQueuePacket(&in); + break; + } + case Barter_BuyerSearchResults: { + BuyerItemSearchResults_Struct bisr{}; + auto emu = (BuyerGeneric_Struct *) in->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(emu->payload), + in->size - sizeof(BuyerGeneric_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(bisr); + + LogTradingDetail("Sending item search results [{}]", bisr.result_count); + + uint32 packet_size = bisr.result_count * sizeof(structs::BuyerItemSearchResultEntry_Struct) + 8; + auto outapp = std::make_unique(OP_Barter, packet_size); + auto eq = (char *) outapp->pBuffer; + + VARSTRUCT_ENCODE_TYPE(uint32, eq, structs::RoF2BuyerActions::BuyerSearchResults); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bisr.result_count); + for (auto const &i: bisr.results) { + strn0cpy(eq, i.item_name, 64); + eq += 64; + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_id); + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_icon); + VARSTRUCT_SKIP_TYPE(uint32, eq); + } + dest->QueuePacket(outapp.get()); + break; + } + default: { + LogTradingDetail("Unhandled action [{}]", sub_action); + dest->FastQueuePacket(&in); + } } - - unsigned char *__emu_buffer = in->pBuffer; - - in->size = 80; - - in->pBuffer = new unsigned char[in->size]; - - char *OutBuffer = (char *)in->pBuffer; - - char Name[64]; - - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, SubAction); - uint32 EntityID = VARSTRUCT_DECODE_TYPE(uint32, Buffer); - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, EntityID); - uint8 Toggle = VARSTRUCT_DECODE_TYPE(uint8, Buffer); - VARSTRUCT_DECODE_STRING(Name, Buffer); - VARSTRUCT_ENCODE_STRING(OutBuffer, Name); - OutBuffer = (char *)in->pBuffer + 72; - VARSTRUCT_ENCODE_TYPE(uint8, OutBuffer, Toggle); - - delete[] __emu_buffer; - dest->FastQueuePacket(&in, ack_req); } ENCODE(OP_BazaarSearch) @@ -682,6 +732,243 @@ namespace RoF2 FINISH_ENCODE(); } + ENCODE(OP_BuyerItems) + { + EQApplicationPacket *inapp = *p; + *p = nullptr; + + auto action = *(uint32 *) inapp->pBuffer; + + switch (action) { + case Barter_BuyerItemUpdate: { + BuyerLineItems_Struct bl{}; + auto emu = (BuyerGeneric_Struct *) inapp->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(emu->payload), + inapp->size - sizeof(BuyerGeneric_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(bl); + + //packet size + auto packet_size = bl.item_name.length() + 1 + 34; + for (auto const &b: bl.trade_items) { + packet_size += b.item_name.length() + 1; + packet_size += 12; + } + + auto outapp = std::make_unique(OP_BuyerItems, packet_size); + char *eq = (char *) outapp->pBuffer; + auto no_trade_items = bl.trade_items.size(); + + VARSTRUCT_ENCODE_TYPE(uint32, eq, structs::RoF2BuyerActions::BuyerModifyBuyLine); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.slot); + VARSTRUCT_ENCODE_TYPE(uint8, eq, bl.enabled ? 1 : 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.item_id); + VARSTRUCT_ENCODE_STRING(eq, bl.item_name.c_str()); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.item_icon); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.item_quantity); + VARSTRUCT_ENCODE_TYPE(uint8, eq, bl.item_toggle ? 1 : 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.item_cost); + VARSTRUCT_ENCODE_TYPE(uint32, eq, no_trade_items); + + for (int i = 0; i < no_trade_items; i++) { + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.trade_items[i].item_id); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.trade_items[i].item_quantity); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bl.trade_items[i].item_icon); + VARSTRUCT_ENCODE_STRING(eq, bl.trade_items[i].item_name.c_str()); + } + dest->QueuePacket(outapp.get()); + safe_delete(inapp); + break; + } + case Barter_BuyerInspectBegin: { + auto emu = (BuyerGeneric_Struct *) inapp->pBuffer; + + BuyerLineItems_Struct bli{}; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(emu->payload), + inapp->size - sizeof(BuyerGeneric_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(bli); + + //packet size + auto packet_size = bli.item_name.length() + 1 + 34; + for (auto const &b: bli.trade_items) { + packet_size += b.item_name.length() + 1; + packet_size += 12; + } + + auto packet = std::make_unique(OP_BuyerItems, packet_size); + char *eq = (char *) packet->pBuffer; + auto no_trade_items = bli.trade_items.size(); + + VARSTRUCT_ENCODE_TYPE(uint32, eq, structs::RoF2BuyerActions::BuyerSendBuyLine); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bli.slot); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bli.slot); + VARSTRUCT_ENCODE_TYPE(uint8, eq, bli.enabled ? 1 : 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bli.item_id); + VARSTRUCT_ENCODE_STRING(eq, bli.item_name.c_str()); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bli.item_icon); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bli.item_quantity); + VARSTRUCT_ENCODE_TYPE(uint8, eq, bli.item_toggle ? 1 : 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bli.item_cost); + VARSTRUCT_ENCODE_TYPE(uint32, eq, no_trade_items); + + for (auto const &i: bli.trade_items) { + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_id); + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_quantity); + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_icon); + VARSTRUCT_ENCODE_STRING(eq, i.item_name.c_str()); + } + dest->QueuePacket(packet.get()); + safe_delete(inapp); + + break; + } + case Barter_BuyerSearch: { + BuyerLineSearch_Struct bls{}; + auto emu = (BuyerGeneric_Struct *) inapp->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(emu->payload), + inapp->size - sizeof(BuyerGeneric_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(bls); + LogTrading("(RoF2) Barter_BuyerSearch action [{}]", emu->action); + + //Calculate size of packet + auto p_size = 0; + p_size += 5 * sizeof(uint32) + 1 * sizeof(uint8); + p_size += bls.search_string.length() + 1; + for (auto const &b: bls.buy_line) { + p_size += 6 * sizeof(uint32) + 2 * sizeof(uint8); + p_size += strlen(b.item_name) + 1; + p_size += b.buyer_name.length() + 1; + for (auto const &d: b.trade_items) { + if (d.item_id != 0) { + p_size += d.item_name.length() + 1; + p_size += 3 * sizeof(uint32); + } + } + p_size += 3 * sizeof(uint32); + } + + BuyerBuyLines_Struct bl{}; + auto outapp = std::make_unique(OP_BuyerItems, p_size); + auto eq = (char *) outapp->pBuffer; + + VARSTRUCT_ENCODE_TYPE(uint32, eq, 1); + VARSTRUCT_ENCODE_STRING(eq, bls.search_string.c_str()); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bls.transaction_id); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint8, eq, 1); + VARSTRUCT_ENCODE_TYPE(uint32, eq, bls.no_items); + for (auto const &b: bls.buy_line) { + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.slot); + VARSTRUCT_ENCODE_TYPE(uint8, eq, 1); + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.item_id); + VARSTRUCT_ENCODE_STRING(eq, b.item_name); + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.item_icon); + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.item_quantity); + VARSTRUCT_ENCODE_TYPE(uint8, eq, 1); + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.item_cost); + auto no_sub_items = b.trade_items.size(); + VARSTRUCT_ENCODE_TYPE(uint32, eq, no_sub_items); + for (auto const &i: b.trade_items) { + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_id); + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_quantity); + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_icon); + VARSTRUCT_ENCODE_STRING(eq, i.item_name.c_str()); + } + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.buyer_entity_id); + VARSTRUCT_ENCODE_TYPE(uint32, eq, b.buyer_id); + VARSTRUCT_ENCODE_TYPE(uint16, eq, b.buyer_zone_id); + VARSTRUCT_ENCODE_TYPE(uint16, eq, b.buyer_zone_instance_id); + VARSTRUCT_ENCODE_STRING(eq, b.buyer_name.c_str()); + } + dest->QueuePacket(outapp.get()); + break; + } + case Barter_RemoveFromMerchantWindow: { + auto emu = (BuyerRemoveItemFromMerchantWindow_Struct *) inapp->pBuffer; + + emu->action = structs::RoF2BuyerActions::BuyerSendBuyLine; + dest->FastQueuePacket(&inapp); + break; + } + case Barter_BuyerTransactionComplete: + case Barter_SellerTransactionComplete: { + BuyerLineSellItem_Struct blsi{}; + auto emu = (BuyerGeneric_Struct *) inapp->pBuffer; + EQ::Util::MemoryStreamReader ss( + reinterpret_cast(emu->payload), + inapp->size - sizeof(BuyerGeneric_Struct) + ); + cereal::BinaryInputArchive ar(ss); + ar(blsi); + + //packet size + auto packet_size = strlen(blsi.item_name) * 2 + 2 + 48 + 30 + blsi.seller_name.length() + 1 + + blsi.buyer_name.length() + 1; + for (auto const &b: blsi.trade_items) { + packet_size += b.item_name.length() + 1; + packet_size += 12; + } + + auto outapp = std::make_unique(OP_BuyerItems, packet_size); + auto eq = (char *) outapp->pBuffer; + + switch (action) { + case Barter_BuyerTransactionComplete: { + VARSTRUCT_ENCODE_TYPE(uint32, eq, structs::RoF2BuyerActions::BuyerBuyItem); + break; + } + case Barter_SellerTransactionComplete: { + VARSTRUCT_ENCODE_TYPE(uint32, eq, structs::RoF2BuyerActions::BuyerSellItem); + break; + } + } + VARSTRUCT_ENCODE_TYPE(uint32, eq, blsi.sub_action); + VARSTRUCT_ENCODE_TYPE(uint32, eq, blsi.error_code); + eq += 16; + VARSTRUCT_ENCODE_STRING(eq, blsi.buyer_name.c_str()); + VARSTRUCT_ENCODE_STRING(eq, blsi.item_name); + VARSTRUCT_ENCODE_STRING(eq, blsi.seller_name.c_str()); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0xFFFFFFFF); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0xFFFFFFFF); + eq += 1; + VARSTRUCT_ENCODE_STRING(eq, blsi.item_name); + eq += 9; + VARSTRUCT_ENCODE_TYPE(uint32, eq, blsi.item_cost); + VARSTRUCT_ENCODE_TYPE(uint32, eq, blsi.trade_items.size()); + + for (auto const &i: blsi.trade_items) { + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, i.item_quantity); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_STRING(eq, i.item_name.c_str()); + } + + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0xFFFFFF); + VARSTRUCT_ENCODE_TYPE(uint32, eq, 0); + VARSTRUCT_ENCODE_TYPE(uint32, eq, blsi.seller_quantity); + + dest->QueuePacket(outapp.get()); + break; + } + default: { + dest->FastQueuePacket(&inapp); + } + } + } + ENCODE(OP_CancelTrade) { ENCODE_LENGTH_EXACT(CancelTrade_Struct); @@ -1690,7 +1977,7 @@ namespace RoF2 uchar *__emu_buffer = in->pBuffer; ItemPacket_Struct *old_item_pkt = (ItemPacket_Struct *) __emu_buffer; - switch(old_item_pkt->PacketType) + switch(old_item_pkt->PacketType) { case ItemPacketParcel: { ParcelMessaging_Struct pms{}; @@ -4348,6 +4635,9 @@ namespace RoF2 if (emu->DestructibleObject) { OtherData = OtherData | 0xe1; // Live has 0xe1 for OtherData } + if (emu->buyer) { + OtherData = OtherData | 0x01; + } VARSTRUCT_ENCODE_TYPE(uint8, Buffer, OtherData); // float EmitterScalingRadius @@ -4459,7 +4749,7 @@ namespace RoF2 VARSTRUCT_ENCODE_STRING(Buffer, emu->lastName); VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // aatitle - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->guild_show); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu->guild_show); VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // TempPet VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->petOwnerId); @@ -4664,6 +4954,66 @@ namespace RoF2 FINISH_DIRECT_DECODE(); } + DECODE(OP_Barter) + { + auto action = *(uint32 *) __packet->pBuffer; + + switch (action) { + case structs::RoF2BuyerActions::BuyerRemoveItem: { + auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; + emu->action = Barter_BuyerItemRemove; + LogTradingDetail("(RoF2) Buyer Remove Item"); + + break; + } + case structs::RoF2BuyerActions::BuyerInspectBegin: { + LogTradingDetail("(RoF2) Buyer Inspect Begin Item"); + + auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; + emu->action = Barter_BuyerInspectBegin; + + break; + } + case structs::RoF2BuyerActions::BuyerInspectEnd: { + LogTradingDetail("(RoF2) Buyer Inspect End Item "); + + auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; + emu->action = Barter_BuyerInspectEnd; + + break; + } + case structs::RoF2BuyerActions::BuyerWelcomeMessage: { + LogTradingDetail("(RoF2) Buyer Welcome Message Update"); + SETUP_DIRECT_DECODE(BuyerWelcomeMessageUpdate_Struct, structs::BuyerWelcomeMessageUpdate_Struct); + + emu->action = Barter_WelcomeMessageUpdate; + strn0cpy(emu->welcome_message, eq->welcome_message, sizeof(emu->welcome_message)); + + FINISH_DIRECT_DECODE(); + break; + } + case structs::RoF2BuyerActions::BuyerItemInspect: { + SETUP_DIRECT_DECODE(BarterItemSearchLinkRequest_Struct, structs::BarterItemSearchLinkRequest_Struct); + LogTradingDetail("(RoF2) Seller ID [{}] Inspecting Item [{}] from Buyer ID [{}] ", + eq->seller_id, + eq->item_id, + eq->buyer_id + ); + + emu->action = Barter_BarterItemInspect; + emu->item_id = eq->item_id; + emu->searcher_id = eq->seller_id; + + FINISH_DIRECT_DECODE(); + break; + } + default: { + auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; + LogTradingDetail("(RoF2) Pass thru OP_Barter packet action [{}]", emu->action); + } + } + } + DECODE(OP_BazaarSearch) { char *Buffer = (char *)__packet->pBuffer; @@ -4742,6 +5092,147 @@ namespace RoF2 FINISH_DIRECT_DECODE(); } + DECODE(OP_BuyerItems) + { + auto action = *(uint32 *) __packet->pBuffer; + + switch (action) { + case structs::RoF2BuyerActions::BuyerModifyBuyLine: + case structs::RoF2BuyerActions::BuyerBuyLine: { + BuyerBuyLines_Struct buyer_buy_lines{}; + auto buffer = (char *) __packet->pBuffer; + + buyer_buy_lines.action = VARSTRUCT_DECODE_TYPE(uint32, buffer); + + buyer_buy_lines.no_items = 1; + if (action == structs::RoF2BuyerActions::BuyerBuyLine) { + buyer_buy_lines.no_items = VARSTRUCT_DECODE_TYPE(uint16, buffer); + } + + buyer_buy_lines.buy_lines.reserve(buyer_buy_lines.no_items); + for (int i = 0; i < buyer_buy_lines.no_items; i++) { + BuyerLineItems_Struct b{}; + b.slot = VARSTRUCT_DECODE_TYPE(uint32, buffer); + b.enabled = VARSTRUCT_DECODE_TYPE(uint8, buffer); + b.item_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + b.item_name = std::string(buffer, strlen(buffer)); + buffer += strlen(buffer) + 1; + b.item_icon = VARSTRUCT_DECODE_TYPE(uint32, buffer); + b.item_quantity = VARSTRUCT_DECODE_TYPE(uint32, buffer); + b.item_toggle = VARSTRUCT_DECODE_TYPE(uint8, buffer); + b.item_cost = VARSTRUCT_DECODE_TYPE(uint32, buffer); + auto trade_items = VARSTRUCT_DECODE_TYPE(uint32, buffer); + buyer_buy_lines.buy_lines.push_back(b); + + if (trade_items > 0) { + buyer_buy_lines.buy_lines[i].trade_items.reserve(trade_items); + for (int x = 0; x < trade_items; x++) { + BuyerLineTradeItems_Struct blti{}; + blti.item_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + blti.item_quantity = VARSTRUCT_DECODE_TYPE(uint32, buffer); + blti.item_icon = VARSTRUCT_DECODE_TYPE(uint32, buffer); + blti.item_name = std::string(buffer, strlen(buffer)); + buffer += strlen(buffer) + 1; + buyer_buy_lines.buy_lines[i].trade_items.push_back(blti); + } + } + buffer += 13; + } + + buffer = nullptr; + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + { + ar(buyer_buy_lines); + } + + auto new_size = sizeof(BuyerGeneric_Struct) + ss.str().length(); + auto new_packet = new unsigned char[new_size]; + __packet->size = new_size; + __packet->pBuffer = new_packet; + auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; + emu->action = Barter_BuyerItemUpdate; + + if (action == structs::RoF2BuyerActions::BuyerBuyLine) { + emu->action = Barter_BuyerItemStart; + } + + memcpy(emu->payload, ss.str().data(), ss.str().length()); + __packet->SetOpcode(OP_Barter); + + break; + } + case structs::RoF2BuyerActions::BuyerSellItem: { + BuyerLineSellItem_Struct sell_item{}; + + char *buffer = (char *) __packet->pBuffer; + + sell_item.action = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.purchase_method = VARSTRUCT_DECODE_TYPE(uint32, buffer); + buffer += 4; + sell_item.buyer_entity_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.buyer_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + buffer += 11; + sell_item.slot = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.enabled = VARSTRUCT_DECODE_TYPE(uint8, buffer); + sell_item.item_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + VARSTRUCT_DECODE_STRING(sell_item.item_name, buffer); + sell_item.item_icon = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.item_quantity = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.item_toggle = VARSTRUCT_DECODE_TYPE(uint8, buffer); + sell_item.item_cost = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.no_trade_items = VARSTRUCT_DECODE_TYPE(uint32, buffer); + + if (sell_item.no_trade_items > 0) { + sell_item.trade_items.reserve(sell_item.no_trade_items); + for (int x = 0; x < sell_item.no_trade_items; x++) { + BuyerLineTradeItems_Struct blti{}; + blti.item_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + blti.item_quantity = VARSTRUCT_DECODE_TYPE(uint32, buffer); + blti.item_icon = VARSTRUCT_DECODE_TYPE(uint32, buffer); + blti.item_name = std::string(buffer, strlen(buffer)); + buffer += strlen(buffer) + 1; + sell_item.trade_items.push_back(blti); + } + } + + if (sell_item.purchase_method) { + sell_item.buyer_entity_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.buyer_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.zone_id = VARSTRUCT_DECODE_TYPE(uint32, buffer); + sell_item.buyer_name = std::string(buffer, strlen(buffer)); + buffer += sell_item.buyer_name.length() + 1; + } + else { + buffer += 13; + } + + sell_item.seller_quantity = VARSTRUCT_DECODE_TYPE(uint32, buffer); + + buffer += 4; + + buffer = nullptr; + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + { + ar(sell_item); + } + + auto new_size = sizeof(BuyerGeneric_Struct) + ss.str().length(); + auto new_packet = new unsigned char[new_size]; + __packet->size = new_size; + __packet->pBuffer = new_packet; + auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; + emu->action = Barter_SellItem; + + memcpy(emu->payload, ss.str().data(), ss.str().length()); + __packet->SetOpcode(OP_Barter); + + break; + } + } + } + DECODE(OP_CastSpell) { DECODE_LENGTH_EXACT(structs::CastSpell_Struct); diff --git a/common/patches/rof2_ops.h b/common/patches/rof2_ops.h index a775c06d7..08d8b9985 100644 --- a/common/patches/rof2_ops.h +++ b/common/patches/rof2_ops.h @@ -48,6 +48,7 @@ E(OP_BeginCast) E(OP_BlockedBuffs) E(OP_Buff) E(OP_BuffCreate) +E(OP_BuyerItems) E(OP_CancelTrade) E(OP_CastSpell) E(OP_ChannelMessage) @@ -149,11 +150,13 @@ D(OP_Animation) D(OP_ApplyPoison) D(OP_AugmentInfo) D(OP_AugmentItem) +D(OP_Barter) D(OP_BazaarSearch) D(OP_BlockedBuffs) D(OP_BookButton) D(OP_Buff) D(OP_BuffRemoveRequest) +D(OP_BuyerItems) D(OP_CastSpell) D(OP_ChannelMessage) D(OP_CharacterCreate) diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 30ced24a0..fa43b65b3 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -354,15 +354,15 @@ struct Spawn_Struct_Bitfields /*29*/ unsigned showname:1; /*30*/ unsigned idleanimationsoff:1; // what we called statue? /*31*/ unsigned untargetable:1; // bClickThrough -/* do these later -32 unsigned buyer:1; -33 unsigned offline:1; -34 unsigned interactiveobject:1; -35 unsigned flung:1; // hmm this vfunc appears to do stuff with leve and flung variables -36 unsigned title:1; -37 unsigned suffix:1; -38 unsigned padding1:1; -39 unsigned padding2:1; + // byte 5 +/*32 unsigned buyer:1; +/*33 unsigned offline:1; +/*34 unsigned interactiveobject:1; +/*35 unsigned flung:1; // hmm this vfunc appears to do stuff with leve and flung variables +/*36 unsigned title:1; +/*37 unsigned suffix:1; +/*38 unsigned padding1:1; +/*39 unsigned padding2:1; 40 unsinged padding3:1; */ /* @@ -3107,19 +3107,139 @@ 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 + 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 RoF2BuyerActions { + BuyerSearchResults = 0x00, + BuyerBuyLine = 0x06, + BuyerModifyBuyLine = 0x07, + BuyerRemoveItem = 0x08, + BuyerSellItem = 0x09, + BuyerBuyItem = 0x0a, + BuyerInspectBegin = 0x0b, + BuyerInspectEnd = 0x0c, + BuyerAppearance = 0x0d, + BuyerSendBuyLine = 0x0e, + BuyerItemInspect = 0x0f, + BuyerBrowsingBuyLine = 0x10, + BarterWelcomeMessage = 0x11, + BuyerWelcomeMessage = 0x13, + BuyerGreeting = 0x14, + BuyerInventoryFull = 0x16 +}; + +struct BarterItemSearchLinkRequest_Struct { + uint32 action; + uint32 unknown_004; + uint32 seller_id; + uint32 buyer_id; + uint32 unknown_016; + uint32 slot_id; // 0xffffffff main buy line 0x0 trade_item_1, 0x1 trade_item_2 + uint32 item_id; + uint32 unknown_028; +}; + +struct BuyerWelcomeMessageUpdate_Struct { + uint32 action; + char unknown_004[64]; + uint32 unknown_068; + char welcome_message[256]; +}; + +struct Buyer_SetAppearance_Struct { + uint32 action; + uint32 entity_id; + char unknown[64]; + uint32 enabled; +}; + +struct BuyerRemoveItem_Struct { + uint32 action; + uint32 unknown004; + uint32 slot_id; + uint32 toggle; +}; + +struct BuyerLineSellItem_Struct { + uint32 action; + uint32 purchase_method; // 0 direct merchant, 1 via /barter window + uint32 unknown008; + uint32 buyer_entity_id; + uint32 seller_entity_id; + char unknown[15]; + uint32 slot; + uint8 enabled; + uint32 item_id; + char item_name[64]; + uint32 item_icon; + uint32 item_quantity; + uint8 item_toggle; + uint32 item_cost; + uint32 no_trade_items; + BuyerLineTradeItems_Struct trade_items[10]; + char unknown2[13]; + uint32 seller_quantity; +}; + +struct BuyerLineItemsSearch_Struct { + uint32 slot; + uint8 enabled; + uint32 item_id; + char item_name[64]; + uint32 item_icon; + uint32 item_quantity; + uint8 item_toggle; + uint32 item_cost; + uint32 buyer_id; + BuyerLineTradeItems_Struct trade_items[MAX_BUYER_COMPENSATION_ITEMS]; +}; + +struct BuyerLineSearch_Struct { + uint32 action; + uint32 no_items; + std::vector buy_line; +}; + +struct BuyerStart_Struct { + uint32 action; + uint16 no_buyer_lines; + uint32 slot; + uint8 enabled; + uint32 item_id; + char item_name[1]; // vary length + uint32 item_icon; + uint32 item_quantity; + uint8 toggle; + uint32 item_cost; + uint32 no_trade_items; + BuyerLineTradeItems_Struct trade_items[1]; // size is actually no_trade_items. If 0, then this is not in packet + char unknown[13]; +}; + +struct BuyerItemSearchResultEntry_Struct { + char item_name[64]; + uint32 item_id; + uint32 item_icon; + uint32 unknown_072; +}; + +struct BuyerItemSearchResults_Struct { + uint32 action; + uint32 result_count; + BuyerItemSearchResultEntry_Struct results[]; }; enum { diff --git a/common/repositories/base/base_buyer_buy_lines_repository.h b/common/repositories/base/base_buyer_buy_lines_repository.h new file mode 100644 index 000000000..5c041a289 --- /dev/null +++ b/common/repositories/base/base_buyer_buy_lines_repository.h @@ -0,0 +1,475 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_BUYER_BUY_LINES_REPOSITORY_H +#define EQEMU_BASE_BUYER_BUY_LINES_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseBuyerBuyLinesRepository { +public: + struct BuyerBuyLines { + uint64_t id; + uint64_t buyer_id; + uint32_t char_id; + int32_t buy_slot_id; + int32_t item_id; + int32_t item_qty; + int32_t item_price; + uint32_t item_icon; + std::string item_name; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "buyer_id", + "char_id", + "buy_slot_id", + "item_id", + "item_qty", + "item_price", + "item_icon", + "item_name", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "buyer_id", + "char_id", + "buy_slot_id", + "item_id", + "item_qty", + "item_price", + "item_icon", + "item_name", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("buyer_buy_lines"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static BuyerBuyLines NewEntity() + { + BuyerBuyLines e{}; + + e.id = 0; + e.buyer_id = 0; + e.char_id = 0; + e.buy_slot_id = 0; + e.item_id = 0; + e.item_qty = 0; + e.item_price = 0; + e.item_icon = 0; + e.item_name = ""; + + return e; + } + + static BuyerBuyLines GetBuyerBuyLines( + const std::vector &buyer_buy_liness, + int buyer_buy_lines_id + ) + { + for (auto &buyer_buy_lines : buyer_buy_liness) { + if (buyer_buy_lines.id == buyer_buy_lines_id) { + return buyer_buy_lines; + } + } + + return NewEntity(); + } + + static BuyerBuyLines FindOne( + Database& db, + int buyer_buy_lines_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + buyer_buy_lines_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + BuyerBuyLines e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.char_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.buy_slot_id = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_id = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_qty = row[5] ? static_cast(atoi(row[5])) : 0; + e.item_price = row[6] ? static_cast(atoi(row[6])) : 0; + e.item_icon = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.item_name = row[8] ? row[8] : ""; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int buyer_buy_lines_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + buyer_buy_lines_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const BuyerBuyLines &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[1] + " = " + std::to_string(e.buyer_id)); + v.push_back(columns[2] + " = " + std::to_string(e.char_id)); + v.push_back(columns[3] + " = " + std::to_string(e.buy_slot_id)); + v.push_back(columns[4] + " = " + std::to_string(e.item_id)); + v.push_back(columns[5] + " = " + std::to_string(e.item_qty)); + v.push_back(columns[6] + " = " + std::to_string(e.item_price)); + v.push_back(columns[7] + " = " + std::to_string(e.item_icon)); + v.push_back(columns[8] + " = '" + Strings::Escape(e.item_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static BuyerBuyLines InsertOne( + Database& db, + BuyerBuyLines e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.buy_slot_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_price)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.buy_slot_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_price)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BuyerBuyLines e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.char_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.buy_slot_id = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_id = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_qty = row[5] ? static_cast(atoi(row[5])) : 0; + e.item_price = row[6] ? static_cast(atoi(row[6])) : 0; + e.item_icon = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.item_name = row[8] ? row[8] : ""; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BuyerBuyLines e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.char_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.buy_slot_id = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_id = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_qty = row[5] ? static_cast(atoi(row[5])) : 0; + e.item_price = row[6] ? static_cast(atoi(row[6])) : 0; + e.item_icon = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.item_name = row[8] ? row[8] : ""; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const BuyerBuyLines &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.buy_slot_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_price)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.buy_slot_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_price)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_BUYER_BUY_LINES_REPOSITORY_H diff --git a/common/repositories/base/base_buyer_repository.h b/common/repositories/base/base_buyer_repository.h index b3953b63d..3dc52b140 100644 --- a/common/repositories/base/base_buyer_repository.h +++ b/common/repositories/base/base_buyer_repository.h @@ -19,40 +19,46 @@ class BaseBuyerRepository { public: struct Buyer { - int32_t charid; - int32_t buyslot; - int32_t itemid; - std::string itemname; - int32_t quantity; - int32_t price; + uint64_t id; + uint32_t char_id; + uint32_t char_entity_id; + std::string char_name; + uint32_t char_zone_id; + uint32_t char_zone_instance_id; + time_t transaction_date; + std::string welcome_message; }; static std::string PrimaryKey() { - return std::string("charid"); + return std::string("id"); } static std::vector Columns() { return { - "charid", - "buyslot", - "itemid", - "itemname", - "quantity", - "price", + "id", + "char_id", + "char_entity_id", + "char_name", + "char_zone_id", + "char_zone_instance_id", + "transaction_date", + "welcome_message", }; } static std::vector SelectColumns() { return { - "charid", - "buyslot", - "itemid", - "itemname", - "quantity", - "price", + "id", + "char_id", + "char_entity_id", + "char_name", + "char_zone_id", + "char_zone_instance_id", + "UNIX_TIMESTAMP(transaction_date)", + "welcome_message", }; } @@ -93,12 +99,14 @@ public: { Buyer e{}; - e.charid = 0; - e.buyslot = 0; - e.itemid = 0; - e.itemname = ""; - e.quantity = 0; - e.price = 0; + e.id = 0; + e.char_id = 0; + e.char_entity_id = 0; + e.char_name = ""; + e.char_zone_id = 0; + e.char_zone_instance_id = 0; + e.transaction_date = 0; + e.welcome_message = ""; return e; } @@ -109,7 +117,7 @@ public: ) { for (auto &buyer : buyers) { - if (buyer.charid == buyer_id) { + if (buyer.id == buyer_id) { return buyer; } } @@ -135,12 +143,14 @@ public: if (results.RowCount() == 1) { Buyer e{}; - e.charid = row[0] ? static_cast(atoi(row[0])) : 0; - e.buyslot = row[1] ? static_cast(atoi(row[1])) : 0; - e.itemid = row[2] ? static_cast(atoi(row[2])) : 0; - e.itemname = row[3] ? row[3] : ""; - e.quantity = row[4] ? static_cast(atoi(row[4])) : 0; - e.price = row[5] ? static_cast(atoi(row[5])) : 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.char_entity_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.char_name = row[3] ? row[3] : ""; + e.char_zone_id = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.char_zone_instance_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.transaction_date = strtoll(row[6] ? row[6] : "-1", nullptr, 10); + e.welcome_message = row[7] ? row[7] : ""; return e; } @@ -174,12 +184,13 @@ public: auto columns = Columns(); - v.push_back(columns[0] + " = " + std::to_string(e.charid)); - v.push_back(columns[1] + " = " + std::to_string(e.buyslot)); - v.push_back(columns[2] + " = " + std::to_string(e.itemid)); - v.push_back(columns[3] + " = '" + Strings::Escape(e.itemname) + "'"); - v.push_back(columns[4] + " = " + std::to_string(e.quantity)); - v.push_back(columns[5] + " = " + std::to_string(e.price)); + v.push_back(columns[1] + " = " + std::to_string(e.char_id)); + v.push_back(columns[2] + " = " + std::to_string(e.char_entity_id)); + v.push_back(columns[3] + " = '" + Strings::Escape(e.char_name) + "'"); + v.push_back(columns[4] + " = " + std::to_string(e.char_zone_id)); + v.push_back(columns[5] + " = " + std::to_string(e.char_zone_instance_id)); + v.push_back(columns[6] + " = FROM_UNIXTIME(" + (e.transaction_date > 0 ? std::to_string(e.transaction_date) : "null") + ")"); + v.push_back(columns[7] + " = '" + Strings::Escape(e.welcome_message) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -187,7 +198,7 @@ public: TableName(), Strings::Implode(", ", v), PrimaryKey(), - e.charid + e.id ) ); @@ -201,12 +212,14 @@ public: { std::vector v; - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.buyslot)); - v.push_back(std::to_string(e.itemid)); - v.push_back("'" + Strings::Escape(e.itemname) + "'"); - v.push_back(std::to_string(e.quantity)); - v.push_back(std::to_string(e.price)); + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back("'" + Strings::Escape(e.char_name) + "'"); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.char_zone_instance_id)); + v.push_back("FROM_UNIXTIME(" + (e.transaction_date > 0 ? std::to_string(e.transaction_date) : "null") + ")"); + v.push_back("'" + Strings::Escape(e.welcome_message) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -217,7 +230,7 @@ public: ); if (results.Success()) { - e.charid = results.LastInsertedID(); + e.id = results.LastInsertedID(); return e; } @@ -236,12 +249,14 @@ public: for (auto &e: entries) { std::vector v; - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.buyslot)); - v.push_back(std::to_string(e.itemid)); - v.push_back("'" + Strings::Escape(e.itemname) + "'"); - v.push_back(std::to_string(e.quantity)); - v.push_back(std::to_string(e.price)); + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back("'" + Strings::Escape(e.char_name) + "'"); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.char_zone_instance_id)); + v.push_back("FROM_UNIXTIME(" + (e.transaction_date > 0 ? std::to_string(e.transaction_date) : "null") + ")"); + v.push_back("'" + Strings::Escape(e.welcome_message) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -275,12 +290,14 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { Buyer e{}; - e.charid = row[0] ? static_cast(atoi(row[0])) : 0; - e.buyslot = row[1] ? static_cast(atoi(row[1])) : 0; - e.itemid = row[2] ? static_cast(atoi(row[2])) : 0; - e.itemname = row[3] ? row[3] : ""; - e.quantity = row[4] ? static_cast(atoi(row[4])) : 0; - e.price = row[5] ? static_cast(atoi(row[5])) : 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.char_entity_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.char_name = row[3] ? row[3] : ""; + e.char_zone_id = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.char_zone_instance_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.transaction_date = strtoll(row[6] ? row[6] : "-1", nullptr, 10); + e.welcome_message = row[7] ? row[7] : ""; all_entries.push_back(e); } @@ -305,12 +322,14 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { Buyer e{}; - e.charid = row[0] ? static_cast(atoi(row[0])) : 0; - e.buyslot = row[1] ? static_cast(atoi(row[1])) : 0; - e.itemid = row[2] ? static_cast(atoi(row[2])) : 0; - e.itemname = row[3] ? row[3] : ""; - e.quantity = row[4] ? static_cast(atoi(row[4])) : 0; - e.price = row[5] ? static_cast(atoi(row[5])) : 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.char_entity_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.char_name = row[3] ? row[3] : ""; + e.char_zone_id = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.char_zone_instance_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.transaction_date = strtoll(row[6] ? row[6] : "-1", nullptr, 10); + e.welcome_message = row[7] ? row[7] : ""; all_entries.push_back(e); } @@ -385,12 +404,14 @@ public: { std::vector v; - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.buyslot)); - v.push_back(std::to_string(e.itemid)); - v.push_back("'" + Strings::Escape(e.itemname) + "'"); - v.push_back(std::to_string(e.quantity)); - v.push_back(std::to_string(e.price)); + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back("'" + Strings::Escape(e.char_name) + "'"); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.char_zone_instance_id)); + v.push_back("FROM_UNIXTIME(" + (e.transaction_date > 0 ? std::to_string(e.transaction_date) : "null") + ")"); + v.push_back("'" + Strings::Escape(e.welcome_message) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -413,12 +434,14 @@ public: for (auto &e: entries) { std::vector v; - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.buyslot)); - v.push_back(std::to_string(e.itemid)); - v.push_back("'" + Strings::Escape(e.itemname) + "'"); - v.push_back(std::to_string(e.quantity)); - v.push_back(std::to_string(e.price)); + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.char_entity_id)); + v.push_back("'" + Strings::Escape(e.char_name) + "'"); + v.push_back(std::to_string(e.char_zone_id)); + v.push_back(std::to_string(e.char_zone_instance_id)); + v.push_back("FROM_UNIXTIME(" + (e.transaction_date > 0 ? std::to_string(e.transaction_date) : "null") + ")"); + v.push_back("'" + Strings::Escape(e.welcome_message) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/base/base_buyer_trade_items_repository.h b/common/repositories/base/base_buyer_trade_items_repository.h new file mode 100644 index 000000000..4ef1e9459 --- /dev/null +++ b/common/repositories/base/base_buyer_trade_items_repository.h @@ -0,0 +1,439 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_BUYER_TRADE_ITEMS_REPOSITORY_H +#define EQEMU_BASE_BUYER_TRADE_ITEMS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseBuyerTradeItemsRepository { +public: + struct BuyerTradeItems { + uint64_t id; + uint64_t buyer_buy_lines_id; + int32_t item_id; + int32_t item_qty; + int32_t item_icon; + std::string item_name; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "buyer_buy_lines_id", + "item_id", + "item_qty", + "item_icon", + "item_name", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "buyer_buy_lines_id", + "item_id", + "item_qty", + "item_icon", + "item_name", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("buyer_trade_items"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static BuyerTradeItems NewEntity() + { + BuyerTradeItems e{}; + + e.id = 0; + e.buyer_buy_lines_id = 0; + e.item_id = 0; + e.item_qty = 0; + e.item_icon = 0; + e.item_name = "0"; + + return e; + } + + static BuyerTradeItems GetBuyerTradeItems( + const std::vector &buyer_trade_itemss, + int buyer_trade_items_id + ) + { + for (auto &buyer_trade_items : buyer_trade_itemss) { + if (buyer_trade_items.id == buyer_trade_items_id) { + return buyer_trade_items; + } + } + + return NewEntity(); + } + + static BuyerTradeItems FindOne( + Database& db, + int buyer_trade_items_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + buyer_trade_items_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + BuyerTradeItems e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_buy_lines_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.item_id = row[2] ? static_cast(atoi(row[2])) : 0; + e.item_qty = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_icon = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_name = row[5] ? row[5] : "0"; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int buyer_trade_items_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + buyer_trade_items_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const BuyerTradeItems &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[1] + " = " + std::to_string(e.buyer_buy_lines_id)); + v.push_back(columns[2] + " = " + std::to_string(e.item_id)); + v.push_back(columns[3] + " = " + std::to_string(e.item_qty)); + v.push_back(columns[4] + " = " + std::to_string(e.item_icon)); + v.push_back(columns[5] + " = '" + Strings::Escape(e.item_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static BuyerTradeItems InsertOne( + Database& db, + BuyerTradeItems e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_buy_lines_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_buy_lines_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BuyerTradeItems e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_buy_lines_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.item_id = row[2] ? static_cast(atoi(row[2])) : 0; + e.item_qty = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_icon = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_name = row[5] ? row[5] : "0"; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BuyerTradeItems e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_buy_lines_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.item_id = row[2] ? static_cast(atoi(row[2])) : 0; + e.item_qty = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_icon = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_name = row[5] ? row[5] : "0"; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const BuyerTradeItems &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_buy_lines_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.buyer_buy_lines_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.item_qty)); + v.push_back(std::to_string(e.item_icon)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_BUYER_TRADE_ITEMS_REPOSITORY_H diff --git a/common/repositories/buyer_buy_lines_repository.h b/common/repositories/buyer_buy_lines_repository.h new file mode 100644 index 000000000..bb470cccd --- /dev/null +++ b/common/repositories/buyer_buy_lines_repository.h @@ -0,0 +1,356 @@ +#ifndef EQEMU_BUYER_BUY_LINES_REPOSITORY_H +#define EQEMU_BUYER_BUY_LINES_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_buyer_buy_lines_repository.h" +#include "buyer_trade_items_repository.h" +#include "character_data_repository.h" +#include "buyer_repository.h" + +#include "../eq_packet_structs.h" + +class BuyerBuyLinesRepository: public BaseBuyerBuyLinesRepository { +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 + * + * BuyerBuyLinesRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * BuyerBuyLinesRepository::GetWhereNeverExpires() + * BuyerBuyLinesRepository::GetWhereXAndY() + * BuyerBuyLinesRepository::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 + + struct WelcomeData_Struct { + uint32 count_of_buyers; + uint32 count_of_items; + }; + + static int CreateBuyLine(Database& db, const BuyerLineItems_Struct b, uint32 char_id) + { + auto buyer = BuyerRepository::GetWhere(db, fmt::format("`char_id` = '{}' LIMIT 1", char_id)); + if (buyer.empty()){ + return 0; + } + + BuyerBuyLinesRepository::BuyerBuyLines buy_lines{}; + buy_lines.id = 0; + buy_lines.buyer_id = buyer.front().id; + buy_lines.char_id = char_id; + buy_lines.buy_slot_id = b.slot; + buy_lines.item_id = b.item_id; + buy_lines.item_name = b.item_name; + buy_lines.item_icon = b.item_icon; + buy_lines.item_qty = b.item_quantity; + buy_lines.item_price = b.item_cost; + auto e = InsertOne(db, buy_lines); + + std::vector queue; + + for (auto const &r: b.trade_items) { + BuyerTradeItemsRepository::BuyerTradeItems bti{}; + bti.id = 0; + bti.buyer_buy_lines_id = e.id; + bti.item_id = r.item_id; + bti.item_qty = r.item_quantity; + bti.item_icon = r.item_icon; + bti.item_name = r.item_name; + + if (bti.item_id) { + queue.push_back(bti); + } + } + + if (!queue.empty()) { + BuyerTradeItemsRepository::InsertMany(db, queue); + } + + return e.id; + } + + static int ModifyBuyLine(Database& db, const BuyerLineItems_Struct b, uint32 char_id) + { + auto b_lines = GetWhere(db, fmt::format("`char_id` = '{}' AND `buy_slot_id` = '{}';", char_id, b.slot)); + if (b_lines.empty() || b_lines.size() > 1){ + return 0; + } + + auto b_line = b_lines.front(); + + b_line.item_qty = b.item_quantity; + b_line.item_price = b.item_cost; + b_line.item_icon = b.item_icon; + auto e = UpdateOne(db, b_line); + + std::vector queue; + + BuyerTradeItemsRepository::DeleteWhere(db, fmt::format("`buyer_buy_lines_id` = '{}';", b_line.id)); + for (auto const &r: b.trade_items) { + BuyerTradeItemsRepository::BuyerTradeItems bti{}; + bti.id = 0; + bti.buyer_buy_lines_id = b_line.id; + bti.item_id = r.item_id; + bti.item_qty = r.item_quantity; + bti.item_icon = r.item_icon; + bti.item_name = r.item_name; + + if (bti.item_id) { + queue.push_back(bti); + } + } + + if (!queue.empty()) { + BuyerTradeItemsRepository::InsertMany(db, queue); + } + + return 1; + } + + static bool DeleteBuyLine(Database &db, uint32 char_id, int32 slot_id = 0xffffffff) + { + std::vector buylines{}; + if (slot_id == 0xffffffff) { + auto buylines = GetWhere(db, fmt::format("`char_id` = '{}'", char_id)); + DeleteWhere(db, fmt::format("`char_id` = '{}'", char_id)); + } + else { + auto buylines = GetWhere(db, fmt::format("`char_id` = '{}' AND `buy_slot_id` = '{}'", char_id, slot_id)); + DeleteWhere(db, fmt::format("`char_id` = '{}' AND `buy_slot_id` = '{}'", char_id, slot_id)); + } + + if (buylines.empty()) { + return 0; + } + + std::vector buyline_ids{}; + for (auto const &bl: buylines) { + buyline_ids.push_back((std::to_string(bl.id))); + } + + if (!buyline_ids.empty()) { + BuyerTradeItemsRepository::DeleteWhere( + db, + fmt::format( + "`buyer_buy_lines_id` IN({})", + Strings::Implode(", ", buyline_ids) + ) + ); + } + + return 1; + } + + static std::vector GetBuyLines(Database &db, uint32 char_id) + { + std::vector all_entries{}; + + auto buy_line = GetWhere(db, fmt::format("`char_id` = '{}';", char_id)); + if (buy_line.empty()){ + return all_entries; + } + + auto buy_line_trade_items = BuyerTradeItemsRepository::GetWhere( + db, + fmt::format( + "`buyer_buy_lines_id` IN (SELECT b.id FROM buyer_buy_lines AS b WHERE b.char_id = '{}')", + char_id + ) + ); + + all_entries.reserve(buy_line.size()); + + for (auto const &l: buy_line) { + BuyerLineItems_Struct bli{}; + bli.item_id = l.item_id; + bli.item_cost = l.item_price; + bli.item_quantity = l.item_qty; + bli.slot = l.buy_slot_id; + bli.item_name = l.item_name; + + for (auto const &i: GetSubIDs(buy_line_trade_items, l.id)) { + BuyerLineTradeItems_Struct blti{}; + blti.item_id = i.item_id; + blti.item_icon = i.item_icon; + blti.item_quantity = i.item_qty; + blti.item_id = i.item_id; + blti.item_name = i.item_name; + bli.trade_items.push_back(blti); + } + all_entries.push_back(bli); + } + + return all_entries; + } + + static BuyerLineSearch_Struct SearchBuyLines( + Database &db, + std::string &search_string, + uint32 char_id = 0, + uint32 zone_id = 0, + uint32 zone_instance_id = 0 + ) + { + BuyerLineSearch_Struct all_entries{}; + std::string where_clause(fmt::format("`item_name` LIKE \"%{}%\" ", search_string)); + + if (char_id) { + where_clause += fmt::format("AND `char_id` = '{}' ", char_id); + } + + if (zone_id) { + auto buyers = BuyerRepository::GetWhere( + db, + fmt::format( + "`char_zone_id` = '{}' AND char_zone_instance_id = '{}'", + zone_id, + zone_instance_id + ) + ); + + std::vector char_ids{}; + for (auto const &bl : buyers) { + char_ids.push_back((std::to_string(bl.char_id))); + } + + where_clause += fmt::format("AND `char_id` IN ({}) ", Strings::Implode(", ", char_ids)); + } + + auto buy_line = GetWhere(db, where_clause); + if (buy_line.empty()){ + return all_entries; + } + + std::vector ids{}; + std::vector char_ids{}; + for (auto const &bl : buy_line) { + if (std::find(ids.begin(), ids.end(), std::to_string(bl.id)) == std::end(ids)) { + ids.push_back(std::to_string(bl.id)); + } + if (std::find(char_ids.begin(), char_ids.end(), std::to_string(bl.char_id)) == std::end(char_ids)) { + char_ids.push_back((std::to_string(bl.char_id))); + } + } + + auto buy_line_trade_items = BuyerTradeItemsRepository::GetWhere( + db, + fmt::format( + "`buyer_buy_lines_id` IN ({});", + Strings::Implode(", ", ids) + ) + ); + + auto char_names = BuyerRepository::GetWhere( + db, + fmt::format( + "`char_id` IN ({});", + Strings::Implode(", ", char_ids) + ) + ); + + all_entries.no_items = buy_line.size(); + for (auto const &l: buy_line) { + BuyerLineItemsSearch_Struct blis{}; + blis.slot = l.buy_slot_id; + blis.item_id = l.item_id; + blis.item_cost = l.item_price; + blis.item_icon = l.item_icon; + blis.item_quantity = l.item_qty; + blis.buyer_id = l.char_id; + auto it = std::find_if( + char_names.cbegin(), + char_names.cend(), + [&](BuyerRepository::Buyer e) { return e.char_id == l.char_id; } + ); + blis.buyer_name = it != char_names.end() ? it->char_name : std::string(""); + blis.buyer_entity_id = it != char_names.end() ? it->char_entity_id : 0; + blis.buyer_zone_id = it != char_names.end() ? it->char_zone_id : 0; + blis.buyer_zone_instance_id = it != char_names.end() ? it->char_zone_instance_id : 0; + strn0cpy(blis.item_name, l.item_name.c_str(), sizeof(blis.item_name)); + + for (auto const &i: GetSubIDs(buy_line_trade_items, l.id)) { + BuyerLineTradeItems_Struct e{}; + e.item_id = i.item_id; + e.item_icon = i.item_icon; + e.item_quantity = i.item_qty; + e.item_id = i.item_id; + e.item_name = i.item_name; + + blis.trade_items.push_back(e); + } + all_entries.buy_line.push_back(blis); + } + + return all_entries; + } + + static std::vector + GetSubIDs(std::vector &in, uint64 id) + { + std::vector results{}; + std::vector indices{}; + + auto it = in.begin(); + while ((it = std::find_if( + it, + in.end(), + [&](BuyerTradeItemsRepository::BuyerTradeItems const &e) { + return e.buyer_buy_lines_id == id; + } + )) + != in.end() + ) { + indices.push_back(std::distance(in.begin(), it)); + results.push_back(*it); + it++; + } + return results; + } + + static WelcomeData_Struct GetWelcomeData(Database &db) + { + WelcomeData_Struct e{}; + + auto results = db.QueryDatabase("SELECT COUNT(DISTINCT char_id), COUNT(char_id) FROM buyer;"); + + if (!results.RowCount()) { + return e; + } + + auto r = results.begin(); + e.count_of_buyers = Strings::ToInt(r[0]); + e.count_of_items = Strings::ToInt(r[1]); + return e; + } + +}; + +#endif //EQEMU_BUYER_BUY_LINES_REPOSITORY_H diff --git a/common/repositories/buyer_repository.h b/common/repositories/buyer_repository.h index 7c3b39840..d20751b0a 100644 --- a/common/repositories/buyer_repository.h +++ b/common/repositories/buyer_repository.h @@ -4,6 +4,8 @@ #include "../database.h" #include "../strings.h" #include "base/base_buyer_repository.h" +#include "base/base_buyer_trade_items_repository.h" +#include "base/base_buyer_buy_lines_repository.h" class BuyerRepository: public BaseBuyerRepository { public: @@ -45,6 +47,93 @@ public: // Custom extended repository methods here + static bool UpdateWelcomeMessage(Database& db, uint32 char_id, const char *message) { + + auto const b = GetWhere(db, fmt::format("`char_id` = '{}';", char_id)); + + if (b.empty()) { + return false; + } + + auto buyer = b.front(); + buyer.welcome_message = message; + return UpdateOne(db, buyer); + } + + static std::string GetWelcomeMessage(Database& db, uint32 char_id) { + + auto const b = GetWhere(db, fmt::format("`char_id` = '{}' LIMIT 1;", char_id)); + if (b.empty()) { + return std::string(); + } + + return b.front().welcome_message; + } + + static int UpdateTransactionDate(Database& db, uint32 char_id, time_t transaction_date) { + auto b = GetWhere(db, fmt::format("`char_id` = '{}' LIMIT 1;", char_id)); + if (b.empty()) { + return 0; + } + + auto e = b.front(); + e.transaction_date = transaction_date; + + return UpdateOne(db, e); + } + + static time_t GetTransactionDate(Database& db, uint32 char_id) { + auto b = GetWhere(db, fmt::format("`char_id` = '{}' LIMIT 1;", char_id)); + if (b.empty()) { + return 0; + } + + auto e = b.front(); + + return e.transaction_date; + } + + static bool DeleteBuyer(Database &db, uint32 char_id) + { + if (char_id == 0) { + Truncate(db); + BaseBuyerBuyLinesRepository::Truncate(db); + BaseBuyerTradeItemsRepository::Truncate(db); + } + else { + auto buyer = GetWhere(db, fmt::format("`char_id` = '{}' LIMIT 1;", char_id)); + if (buyer.empty()) { + return false; + } + + auto buy_lines = BaseBuyerBuyLinesRepository::GetWhere( + db, + fmt::format("`buyer_id` = '{}'", buyer.front().id) + ); + if (buy_lines.empty()) { + return false; + } + + std::vector buy_line_ids{}; + for (auto const &bl: buy_lines) { + buy_line_ids.push_back(std::to_string(bl.id)); + } + + DeleteWhere(db, fmt::format("`char_id` = '{}';", char_id)); + BaseBuyerBuyLinesRepository::DeleteWhere( + db, + fmt::format("`id` IN({})", Strings::Implode(", ", buy_line_ids)) + ); + BaseBuyerTradeItemsRepository::DeleteWhere( + db, + fmt::format( + "`buyer_buy_lines_id` IN({})", + Strings::Implode(", ", buy_line_ids)) + ); + } + + return true; + } }; #endif //EQEMU_BUYER_REPOSITORY_H diff --git a/common/repositories/buyer_trade_items_repository.h b/common/repositories/buyer_trade_items_repository.h new file mode 100644 index 000000000..f90430eb3 --- /dev/null +++ b/common/repositories/buyer_trade_items_repository.h @@ -0,0 +1,81 @@ +#ifndef EQEMU_BUYER_TRADE_ITEMS_REPOSITORY_H +#define EQEMU_BUYER_TRADE_ITEMS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_buyer_trade_items_repository.h" + +class BuyerTradeItemsRepository: public BaseBuyerTradeItemsRepository { +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 + * + * BuyerTradeItemsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * BuyerTradeItemsRepository::GetWhereNeverExpires() + * BuyerTradeItemsRepository::GetWhereXAndY() + * BuyerTradeItemsRepository::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 GetTradeItems(Database& db, const uint32 char_id) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "SELECT bti.* " + "FROM buyer_trade_items AS bti " + "INNER JOIN buyer_buy_lines AS bbl ON bti.buyer_buy_lines_id = bbl.id " + "WHERE bbl.char_id = '{}';", + char_id + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BuyerTradeItems e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.buyer_buy_lines_id = row[1] ? strtoull(row[1], nullptr, 10) : 0; + e.item_id = row[2] ? static_cast(atoi(row[2])) : 0; + e.item_qty = row[3] ? static_cast(atoi(row[3])) : 0; + e.item_icon = row[4] ? static_cast(atoi(row[4])) : 0; + e.item_name = row[5] ? row[5] : "0"; + + all_entries.push_back(e); + } + + return all_entries; + } +}; + +#endif //EQEMU_BUYER_TRADE_ITEMS_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index 9edd9e427..3964906b9 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -814,6 +814,7 @@ RULE_INT(Bazaar, MaxBarterSearchResults, 200, "The maximum results returned in t 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_INT(Bazaar, MaxBuyerInventorySearchResults, 200, "Maximum number of search results when a Buyer searches the global item list. Default is 200. RoF+ Only.") RULE_CATEGORY_END() RULE_CATEGORY(Mail) diff --git a/common/servertalk.h b/common/servertalk.h index d39af49b4..46a3e08bc 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -140,6 +140,7 @@ #define ServerOP_TraderMessaging 0x0120 #define ServerOP_BazaarPurchase 0x0121 +#define ServerOP_BuyerMessaging 0x0122 #define ServerOP_InstanceUpdateTime 0x014F #define ServerOP_AdventureRequest 0x0150 diff --git a/common/version.h b/common/version.h index 611397ed8..c4446dafb 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 9280 +#define CURRENT_BINARY_DATABASE_VERSION 9281 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9044 #endif diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 518e312de..0995b83ba 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -433,6 +433,7 @@ OP_TraderShop=0x31df OP_TraderBulkSend=0x6a96 OP_Trader=0x4ef5 OP_Barter=0x243a +OP_BuyerItems=0x1a6a OP_TraderBuy=0x0000 OP_ShopItem=0x0000 OP_BazaarInspect=0x0000 diff --git a/world/world_boot.cpp b/world/world_boot.cpp index 695c81f21..859eff534 100644 --- a/world/world_boot.cpp +++ b/world/world_boot.cpp @@ -291,6 +291,8 @@ bool WorldBoot::DatabaseLoadRoutines(int argc, char **argv) LogInfo("Loading items"); LogInfo("Clearing trader table details"); database.ClearTraderDetails(); + database.ClearBuyerDetails(); + LogInfo("Clearing buyer table details"); if (!content_db.LoadItems(hotfix_name)) { LogError("Error: Could not load item data. But ignoring"); diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index e15aa511f..c54164594 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1742,7 +1742,6 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { } zoneserver_list.SendPacketToBootedZones(pack); - break; } case ServerOP_BazaarPurchase: { @@ -1753,9 +1752,40 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { "ServerOP_BazaarPurchase", in->trader_buy_struct.trader_id ); + return; } zoneserver_list.SendPacket(Zones::BAZAAR, pack); + break; + } + case ServerOP_BuyerMessaging: { + auto in = (BuyerMessaging_Struct *)pack->pBuffer; + switch (in->action) { + case Barter_AddToBarterWindow: + case Barter_RemoveFromBarterWindow: { + if (in->buyer_id <= 0) { + LogTrading("World Message [{}] received with invalid buyer_id [{}]", + "ServerOP_BecomeBuyer", + in->buyer_id + ); + return; + } + + zoneserver_list.SendPacketToBootedZones(pack); + break; + } + case Barter_SellItem: { + zoneserver_list.SendPacket(Zones::BAZAAR, pack); + break; + } + case Barter_FailedTransaction: + case Barter_BuyerTransactionComplete: { + zoneserver_list.SendPacket(in->zone_id, pack); + break; + } + default: + return; + } } default: { LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); diff --git a/zone/api_service.cpp b/zone/api_service.cpp index 25335a784..a0d02b1c9 100644 --- a/zone/api_service.cpp +++ b/zone/api_service.cpp @@ -651,7 +651,6 @@ Json::Value ApiGetClientListDetail(EQ::Net::WebsocketServerConnection *connectio row["base_wis"] = client->GetBaseWIS(); row["become_npc_level"] = client->GetBecomeNPCLevel(); row["boat_id"] = client->GetBoatID(); - row["buyer_welcome_message"] = client->GetBuyerWelcomeMessage(); row["calc_atk"] = client->CalcATK(); row["calc_base_mana"] = client->CalcBaseMana(); row["calc_current_weight"] = client->CalcCurrentWeight(); diff --git a/zone/client.cpp b/zone/client.cpp index 3c40d506c..1aa4aca3a 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -203,7 +203,6 @@ Client::Client(EQStreamInterface *ieqs) : Mob( port = ntohs(eqs->GetRemotePort()); client_state = CLIENT_CONNECTING; SetTrader(false); - Buyer = false; Haste = 0; SetCustomerID(0); SetTraderID(0); @@ -386,6 +385,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob( m_parcel_merchant_engaged = false; m_parcels.clear(); + m_buyer_id = 0; + SetBotPulling(false); SetBotPrecombat(false); @@ -429,8 +430,9 @@ Client::~Client() { TraderEndTrader(); } - if(Buyer) + if(IsBuyer()) { ToggleBuyerMode(false); + } if(conn_state != ClientConnectFinished) { LogDebug("Client [{}] was destroyed before reaching the connected state:", GetName()); @@ -2163,12 +2165,13 @@ void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) Mob::FillSpawnStruct(ns, ForWho); // Populate client-specific spawn information - ns->spawn.afk = AFK; - ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live - ns->spawn.anon = m_pp.anon; - ns->spawn.gm = GetGM() ? 1 : 0; - ns->spawn.guildID = GuildID(); - ns->spawn.trader = IsTrader(); + ns->spawn.afk = AFK; + ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live + ns->spawn.anon = m_pp.anon; + ns->spawn.gm = GetGM() ? 1 : 0; + ns->spawn.guildID = GuildID(); + ns->spawn.trader = IsTrader(); + ns->spawn.buyer = IsBuyer(); // ns->spawn.linkdead = IsLD() ? 1 : 0; // ns->spawn.pvp = GetPVP(false) ? 1 : 0; ns->spawn.show_name = true; @@ -11993,7 +11996,7 @@ void Client::SendPath(Mob* target) target->IsClient() && ( target->CastToClient()->IsTrader() || - target->CastToClient()->Buyer + target->CastToClient()->IsBuyer() ) ) { Message( @@ -12597,3 +12600,104 @@ void Client::RemoveItemBySerialNumber(uint32 serial_number, uint32 quantity) } } } + +void Client::AddMoneyToPPWithOverflow(uint64 copper, bool update_client) +{ + //I noticed in the ROF2 client that the client auto updates the currency values using overflow + //Therefore, I created this method to ensure that the db matches and clients don't see 10 pp 5 gp + //becoming 9pp 15 gold with the current AddMoneyToPP method. + + auto add_pp = copper / 1000; + auto add_gp = (copper - add_pp * 1000) / 100; + auto add_sp = (copper - add_pp * 1000 - add_gp * 100) / 10; + auto add_cp = copper - add_pp * 1000 - add_gp * 100 - add_sp * 10; + + m_pp.copper += add_cp; + if (m_pp.copper >= 10) { + m_pp.silver += m_pp.copper / 10; + m_pp.copper = m_pp.copper % 10; + } + + m_pp.silver += add_sp; + if (m_pp.silver >= 10) { + m_pp.gold += m_pp.silver / 10; + m_pp.silver = m_pp.silver % 10; + } + + m_pp.gold += add_gp; + if (m_pp.gold >= 10) { + m_pp.platinum += m_pp.gold / 10; + m_pp.gold = m_pp.gold % 10; + } + + m_pp.platinum += add_pp; + + if (update_client) { + SendMoneyUpdate(); + } + + RecalcWeight(); + SaveCurrency(); + + LogDebug("Client::AddMoneyToPPWithOverflow() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", + GetName(), + m_pp.platinum, + m_pp.gold, + m_pp.silver, + m_pp.copper + ); +} + +bool Client::TakeMoneyFromPPWithOverFlow(uint64 copper, bool update_client) +{ + int32 remove_pp = copper / 1000; + int32 remove_gp = (copper - remove_pp * 1000) / 100; + int32 remove_sp = (copper - remove_pp * 1000 - remove_gp * 100) / 10; + int32 remove_cp = copper - remove_pp * 1000 - remove_gp * 100 - remove_sp * 10; + + uint64 current_money = GetCarriedMoney(); + + if (copper > current_money) { + return false; //client does not have enough money on them + } + + m_pp.copper -= remove_cp; + if (m_pp.copper < 0) { + m_pp.silver -= 1; + m_pp.copper = m_pp.copper + 10; + if (m_pp.copper >= 10) { + m_pp.silver += m_pp.copper / 10; + m_pp.copper = m_pp.copper % 10; + } + } + + m_pp.silver -= remove_sp; + if (m_pp.silver < 0) { + m_pp.gold -= 1; + m_pp.silver = m_pp.silver + 10; + if (m_pp.silver >= 10) { + m_pp.gold += m_pp.silver / 10; + m_pp.silver = m_pp.silver % 10; + } + } + + m_pp.gold -= remove_gp; + if (m_pp.gold < 0) { + m_pp.platinum -= 1; + m_pp.gold = m_pp.gold + 10; + if (m_pp.gold >= 10) { + m_pp.platinum += m_pp.gold / 10; + m_pp.gold = m_pp.gold % 10; + } + } + + m_pp.platinum -= remove_pp; + + if (update_client) { + SendMoneyUpdate(); + } + + SaveCurrency(); + RecalcWeight(); + return true; +} \ No newline at end of file diff --git a/zone/client.h b/zone/client.h index f54680728..37f60194c 100644 --- a/zone/client.h +++ b/zone/client.h @@ -71,6 +71,7 @@ namespace EQ #include "../common/repositories/character_parcels_repository.h" #include "../common/repositories/trader_repository.h" #include "../common/guild_base.h" +#include "../common/repositories/buyer_buy_lines_repository.h" #ifdef _WINDOWS // since windows defines these within windef.h (which windows.h include) @@ -289,6 +290,7 @@ public: void TraderPriceUpdate(const EQApplicationPacket *app); void SendBazaarDone(uint32 trader_id); void SendBulkBazaarTraders(); + void SendBulkBazaarBuyers(); void DoBazaarInspect(const BazaarInspect_Struct &in); void SendBazaarDeliveryCosts(); static std::string DetermineMoneyString(uint64 copper); @@ -308,8 +310,10 @@ public: bool TryStacking(EQ::ItemInstance* item, uint8 type = ItemPacketTrade, bool try_worn = true, bool try_cursor = true); void SendTraderPacket(Client* trader, uint32 Unknown72 = 51); void SendBuyerPacket(Client* Buyer); + void SendBuyerToBarterWindow(Client* buyer, uint32 action); GetItems_Struct* GetTraderItems(); void SendBazaarWelcome(); + void SendBarterWelcome(); void DyeArmor(EQ::TintProfile* dye); void DyeArmorBySlot(uint8 slot, uint8 red, uint8 green, uint8 blue, uint8 use_tint = 0x00); uint8 SlotConvert(uint8 slot,bool bracer=false); @@ -376,14 +380,35 @@ public: uint32 GetCustomerID() { return customer_id; } void SetCustomerID(uint32 id) { customer_id = id; } - void SendBuyerResults(char *SearchQuery, uint32 SearchID); + void SetBuyerID(uint32 id) { m_buyer_id = id; } + uint32 GetBuyerID() { return m_buyer_id; } + bool IsBuyer() { return m_buyer_id != 0 ? true : false; } + void SetBarterTime() { m_barter_time = time(nullptr); } + uint32 GetBarterTime() { return m_barter_time; } + void SetBuyerWelcomeMessage(const char* welcome_message); + void SendBuyerGreeting(uint32 char_id); + void SendSellerBrowsing(const std::string &browser); + void SendBuyerMode(bool status); + bool IsInBuyerSpace(); + void SendBuyLineUpdate(const BuyerLineItems_Struct &buy_line); + void CheckIfMovedItemIsPartOfBuyLines(uint32 item_id); + + void SendBuyerResults(BarterSearchRequest_Struct& bsr); void ShowBuyLines(const EQApplicationPacket *app); void SellToBuyer(const EQApplicationPacket *app); void ToggleBuyerMode(bool TurnOn); - void UpdateBuyLine(const EQApplicationPacket *app); + void ModifyBuyLine(const EQApplicationPacket *app); + void CreateStartingBuyLines(const EQApplicationPacket *app); void BuyerItemSearch(const EQApplicationPacket *app); - void SetBuyerWelcomeMessage(const char* WelcomeMessage) { BuyerWelcomeMessage = WelcomeMessage; } - const char* GetBuyerWelcomeMessage() { return BuyerWelcomeMessage.c_str(); } + void SendWindowUpdatesToSellerAndBuyer(BuyerLineSellItem_Struct& blsi); + void SendBarterBuyerClientMessage(BuyerLineSellItem_Struct& blsi, BarterBuyerActions action, BarterBuyerSubActions sub_action, BarterBuyerSubActions error_code); + bool BuildBuyLineMap(std::map& item_map, BuyerBuyLines_Struct& bl); + bool BuildBuyLineMapFromVector(std::map& item_map, std::vector& bl); + void RemoveItemFromBuyLineMap(std::map& item_map, const BuyerLineItems_Struct& bl); + bool ValidateBuyLineItems(std::map& item_map); + int64 ValidateBuyLineCost(std::map& item_map); + bool DoBarterBuyerChecks(BuyerLineSellItem_Struct& sell_line); + bool DoBarterSellerChecks(BuyerLineSellItem_Struct& sell_line); void FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho); bool ShouldISpawnFor(Client *c) { return !GMHideMe(c) && !IsHoveringForRespawn(); } @@ -827,9 +852,11 @@ public: void QuestReadBook(const char* text, uint8 type); void SendMoneyUpdate(); bool TakeMoneyFromPP(uint64 copper, bool update_client = false); + bool TakeMoneyFromPPWithOverFlow(uint64 copper, bool update_client); bool TakePlatinum(uint32 platinum, bool update_client = false); void AddMoneyToPP(uint64 copper, bool update_client = false); void AddMoneyToPP(uint32 copper, uint32 silver, uint32 gold, uint32 platinum, bool update_client = false); + void AddMoneyToPPWithOverflow(uint64 copper, bool update_client); void AddPlatinum(uint32 platinu, bool update_client = false); bool HasMoney(uint64 copper); uint64 GetCarriedMoney(); @@ -1058,6 +1085,8 @@ public: int32 GetItemIDAt(int16 slot_id); int32 GetAugmentIDAt(int16 slot_id, uint8 augslot); bool PutItemInInventory(int16 slot_id, const EQ::ItemInstance& inst, bool client_update = false); + bool PutItemInInventoryWithStacking(EQ::ItemInstance* inst); + bool FindNumberOfFreeInventorySlotsWithSizeCheck(std::vector items); bool PushItemOnCursor(const EQ::ItemInstance& inst, bool client_update = false); void SendCursorBuffer(); void DeleteItemInInventory(int16 slot_id, int16 quantity = 0, bool client_update = false, bool update_db = true); @@ -1096,7 +1125,6 @@ public: 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; } @@ -1913,8 +1941,8 @@ private: uint8 firstlogon; uint32 mercid; // current merc uint8 mercSlot; // selected merc slot - bool Buyer; - std::string BuyerWelcomeMessage; + uint32 m_buyer_id; + uint32 m_barter_time; int32 m_parcel_platinum; int32 m_parcel_gold; int32 m_parcel_silver; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index a02fdc17d..e12d4e9d4 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -65,6 +65,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/repositories/account_repository.h" #include "../common/repositories/character_corpses_repository.h" #include "../common/repositories/guild_tributes_repository.h" +#include "../common/repositories/buyer_buy_lines_repository.h" #include "../common/events/player_event_logs.h" #include "../common/repositories/character_stats_record_repository.h" @@ -758,8 +759,6 @@ void Client::CompleteConnect() entity_list.SendIllusionWearChange(this); - entity_list.SendTraders(this); - SendWearChangeAndLighting(EQ::textures::LastTexture); Mob* pet = GetPet(); if (pet) { @@ -3759,116 +3758,104 @@ void Client::Handle_OP_Barter(const EQApplicationPacket *app) } char* Buf = (char *)app->pBuffer; + auto in = (BuyerGeneric_Struct *)app->pBuffer; // The first 4 bytes of the packet determine the action. A lot of Barter packets require the // packet the client sent, sent back to it as an acknowledgement. // - uint32 Action = VARSTRUCT_DECODE_TYPE(uint32, Buf); + //uint32 Action = VARSTRUCT_DECODE_TYPE(uint32, Buf); - switch (Action) - { + switch (in->action) { - case Barter_BuyerSearch: - { - BuyerItemSearch(app); - break; - } - - case Barter_SellerSearch: - { - BarterSearchRequest_Struct *bsr = (BarterSearchRequest_Struct*)app->pBuffer; - SendBuyerResults(bsr->SearchString, bsr->SearchID); - break; - } - - case Barter_BuyerModeOn: - { - if (!IsTrader()) { - ToggleBuyerMode(true); + case Barter_BuyerSearch: { + BuyerItemSearch(app); + break; } - else { - Buf = (char *)app->pBuffer; - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerModeOff); - Message(Chat::Red, "You cannot be a Trader and Buyer at the same time."); + + case Barter_SellerSearch: { + auto bsr = (BarterSearchRequest_Struct *) app->pBuffer; + SendBuyerResults(*bsr); + break; } - QueuePacket(app); - break; - } - case Barter_BuyerModeOff: - { - QueuePacket(app); - ToggleBuyerMode(false); - break; - } + case Barter_BuyerModeOn: { + if (!IsTrader()) { + ToggleBuyerMode(true); + } + else { + ToggleBuyerMode(false); + Message(Chat::Red, "You cannot be a Trader and Buyer at the same time."); + } + break; + } - case Barter_BuyerItemUpdate: - { - UpdateBuyLine(app); - break; - } + case Barter_BuyerModeOff: { + ToggleBuyerMode(false); + break; + } - case Barter_BuyerItemRemove: - { - BuyerRemoveItem_Struct* bris = (BuyerRemoveItem_Struct*)app->pBuffer; - database.RemoveBuyLine(CharacterID(), bris->BuySlot); - QueuePacket(app); - break; - } + case Barter_BuyerItemUpdate: { + ModifyBuyLine(app); + break; + } - case Barter_SellItem: - { - SellToBuyer(app); - break; - } + case Barter_BuyerItemStart: { + CreateStartingBuyLines(app); + break; + } - case Barter_BuyerInspectBegin: - { - ShowBuyLines(app); - break; - } + case Barter_BuyerItemRemove: { + auto bris = (BuyerRemoveItem_Struct *) app->pBuffer; + BuyerBuyLinesRepository::DeleteBuyLine(database, CharacterID(), bris->buy_slot_id); + QueuePacket(app); + break; + } - case Barter_BuyerInspectEnd: - { - BuyerInspectRequest_Struct* bir = (BuyerInspectRequest_Struct*)app->pBuffer; - Client *Buyer = entity_list.GetClientByID(bir->BuyerID); - if (Buyer) - Buyer->WithCustomer(0); + case Barter_SellItem: { + SellToBuyer(app); + break; + } - break; - } + case Barter_BuyerInspectBegin: { + ShowBuyLines(app); + break; + } - case Barter_BarterItemInspect: - { - BarterItemSearchLinkRequest_Struct* bislr = (BarterItemSearchLinkRequest_Struct*)app->pBuffer; + case Barter_BuyerInspectEnd: { + auto bir = (BuyerInspectRequest_Struct *) app->pBuffer; + auto buyer = entity_list.GetClientByID(bir->buyer_id); + if (buyer) { + buyer->WithCustomer(0); + } - const EQ::ItemData* item = database.GetItem(bislr->ItemID); + break; + } + case Barter_BarterItemInspect: { + auto bislr = (BarterItemSearchLinkRequest_Struct *) app->pBuffer; + const EQ::ItemData *item = database.GetItem(bislr->item_id); - if (!item) - Message(Chat::Red, "Error: This item does not exist!"); - else - { - EQ::ItemInstance* inst = database.CreateItem(item); - if (inst) - { + if (!item) { + Message(Chat::Red, "Error: This item does not exist!"); + return; + } + + EQ::ItemInstance *inst = database.CreateItem(item); + if (inst) { SendItemPacket(0, inst, ItemPacketViewLink); safe_delete(inst); } - } break; } - case Barter_Welcome: { - //SendBazaarWelcome(); + SendBarterWelcome(); break; } - case Barter_WelcomeMessageUpdate: - { - BuyerWelcomeMessageUpdate_Struct* bwmu = (BuyerWelcomeMessageUpdate_Struct*)app->pBuffer; - SetBuyerWelcomeMessage(bwmu->WelcomeMessage); + case Barter_WelcomeMessageUpdate: { + auto bwmu = (BuyerWelcomeMessageUpdate_Struct *) app->pBuffer; + SetBuyerWelcomeMessage(bwmu->welcome_message); break; } @@ -3892,15 +3879,20 @@ void Client::Handle_OP_Barter(const EQApplicationPacket *app) break; } - case Barter_Unknown23: + case Barter_Greeting: { - // Sent by SoD client for no discernible reason. + auto data = (BuyerGreeting_Struct *)app->pBuffer; + SendBuyerGreeting(data->buyer_id); + } + case Barter_OpenBarterWindow: + { + SendBulkBazaarBuyers(); break; } default: Message(Chat::Red, "Unrecognised Barter action."); - LogTrading("Unrecognised Barter Action [{}]", Action); + LogTrading("Unrecognised Barter Action [{}]", in->action); } } @@ -4982,6 +4974,10 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { TraderEndTrader(); } + if (IsBuyer()) { + ToggleBuyerMode(false); + } + /* Break Hide if moving without sneaking and set rewind timer if moved */ if ((hidden || improved_hidden) && !sneaking) { hidden = false; @@ -15640,7 +15636,7 @@ void Client::Handle_OP_Trader(const EQApplicationPacket *app) break; } case TraderOn: { - if (Buyer) { + if (IsBuyer()) { TraderEndTrader(); Message(Chat::Red, "You cannot be a Trader and Buyer at the same time."); return; @@ -15686,7 +15682,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) auto trader = entity_list.GetClientByID(in->trader_id); switch (in->method) { - case ByVendor: { + case BazaarByVendor: { if (trader) { LogTrading("Buy item directly from vendor id [{}] item_id [{}] quantity [{}] " "serial_number [{}]", @@ -15699,7 +15695,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) } break; } - case ByParcel: { + case BazaarByParcel: { if (!RuleB(Parcel, EnableParcelMerchants) || !RuleB(Bazaar, EnableParcelDelivery)) { LogTrading( "Bazaar purchase attempt by parcel delivery though 'Parcel:EnableParcelMerchants' or " @@ -15709,7 +15705,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) 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->method = BazaarByParcel; in->sub_action = Failed; TradeRequestFailed(app); return; @@ -15724,7 +15720,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) BuyTraderItemOutsideBazaar(in, app); break; } - case ByDirectToInventory: { + case BazaarByDirectToInventory: { if (!RuleB(Parcel, EnableDirectToInventoryDelivery)) { LogTrading("Bazaar purchase attempt by direct inventory delivery though " "'Parcel:EnableDirectToInventoryDelivery' not enabled." @@ -15733,7 +15729,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) Chat::Yellow, "Direct inventory delivey is not enabled on this server. Please visit the vendor directly." ); - in->method = ByDirectToInventory; + in->method = BazaarByDirectToInventory; in->sub_action = Failed; TradeRequestFailed(app); return; @@ -15750,7 +15746,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) Chat::Yellow, "Direct inventory delivey is not yet implemented. Please visit the vendor directly or purchase via parcel delivery." ); - in->method = ByDirectToInventory; + in->method = BazaarByDirectToInventory; in->sub_action = Failed; TradeRequestFailed(app); break; diff --git a/zone/inventory.cpp b/zone/inventory.cpp index f49a5e7ce..030770a78 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -1864,6 +1864,10 @@ 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 (IsBuyer() && srcitemid > 0) { + CheckIfMovedItemIsPartOfBuyLines(srcitemid); + } + if (IsTrader() && srcitemid>0){ EQ::ItemInstance* srcbag; EQ::ItemInstance* dstbag; @@ -4850,3 +4854,63 @@ bool Client::HasItemOnCorpse(uint32 item_id) return false; } + +bool Client::PutItemInInventoryWithStacking(EQ::ItemInstance *inst) +{ + auto free_id = GetInv().FindFirstFreeSlotThatFitsItem(inst->GetItem()); + if (inst->IsStackable()) { + if (TryStacking(inst, ItemPacketTrade, true, false)) { + return true; + } + } + if (free_id != INVALID_INDEX) { + if (PutItemInInventory(free_id, *inst, true)) { + return true; + } + } + return false; +}; + +bool Client::FindNumberOfFreeInventorySlotsWithSizeCheck(std::vector items) +{ + uint32 count = 0; + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + if ((((uint64) 1 << i) & GetInv().GetLookup()->PossessionsBitmask) == 0) { + continue; + } + + EQ::ItemInstance *inv_item = GetInv().GetItem(i); + + if (!inv_item) { + // Found available slot in personal inventory. Fits all sizes + count++; + } + + if (count >= items.size()) { + return true; + } + + if (inv_item->IsClassBag()) { + for (auto const& item:items) { + auto item_tmp = database.GetItem(item.item_id); + if (EQ::InventoryProfile::CanItemFitInContainer(item_tmp, inv_item->GetItem())) { + int16 base_slot_id = EQ::InventoryProfile::CalcSlotId(i, EQ::invbag::SLOT_BEGIN); + uint8 bag_size = inv_item->GetItem()->BagSlots; + + for (uint8 bag_slot = EQ::invbag::SLOT_BEGIN; bag_slot < bag_size; bag_slot++) { + auto bag_item = GetInv().GetItem(base_slot_id + bag_slot); + if (!bag_item) { + // Found a bag slot that fits the item + count++; + } + } + + if (count >= items.size()) { + return true; + } + } + } + } + } + return false; +}; \ No newline at end of file diff --git a/zone/position.cpp b/zone/position.cpp index 0c49ca784..52062120e 100644 --- a/zone/position.cpp +++ b/zone/position.cpp @@ -4,6 +4,8 @@ #include #include "../common/strings.h" #include "../common/data_verification.h" +#include +#include "../common/types.h" constexpr float position_eps = 0.0001f; @@ -282,3 +284,54 @@ float CalculateHeadingAngleBetweenPositions(float x1, float y1, float x2, float return (90.0f - angle + 270.0f) * 511.5f * 0.0027777778f; } } +bool IsWithinCircularArc(glm::vec4 arc_center, glm::vec4 point, uint32 arc_offset, uint32 arc_radius, uint32 arc_radius_limit) +{ + auto CheckClockwise = [](double v_x, double v_y, double check_x, double check_y) -> bool { + return -v_y * check_x + v_x * check_y >= 0; + }; + + auto CheckRadiusLimit = [](double check_x, double check_y, uint32 radius, uint32 radius_limit) -> bool { + auto w = check_x * check_x + check_y * check_y; + if (w >= radius_limit * radius_limit && w <= radius * radius) { + return true; + } + return false; + }; + + auto DegreesToRadians = [](float in) -> double { + return in / 180.0f * std::numbers::pi; + }; + + auto h = arc_center.w / 512.0f * 360.0f + arc_offset; + auto a = DegreesToRadians(h); + + auto vs_x = -arc_radius * cos(a); + auto vs_y = arc_radius * sin(a); + + h += 90; + a = DegreesToRadians(h); + auto ve_x = -arc_radius * cos(a); + auto ve_y = arc_radius * sin(a); + + double check_x = point.x - arc_center.x; + double check_y = point.y - arc_center.y; + + return CheckClockwise(vs_x, vs_y, check_x, check_y) && CheckRadiusLimit(check_x, check_y, arc_radius, arc_radius_limit) && !CheckClockwise(ve_x, ve_y, check_x, check_y); +} + +bool IsWithinSquare(glm::vec4 center, uint32 area, glm::vec4 position) { + auto l = std::abs(std::sqrt(area)); + if (l <= 0) { + return false; + } + + auto x_min = center.x - l; + auto x_max = center.x + l; + auto y_min = center.y - l; + auto y_max = center.y + l; + + auto x = position.x; + auto y = position.y; + + return x > x_min && x < x_max && y > y_min && y < y_max; +} diff --git a/zone/position.h b/zone/position.h index dbafdc911..ae43aca4f 100644 --- a/zone/position.h +++ b/zone/position.h @@ -23,6 +23,7 @@ #include #include #include +#include "../common/types.h" std::string to_string(const glm::vec4 &position); std::string to_string(const glm::vec3 &position); @@ -62,4 +63,7 @@ bool IsPositionWithinSimpleCylinder(const glm::vec4 &p1, const glm::vec4 &cylind float CalculateHeadingAngleBetweenPositions(float x1, float y1, float x2, float y2); +bool IsWithinCircularArc(glm::vec4 arc_center, glm::vec4 point, uint32 arc_offset, uint32 arc_radius, uint32 arc_radius_limit); +bool IsWithinSquare(glm::vec4 center, uint32 area, glm::vec4 position); + #endif diff --git a/zone/string_ids.h b/zone/string_ids.h index 96b6deb0e..b778170b6 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -413,6 +413,8 @@ #define MAX_ACTIVE_TASKS 6010 //Sorry %3, you already have the maximum number of active tasks. #define TASK_REQUEST_COOLDOWN_TIMER 6011 //Sorry, %3, but you can't request another task for %4 minutes and %5 seconds. #define FORAGE_MASTERY 6012 //Your forage mastery has enabled you to find something else! +#define BUYER_WELCOME 6065 //There are %1 Buyers waiting to purchase your loot. Type /barter to search for them, or use /buyer to set up your own Buy Lines. +#define BUYER_GREETING 6070 //%1 greets you, '%2' #define GUILD_BANK_CANNOT_DEPOSIT 6097 // Cannot deposit this item. Containers must be empty, and only one of each LORE and no NO TRADE or TEMPORARY items may be deposited. #define GUILD_BANK_FULL 6098 // There is no more room in the Guild Bank. #define GUILD_BANK_TRANSFERRED 6100 // '%1' transferred to Guild Bank from Deposits. diff --git a/zone/trading.cpp b/zone/trading.cpp index 114b3c321..ad7131c6a 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -24,6 +24,8 @@ #include "../common/misc_functions.h" #include "../common/events/player_event_logs.h" #include "../common/repositories/trader_repository.h" +#include "../common/repositories/buyer_repository.h" +#include "../common/repositories/buyer_buy_lines_repository.h" #include "client.h" #include "entity.h" @@ -33,6 +35,7 @@ #include "string_ids.h" #include "worldserver.h" #include "../common/bazaar.h" +#include class QueryServ; @@ -1781,6 +1784,12 @@ void Client::SendBazaarWelcome() QueuePacket(outapp.get()); } +void Client::SendBarterWelcome() +{ + const auto results = BuyerBuyLinesRepository::GetWelcomeData(database); + MessageString(Chat::White, BUYER_WELCOME, std::to_string(results.count_of_buyers).c_str()); +} + void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria) { auto results = Bazaar::GetSearchResults(database, search_criteria, GetZoneID()); @@ -2272,525 +2281,339 @@ static void UpdateTraderCustomerPriceChanged( // safe_delete(inst); } -void Client::SendBuyerResults(char* searchString, uint32 searchID) { +void Client::SendBuyerResults(BarterSearchRequest_Struct& bsr) +{ + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + std::string search_string(bsr.search_string); + BuyerLineSearch_Struct results{}; - // This method is called when a potential seller in the /barter window searches for matching buyers - // - LogDebug("[CLIENT] Client::SendBuyerResults [{}]\n", searchString); + SetBarterTime(); - auto escSearchString = new char[strlen(searchString) * 2 + 1]; - database.DoEscapeString(escSearchString, searchString, strlen(searchString)); - - std::string query = StringFormat("SELECT * FROM buyer WHERE itemname LIKE '%%%s%%' ORDER BY charid LIMIT %i", - escSearchString, RuleI(Bazaar, MaxBarterSearchResults)); - safe_delete_array(escSearchString); - auto results = database.QueryDatabase(query); - if (!results.Success()) { - return; - } - - int numberOfRows = results.RowCount(); - - if(numberOfRows == RuleI(Bazaar, MaxBarterSearchResults)) - Message(Chat::Yellow, "Your search found too many results; some are not displayed."); - else if(strlen(searchString) == 0) - Message(Chat::NPCQuestSay, "There are %i Buy Lines.", numberOfRows); - else - Message(Chat::NPCQuestSay, "There are %i Buy Lines that match the search string '%s'.", numberOfRows, searchString); - - if(numberOfRows == 0) - return; - - uint32 lastCharID = 0; - Client *buyer = nullptr; - - for (auto &row = results.begin(); row != results.end(); ++row) { - char itemName[64]; - - uint32 charID = Strings::ToInt(row[0]); - uint32 buySlot = Strings::ToInt(row[1]); - uint32 itemID = Strings::ToInt(row[2]); - strcpy(itemName, row[3]); - uint32 quantity = Strings::ToInt(row[4]); - uint32 price = Strings::ToInt(row[5]); - - // Each item in the search results is sent as a single fixed length packet, although the position of - // the fields varies due to the use of variable length strings. The reason the packet is so big, is - // to allow item compensation, e.g. a buyer could offer to buy a Blade Of Carnage for 10000pp plus - // other items in exchange. Item compensation is not currently supported in EQEmu. - // - auto outapp = new EQApplicationPacket(OP_Barter, 940); - - char *buf = (char *)outapp->pBuffer; - - const EQ::ItemData* item = database.GetItem(itemID); - - if(!item) { - safe_delete(outapp); - continue; + if (bsr.search_scope == 1) { + // Local Buyers + results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string, 0, GetZoneID(), GetInstanceID()); + } + else if (bsr.buyer_id) { + // Specific Buyer + results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string, bsr.buyer_id); + } else { + // All Buyers + results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string); } - // Save having to scan the client list when dealing with multiple buylines for the same Character. - if(charID != lastCharID) { - buyer = entity_list.GetClientByCharID(charID); - lastCharID = charID; + if (results.buy_line.empty()) { + Message(Chat::White, "No buylines could be found."); + return; } - if(!buyer) { - safe_delete(outapp); - continue; + std::string buyer_name = "ID {} not in zone."; + if (search_string.empty()) { + search_string = "*"; } - VARSTRUCT_ENCODE_TYPE(uint32, buf, Barter_BuyerSearchResults); // Command - VARSTRUCT_ENCODE_TYPE(uint32, buf, searchID); // Match up results with the request - VARSTRUCT_ENCODE_TYPE(uint32, buf, buySlot); // Slot in this Buyer's list - VARSTRUCT_ENCODE_TYPE(uint8, buf, 0x01); // Unknown - probably a flag field - VARSTRUCT_ENCODE_TYPE(uint32, buf, itemID); // ItemID - VARSTRUCT_ENCODE_STRING(buf, itemName); // Itemname - VARSTRUCT_ENCODE_TYPE(uint32, buf, item->Icon); // Icon - VARSTRUCT_ENCODE_TYPE(uint32, buf, quantity); // Quantity - VARSTRUCT_ENCODE_TYPE(uint8, buf, 0x01); // Unknown - probably a flag field - VARSTRUCT_ENCODE_TYPE(uint32, buf, price); // Price - VARSTRUCT_ENCODE_TYPE(uint32, buf, buyer->GetID()); // Entity ID - VARSTRUCT_ENCODE_TYPE(uint32, buf, 0); // Flag for + Items , probably ItemCount - VARSTRUCT_ENCODE_STRING(buf, buyer->GetName()); // Seller Name + results.search_string = std::move(search_string); + results.transaction_id = bsr.transaction_id; + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + { ar(results); } - QueuePacket(outapp); - safe_delete(outapp); - } + auto packet = std::make_unique(OP_BuyerItems, ss.str().length() + sizeof(BuyerGeneric_Struct)); + auto emu = (BuyerGeneric_Struct *) packet->pBuffer; + emu->action = Barter_BuyerSearch; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + QueuePacket(packet.get()); + + ss.str(""); + ss.clear(); + + } } -void Client::ShowBuyLines(const EQApplicationPacket *app) { +void Client::ShowBuyLines(const EQApplicationPacket *app) +{ + auto bir = (BuyerInspectRequest_Struct *) app->pBuffer; + auto buyer = entity_list.GetClientByID(bir->buyer_id); - BuyerInspectRequest_Struct* bir = ( BuyerInspectRequest_Struct*)app->pBuffer; - - Client *Buyer = entity_list.GetClientByID(bir->BuyerID); - - if(!Buyer) { - bir->Approval = 0; // Tell the client that the Buyer is unavailable + if (!buyer || buyer->GetCustomerID()) { + bir->approval = 0; // Tell the client that the Buyer is unavailable QueuePacket(app); - Message(Chat::Red, "The Buyer has gone away."); - return; - } - - bir->Approval = Buyer->WithCustomer(GetID()); - - QueuePacket(app); - - if(bir->Approval == 0) { MessageString(Chat::Yellow, TRADER_BUSY); return; } - const char *WelcomeMessagePointer = Buyer->GetBuyerWelcomeMessage(); + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + SetBarterTime(); + bir->approval = buyer->WithCustomer(GetID()); + QueuePacket(app); - if(strlen(WelcomeMessagePointer) > 0) - Message(Chat::NPCQuestSay, "%s greets you, '%s'.", Buyer->GetName(), WelcomeMessagePointer); + auto results = BuyerBuyLinesRepository::GetBuyLines(database, buyer->CharacterID()); + auto greeting = BuyerRepository::GetWelcomeMessage(database, buyer->GetBuyerID()); - auto outapp = new EQApplicationPacket(OP_Barter, sizeof(BuyerBrowsing_Struct)); - - BuyerBrowsing_Struct* bb = (BuyerBrowsing_Struct*)outapp->pBuffer; - - // This packet produces the SoandSo is browsing your Buy Lines message - bb->Action = Barter_SellerBrowsing; - - sprintf(bb->PlayerName, "%s", GetName()); - - Buyer->QueuePacket(outapp); - - safe_delete(outapp); - - std::string query = StringFormat("SELECT * FROM buyer WHERE charid = %i", Buyer->CharacterID()); - auto results = database.QueryDatabase(query); - if (!results.Success() || results.RowCount() == 0) - return; - - for (auto &row = results.begin(); row != results.end(); ++row) { - char ItemName[64]; - uint32 BuySlot = Strings::ToInt(row[1]); - uint32 ItemID = Strings::ToInt(row[2]); - strcpy(ItemName, row[3]); - uint32 Quantity = Strings::ToInt(row[4]); - uint32 Price = Strings::ToInt(row[5]); - - auto outapp = new EQApplicationPacket(OP_Barter, 936); - - char *Buf = (char *)outapp->pBuffer; - - const EQ::ItemData* item = database.GetItem(ItemID); - - if(!item) { - safe_delete(outapp); - continue; + if (greeting.length() == 0) { + greeting = "Welcome!"; } - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerInspectWindow); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // Flag - VARSTRUCT_ENCODE_TYPE(uint32, Buf, ItemID); - VARSTRUCT_ENCODE_STRING(Buf, ItemName); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, item->Icon); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // Flag - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Buyer->GetID()); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); - VARSTRUCT_ENCODE_STRING(Buf, Buyer->GetName()); + MessageString(Chat::NPCQuestSay, BUYER_GREETING, buyer->GetName(), greeting.c_str()); + const std::string name(GetName()); + buyer->SendSellerBrowsing(name); - QueuePacket(outapp); - safe_delete(outapp); - } + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + + for (auto l : results) { + const EQ::ItemData *item = database.GetItem(l.item_id); + l.enabled = 1; + l.item_icon = item->Icon; + l.item_toggle = 1; + + { ar(l); } + + auto packet = std::make_unique(OP_BuyerItems, ss.str().length() + sizeof(BuyerGeneric_Struct)); + auto emu = (BuyerGeneric_Struct *) packet->pBuffer; + + emu->action = Barter_BuyerInspectBegin; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + QueuePacket(packet.get()); + + ss.str(""); + ss.clear(); + } + + return; + } } -void Client::SellToBuyer(const EQApplicationPacket *app) { +void Client::SellToBuyer(const EQApplicationPacket *app) +{ + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + BuyerLineSellItem_Struct sell_line{}; + auto in = (BuyerGeneric_Struct *) app->pBuffer; + EQ::Util::MemoryStreamReader ss_in( + reinterpret_cast(in->payload), + app->size - sizeof(BuyerGeneric_Struct)); + cereal::BinaryInputArchive ar(ss_in); + ar(sell_line); - char* Buf = (char *)app->pBuffer; + sell_line.seller_name = GetCleanName(); - char ItemName[64]; - - /*uint32 Action =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused - uint32 Quantity = VARSTRUCT_DECODE_TYPE(uint32, Buf); - uint32 BuyerID = VARSTRUCT_DECODE_TYPE(uint32, Buf); - uint32 BuySlot = VARSTRUCT_DECODE_TYPE(uint32, Buf); - uint32 UnknownByte = VARSTRUCT_DECODE_TYPE(uint8, Buf); - uint32 ItemID = VARSTRUCT_DECODE_TYPE(uint32, Buf); - /* ItemName */ VARSTRUCT_DECODE_STRING(ItemName, Buf); - /*uint32 Unknown2 =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused - uint32 QtyBuyerWants = VARSTRUCT_DECODE_TYPE(uint32, Buf); - UnknownByte = VARSTRUCT_DECODE_TYPE(uint8, Buf); - uint32 Price = VARSTRUCT_DECODE_TYPE(uint32, Buf); - /*uint32 BuyerID2 =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused - /*uint32 Unknown3 =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused - - const EQ::ItemData *item = database.GetItem(ItemID); - - if(!item || !Quantity || !Price || !QtyBuyerWants) return; - - if (m_inv.HasItem(ItemID, Quantity, invWhereWorn | invWherePersonal | invWhereCursor) == INVALID_INDEX) { - Message(Chat::Red, "You do not have %i %s on you.", Quantity, item->Name); - return; - } - - - Client *Buyer = entity_list.GetClientByID(BuyerID); - - if(!Buyer || !Buyer->IsBuyer()) { - Message(Chat::Red, "The Buyer has gone away."); - return; - } - - // For Stackable items, HasSpaceForItem will try check if there is space to stack with existing stacks in - // the buyer inventory. - if(!(Buyer->GetInv().HasSpaceForItem(item, Quantity))) { - Message(Chat::Red, "The Buyer does not have space for %i %s", Quantity, item->Name); - return; - } - - if((static_cast(Quantity) * static_cast(Price)) > MAX_TRANSACTION_VALUE) { - Message(Chat::Red, "That would exceed the single transaction limit of %u platinum.", MAX_TRANSACTION_VALUE / 1000); - return; - } - - if(!Buyer->HasMoney(Quantity * Price)) { - Message(Chat::Red, "The Buyer does not have sufficient money to purchase that quantity of %s.", item->Name); - Buyer->Message(Chat::Red, "%s tried to sell you %i %s, but you have insufficient funds.", GetName(), Quantity, item->Name); - return; - } - - if(Buyer->CheckLoreConflict(item)) { - Message(Chat::Red, "That item is LORE and the Buyer already has one."); - Buyer->Message(Chat::Red, "%s tried to sell you %s but this item is LORE and you already have one.", - GetName(), item->Name); - return; - } - - if(item->NoDrop == 0) { - Message(Chat::Red, "That item is NODROP."); - return; - } - - if(item->IsClassBag()) { - Message(Chat::Red, "That item is a Bag."); - return; - } - - if(!item->Stackable) { - - for(uint32 i = 0; i < Quantity; i++) { - - int16 SellerSlot = m_inv.HasItem(ItemID, 1, invWhereWorn|invWherePersonal|invWhereCursor); - - // This shouldn't happen, as we already checked there was space in the Buyer's inventory - if (SellerSlot == INVALID_INDEX) { - - if(i > 0) { - // Set the Quantity to the actual number we successfully transferred. - Quantity = i; + switch (sell_line.purchase_method) { + case BarterInBazaar: + case BarterByVendor: { + auto buyer = entity_list.GetClientByID(sell_line.buyer_entity_id); + if (!buyer) { + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); break; } - LogError("Unexpected error while moving item from seller to buyer"); - Message(Chat::Red, "Internal error while processing transaction."); - return; - } - EQ::ItemInstance* ItemToTransfer = m_inv.PopItem(SellerSlot); - - if(!ItemToTransfer || !Buyer->MoveItemToInventory(ItemToTransfer, true)) { - LogError("Unexpected error while moving item from seller to buyer"); - Message(Chat::Red, "Internal error while processing transaction."); - - if(ItemToTransfer) - safe_delete(ItemToTransfer); - - return; - } - - database.SaveInventory(CharacterID(), 0, SellerSlot); - - safe_delete(ItemToTransfer); - - // Remove the item from inventory, clientside - // - auto outapp2 = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); - - MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer; - mis->from_slot = SellerSlot; - mis->to_slot = 0xFFFFFFFF; - mis->number_in_stack = 0xFFFFFFFF; - - QueuePacket(outapp2); - safe_delete(outapp2); - - } - } - else { - // Stackable - // - uint32 QuantityMoved = 0; - - while(QuantityMoved < Quantity) { - - // Find the slot on the seller that has a stack of at least 1 of the item - int16 SellerSlot = m_inv.HasItem(ItemID, 1, invWhereWorn|invWherePersonal|invWhereCursor); - - if (SellerSlot == INVALID_INDEX) { - LogError("Unexpected error while moving item from seller to buyer"); - Message(Chat::Red, "Internal error while processing transaction."); - return; - } - - EQ::ItemInstance* ItemToTransfer = m_inv.PopItem(SellerSlot); - - if(!ItemToTransfer) { - LogError("Unexpected error while moving item from seller to buyer"); - Message(Chat::Red, "Internal error while processing transaction."); - return; - } - - // If the stack we found has less than the quantity we are selling ... - if(ItemToTransfer->GetCharges() <= (Quantity - QuantityMoved)) { - // Transfer the entire stack - - QuantityMoved += ItemToTransfer->GetCharges(); - - if(!Buyer->MoveItemToInventory(ItemToTransfer, true)) { - LogError("Unexpected error while moving item from seller to buyer"); - Message(Chat::Red, "Internal error while processing transaction."); - safe_delete(ItemToTransfer); + if (!DoBarterBuyerChecks(sell_line)) { return; - } - // Delete the entire stack from the seller's inventory - database.SaveInventory(CharacterID(), 0, SellerSlot); + }; - safe_delete(ItemToTransfer); + if (!DoBarterSellerChecks(sell_line)) { + return; + }; - // and tell the client to do the same. - auto outapp2 = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct)); + BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr)); - MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer; - mis->from_slot = SellerSlot; - mis->to_slot = 0xFFFFFFFF; - mis->number_in_stack = 0xFFFFFFFF; - - QueuePacket(outapp2); - safe_delete(outapp2); - } - else { - //Move the amount we need, and put the rest of the stack back in the seller's inventory - // - int QuantityToRemoveFromStack = Quantity - QuantityMoved; - - ItemToTransfer->SetCharges(ItemToTransfer->GetCharges() - QuantityToRemoveFromStack); - - m_inv.PutItem(SellerSlot, *ItemToTransfer); - - database.SaveInventory(CharacterID(), ItemToTransfer, SellerSlot); - - ItemToTransfer->SetCharges(QuantityToRemoveFromStack); - - if(!Buyer->MoveItemToInventory(ItemToTransfer, true)) { - LogError("Unexpected error while moving item from seller to buyer"); - Message(Chat::Red, "Internal error while processing transaction."); - safe_delete(ItemToTransfer); + if (!FindNumberOfFreeInventorySlotsWithSizeCheck(sell_line.trade_items)) { + LogTradingDetail("Seller {} has insufficient inventory space for {} compensation items.", + GetCleanName(), + sell_line.trade_items.size() + ); + Message(Chat::Red, "Insufficient inventory space for the compensation items."); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); return; } - safe_delete(ItemToTransfer); + for (auto const &ti: sell_line.trade_items) { + std::unique_ptr inst( + database.CreateItem( + ti.item_id, + ti.item_quantity * + sell_line.seller_quantity + ) + ); - auto outapp2 = new EQApplicationPacket(OP_DeleteItem, sizeof(MoveItem_Struct)); + if (inst.get()->GetItem()) { + buyer->RemoveItem(ti.item_id, ti.item_quantity * sell_line.seller_quantity); + if (!PutItemInInventoryWithStacking(inst.get())) { + Message(Chat::Red, "Error putting item in your inventory."); + buyer->PutItemInInventoryWithStacking(inst.get()); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + return; + } + } + } - MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer; - mis->from_slot = SellerSlot; - mis->to_slot = 0xFFFFFFFF; - mis->number_in_stack = 0xFFFFFFFF; + std::unique_ptr buy_inst( + database.CreateItem( + sell_line.item_id, + sell_line.seller_quantity + ) + ); + RemoveItem(sell_line.item_id, sell_line.seller_quantity); + if (buy_inst->IsStackable()) { + if (!buyer->PutItemInInventoryWithStacking(buy_inst.get())) { + buyer->Message(Chat::Red, "Error putting item in your inventory."); + PutItemInInventoryWithStacking(buy_inst.get()); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + return; + } + } + else { + for (int i = 1; i <= sell_line.seller_quantity; i++) { + buy_inst->SetCharges(1); + if (!buyer->PutItemInInventoryWithStacking(buy_inst.get())) { + buyer->Message(Chat::Red, "Error putting item in your inventory."); + PutItemInInventoryWithStacking(buy_inst.get()); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + return; + } + } + } - for(int i = 0; i < QuantityToRemoveFromStack; i++) - QueuePacket(outapp2); + uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; + AddMoneyToPPWithOverflow(total_cost, false); + buyer->TakeMoneyFromPPWithOverFlow(total_cost, false); - safe_delete(outapp2); + if (player_event_logs.IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) { + PlayerEvent::BarterTransaction e{}; + e.status = "Successful Barter Transaction"; + e.item_id = sell_line.item_id; + e.item_quantity = sell_line.seller_quantity; + e.item_name = sell_line.item_name; + e.trade_items = sell_line.trade_items; + for (auto &t: e.trade_items) { + t *= sell_line.seller_quantity; + } + e.total_cost = total_cost; + e.buyer_name = buyer->GetCleanName(); + e.seller_name = GetCleanName(); + RecordPlayerEventLog(PlayerEvent::BARTER_TRANSACTION, e); + } - QuantityMoved = Quantity; + SendWindowUpdatesToSellerAndBuyer(sell_line); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Success, + Barter_Success + ); + buyer->SendBarterBuyerClientMessage( + sell_line, + Barter_BuyerTransactionComplete, + Barter_Success, + Barter_Success + ); + break; + } + case BarterOutsideBazaar: { + bool seller_error = false; + auto buyer_time = BuyerRepository::GetTransactionDate(database, sell_line.buyer_id); + + if (buyer_time > GetBarterTime()) { + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_DataOutOfDate + ); + return; + } + + if (sell_line.trade_items.size() > 0) { + Message(Chat::Red, "You must visit the buyer directly when receiving compensation items."); + seller_error = true; + } + + auto buy_item_slot_id = GetInv().HasItem( + sell_line.item_id, + sell_line.seller_quantity, + invWherePersonal + ); + auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id); + if (!buy_item) { + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_SellerDoesNotHaveItem + ); + break; + } + + if (seller_error) { + LogTradingDetail("Seller Error [{}] Sell/Buy Transaction Failed.", + seller_error + ); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + return; + } + + BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr)); + + auto server_packet = std::make_unique( + ServerOP_BuyerMessaging, + sizeof(BuyerMessaging_Struct) + ); + + auto data = (BuyerMessaging_Struct *) server_packet->pBuffer; + + data->action = Barter_SellItem; + data->buyer_entity_id = sell_line.buyer_entity_id; + data->buyer_id = sell_line.buyer_id; + data->seller_entity_id = GetID(); + data->buy_item_id = sell_line.item_id; + data->buy_item_qty = sell_line.item_quantity; + data->buy_item_cost = sell_line.item_cost; + data->buy_item_icon = sell_line.item_icon; + data->zone_id = GetZoneID(); + data->slot = sell_line.slot; + data->seller_quantity = sell_line.seller_quantity; + strn0cpy(data->item_name, sell_line.item_name, sizeof(data->item_name)); + strn0cpy(data->buyer_name, sell_line.buyer_name.c_str(), sizeof(data->buyer_name)); + strn0cpy(data->seller_name, GetCleanName(), sizeof(data->seller_name)); + + worldserver.SendPacket(server_packet.get()); + + break; } } - } - - Buyer->TakeMoneyFromPP(Quantity * Price); - - AddMoneyToPP(Quantity * Price); - - if(RuleB(Bazaar, AuditTrail)) - BazaarAuditTrail(GetName(), Buyer->GetName(), ItemName, Quantity, Quantity * Price, 1); - - // We now send a packet to the Seller, which causes it to display 'You have sold to for ' - // - // The PacketLength of 1016 is from the only instance of this packet I have seen, which is from Live, November 2008 - // The Titanium/6.2 struct is slightly different in that it appears to use fixed length strings instead of variable - // length as used on Live. The extra space in the packet is also likely to be used for Item compensation, if we ever - // implement that. - // - uint32 PacketLength = 1016; - - auto outapp = new EQApplicationPacket(OP_Barter, PacketLength); - - Buf = (char *)outapp->pBuffer; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_SellerTransactionComplete); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity * Price); - - if (ClientVersion() >= EQ::versions::ClientVersion::SoD) - { - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); // Think this is the upper 32 bits of a 64 bit price - } - - sprintf(Buf, "%s", Buyer->GetName()); Buf += 64; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0x01); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00); - - sprintf(Buf, "%s", ItemName); Buf += 64; - - QueuePacket(outapp); - - // This next packet goes to the Buyer and produces the 'You've bought from for ' - // - - Buf = (char *)outapp->pBuffer; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerTransactionComplete); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity * Price); - - if (Buyer->ClientVersion() >= EQ::versions::ClientVersion::SoD) - { - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); // Think this is the upper 32 bits of a 64 bit price - } - - sprintf(Buf, "%s", GetName()); Buf += 64; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0x01); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00); - - sprintf(Buf, "%s", ItemName); Buf += 64; - - Buyer->QueuePacket(outapp); - - safe_delete(outapp); - - // Next we update the buyer table in the database to reflect the reduced quantity the Buyer wants to buy. - // - database.UpdateBuyLine(Buyer->CharacterID(), BuySlot, QtyBuyerWants - Quantity); - - // Next we update the Seller's Barter Window to reflect the reduced quantity the Buyer is now looking to buy. - // - auto outapp3 = new EQApplicationPacket(OP_Barter, 936); - - Buf = (char *)outapp3->pBuffer; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerInspectWindow); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // Unknown - VARSTRUCT_ENCODE_TYPE(uint32, Buf,ItemID); - VARSTRUCT_ENCODE_STRING(Buf, ItemName); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, item->Icon); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, QtyBuyerWants - Quantity); - - // If the amount we have just sold completely satisfies the quantity the Buyer was looking for, - // setting the next byte to 0 will remove the item from the Barter Window. - // - if(QtyBuyerWants - Quantity > 0) { - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // 0 = Toggle Off, 1 = Toggle On - } - else { - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0); // 0 = Toggle Off, 1 = Toggle On - } - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Buyer->GetID()); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); - - VARSTRUCT_ENCODE_STRING(Buf, Buyer->GetName()); - - QueuePacket(outapp3); - safe_delete(outapp3); - - // The next packet updates the /buyer window with the reduced quantity, and toggles the buy line off if the - // quantity they wanted to buy has been met. - // - auto outapp4 = new EQApplicationPacket(OP_Barter, 936); - - Buf = (char*)outapp4->pBuffer; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerItemUpdate); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, ItemID); - VARSTRUCT_ENCODE_STRING(Buf, ItemName); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, item->Icon); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, QtyBuyerWants - Quantity); - - if((QtyBuyerWants - Quantity) > 0) { - - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // 0 = Toggle Off, 1 = Toggle On - } - else { - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0); // 0 = Toggle Off, 1 = Toggle On - } - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x08f4); // Unknown - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); - VARSTRUCT_ENCODE_STRING(Buf, Buyer->GetName()); - - Buyer->QueuePacket(outapp4); - safe_delete(outapp4); - - return; } void Client::SendBuyerPacket(Client* Buyer) { @@ -2810,160 +2633,266 @@ void Client::SendBuyerPacket(Client* Buyer) { safe_delete(outapp); } -void Client::ToggleBuyerMode(bool TurnOn) { +void Client::ToggleBuyerMode(bool status) +{ + auto outapp = std::make_unique(OP_Barter, sizeof(BuyerSetAppearance_Struct)); + auto data = (BuyerSetAppearance_Struct *) outapp->pBuffer; - auto outapp = new EQApplicationPacket(OP_Barter, 13 + strlen(GetName())); + data->action = Barter_BuyerAppearance; + data->entity_id = GetID(); - char* Buf = (char*)outapp->pBuffer; + if (status && IsInBuyerSpace()) { + SetBuyerID(CharacterID()); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerAppearance); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, GetID()); + BuyerRepository::Buyer b{}; + b.id = 0; + b.char_id = GetBuyerID(); + b.char_entity_id = GetID(); + b.char_zone_id = GetZoneID(); + b.char_zone_instance_id = GetInstanceID(); + b.char_name = GetCleanName(); + b.transaction_date = time(nullptr); + BuyerRepository::DeleteBuyer(database, GetBuyerID()); + BuyerRepository::InsertOne(database, b); - if(TurnOn) { - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x01); - } - else { - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00); - database.DeleteBuyLines(CharacterID()); + data->status = BuyerBarter::On; SetCustomerID(0); - } - - VARSTRUCT_ENCODE_STRING(Buf, GetName()); - - entity_list.QueueClients(this, outapp, false); - - safe_delete(outapp); - - Buyer = TurnOn; -} - -void Client::UpdateBuyLine(const EQApplicationPacket *app) { - - // This method is called when: - // - // /buyer mode is first turned on, once for each item - // A BuyLine is toggled on or off in the/buyer window. - // - char* Buf = (char*)app->pBuffer; - - char ItemName[64]; - - /*uint32 Action =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused - uint32 BuySlot = VARSTRUCT_DECODE_TYPE(uint32, Buf); - uint8 Unknown009 = VARSTRUCT_DECODE_TYPE(uint8, Buf); - uint32 ItemID = VARSTRUCT_DECODE_TYPE(uint32, Buf); - /* ItemName */ VARSTRUCT_DECODE_STRING(ItemName, Buf); - uint32 Icon = VARSTRUCT_DECODE_TYPE(uint32, Buf); - uint32 Quantity = VARSTRUCT_DECODE_TYPE(uint32, Buf); - uint8 ToggleOnOff = VARSTRUCT_DECODE_TYPE(uint8, Buf); - uint32 Price = VARSTRUCT_DECODE_TYPE(uint32, Buf); - /*uint32 UnknownZ =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused - uint32 ItemCount = VARSTRUCT_DECODE_TYPE(uint32, Buf); - - const EQ::ItemData *item = database.GetItem(ItemID); - - if(!item) return; - - bool LoreConflict = CheckLoreConflict(item); - - LogTrading("UpdateBuyLine: Char: [{}] BuySlot: [{}] ItemID [{}] [{}] Quantity [{}] Toggle: [{}] Price [{}] ItemCount [{}] LoreConflict [{}]", - GetName(), BuySlot, ItemID, item->Name, Quantity, ToggleOnOff, Price, ItemCount, LoreConflict); - - if((item->NoDrop != 0) && (!item->IsClassBag())&& !LoreConflict && (Quantity > 0) && HasMoney(Quantity * Price) && ToggleOnOff && (ItemCount == 0)) { - LogTrading("Adding to database"); - database.AddBuyLine(CharacterID(), BuySlot, ItemID, ItemName, Quantity, Price); - QueuePacket(app); + SendBuyerMode(true); + SendBuyerToBarterWindow(this, Barter_AddToBarterWindow); + Message(Chat::Yellow, "Barter Mode ON."); } else { - if(ItemCount > 0) { - Message(Chat::Red, "Buy line %s disabled as Item Compensation is not currently supported.", ItemName); - } else if(Quantity <= 0) { - Message(Chat::Red, "Buy line %s disabled as the quantity is invalid.", ItemName); - } else if(LoreConflict) { - Message(Chat::Red, "Buy line %s disabled as the item is LORE and you have one already.", ItemName); - } else if(item->NoDrop == 0) { - Message(Chat::Red, "Buy line %s disabled as the item is NODROP.", ItemName); - } else if(item->IsClassBag()) { - Message(Chat::Red, "Buy line %s disabled as the item is a Bag.", ItemName); - } else if(ToggleOnOff) { - Message(Chat::Red, "Buy line %s disabled due to insufficient funds.", ItemName); - } else { - database.RemoveBuyLine(CharacterID(), BuySlot); + data->status = BuyerBarter::Off; + BuyerRepository::DeleteBuyer(database, GetBuyerID()); + SetCustomerID(0); + SendBuyerToBarterWindow(this, Barter_RemoveFromBarterWindow); + SendBuyerMode(false); + SetBuyerID(0); + if (!IsInBuyerSpace()) { + Message(Chat::Red, "You must be in a Barter Stall to start Barter Mode."); } - - auto outapp = new EQApplicationPacket(OP_Barter, 936); - - Buf = (char*)outapp->pBuffer; - - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerItemUpdate); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, Unknown009); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, ItemID); - VARSTRUCT_ENCODE_STRING(Buf, ItemName); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Icon); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity); - VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0); // Toggle the Buy Line off in the client - VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price); - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x08f4); // Unknown - VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); - VARSTRUCT_ENCODE_STRING(Buf, GetName()); - - QueuePacket(outapp); - safe_delete(outapp); + Message(Chat::Yellow, fmt::format("Barter Mode OFF. Buy lines deactivated.").c_str()); } + entity_list.QueueClients(this, outapp.get(), false); } -void Client::BuyerItemSearch(const EQApplicationPacket *app) { +void Client::ModifyBuyLine(const EQApplicationPacket *app) +{ + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + BuyerBuyLines_Struct bl{}; + auto in = (BuyerGeneric_Struct *) app->pBuffer; + EQ::Util::MemoryStreamReader ss_in( + reinterpret_cast(in->payload), + app->size - sizeof(BuyerGeneric_Struct) + ); + cereal::BinaryInputArchive ar(ss_in); + ar(bl); - BuyerItemSearch_Struct* bis = (BuyerItemSearch_Struct*)app->pBuffer; - - auto outapp = new EQApplicationPacket(OP_Barter, sizeof(BuyerItemSearchResults_Struct)); - - BuyerItemSearchResults_Struct* bisr = (BuyerItemSearchResults_Struct*)outapp->pBuffer; - - const EQ::ItemData* item = 0; - - int Count=0; - - char Name[64]; - char Criteria[255]; - - strn0cpy(Criteria, bis->SearchString, sizeof(Criteria)); - - strupr(Criteria); - - char* pdest; - - uint32 it = 0; - - while ((item = database.IterateItems(&it))) { - - strn0cpy(Name, item->Name, sizeof(Name)); - - strupr(Name); - - pdest = strstr(Name, Criteria); - - if (pdest != nullptr) { - sprintf(bisr->Results[Count].ItemName, "%s", item->Name); - bisr->Results[Count].ItemID = item->ID; - bisr->Results[Count].Unknown068 = item->Icon; - bisr->Results[Count].Unknown072 = 0x00000000; - Count++; + if (bl.buy_lines.empty()) { + return; + } + + BuyerRepository::UpdateTransactionDate(database, GetBuyerID(), time(nullptr)); + int64 current_total_cost = 0; + bool pass = false; + + auto current_buy_lines = BuyerBuyLinesRepository::GetBuyLines(database, CharacterID()); + + std::map item_map; + BuildBuyLineMapFromVector(item_map, current_buy_lines); + + current_total_cost = ValidateBuyLineCost(item_map); + + auto buy_line = bl.buy_lines.front(); + auto it = std::find_if( + current_buy_lines.cbegin(), + current_buy_lines.cend(), + [&](BuyerLineItems_Struct bl) { + return bl.slot == buy_line.slot; + } + ); + + if (buy_line.item_toggle) { + current_total_cost += buy_line.item_cost * buy_line.item_quantity; + if (it != std::end(current_buy_lines)) { + current_total_cost -= it->item_cost * it->item_quantity; + if (current_total_cost > GetCarriedMoney()) { + buy_line.item_cost = it->item_cost; + buy_line.item_quantity = it->item_quantity; + Message( + Chat::Red, + fmt::format( + "You currently do not have sufficient funds to support your buy lines. You have {} and need {}", + DetermineMoneyString(GetCarriedMoney()), + DetermineMoneyString(current_total_cost)).c_str() + ); + SendBuyLineUpdate(buy_line); + return; + } + else { + RemoveItemFromBuyLineMap(item_map, *it); + BuildBuyLineMapFromVector(item_map, bl.buy_lines); + } + } + else { + BuildBuyLineMapFromVector(item_map, bl.buy_lines); + } + } + else { + current_total_cost -= static_cast(buy_line.item_cost) * static_cast(buy_line.item_quantity); + std::map item_map_tmp; + BuildBuyLineMapFromVector(item_map_tmp, bl.buy_lines); + if (ValidateBuyLineItems(item_map_tmp)) { + pass = true; + } + } + + if (current_total_cost > static_cast(GetCarriedMoney())) { + Message( + Chat::Red, + fmt::format( + "You currently do not have sufficient funds to support your buy lines. You have {} and need {}", + DetermineMoneyString(GetCarriedMoney()), + DetermineMoneyString(current_total_cost)).c_str() + ); + buy_line.item_toggle = 0; + SendBuyLineUpdate(buy_line); + return; + } + + bool buyer_error = false; + + if (!ValidateBuyLineItems(item_map)) { + buy_line.item_toggle = 0; + } + + buy_line.item_icon = database.GetItem(buy_line.item_id)->Icon; + if ((buy_line.item_toggle && it != std::end(current_buy_lines)) || pass) { + BuyerBuyLinesRepository::ModifyBuyLine(database, buy_line, GetBuyerID()); + Message(Chat::Yellow, fmt::format("Buy line for {} modified.", buy_line.item_name).c_str()); + } + else if (buy_line.item_toggle && it == std::end(current_buy_lines)) { + BuyerBuyLinesRepository::CreateBuyLine(database, buy_line, GetBuyerID()); + Message(Chat::Yellow, fmt::format("Buy line for {} enabled.", buy_line.item_name).c_str()); + } + else if (!buy_line.item_toggle) { + BuyerBuyLinesRepository::DeleteBuyLine(database, GetBuyerID(), buy_line.slot); + Message(Chat::Yellow, fmt::format("Buy line for {} disabled.", buy_line.item_name).c_str()); + } + else { + BuyerBuyLinesRepository::DeleteBuyLine(database, GetBuyerID(), buy_line.slot); + Message( + Chat::Yellow, + fmt::format("Unhandled modification. Buy line for {} disabled.", buy_line.item_name).c_str()); + } + + SendBuyLineUpdate(buy_line); + + if (IsThereACustomer()) { + auto customer = entity_list.GetClientByID(GetCustomerID()); + if (!customer) { + return; + } + + auto it = std::find_if( + current_buy_lines.cbegin(), + current_buy_lines.cend(), + [&](BuyerLineItems_Struct bl) { + return bl.slot == buy_line.slot; + } + ); + if (it == std::end(current_buy_lines) && !buy_line.item_toggle) { + return; + } + + std::stringstream ss_customer{}; + cereal::BinaryOutputArchive ar_customer(ss_customer); + + BuyerLineItems_Struct blis{}; + blis.enabled = buy_line.enabled; + blis.item_cost = buy_line.item_cost; + blis.item_icon = buy_line.item_icon; + blis.item_id = buy_line.item_id; + blis.item_quantity = buy_line.item_quantity; + blis.item_toggle = buy_line.item_toggle; + blis.slot = buy_line.slot; + blis.item_name = buy_line.item_name; + for (auto const &i: buy_line.trade_items) { + BuyerLineTradeItems_Struct bltis{}; + bltis.item_icon = i.item_icon; + bltis.item_id = i.item_id; + bltis.item_quantity = i.item_quantity; + bltis.item_name = i.item_name; + blis.trade_items.push_back(bltis); + } + + { ar_customer(blis); } + + auto packet = std::make_unique( + OP_BuyerItems, + ss_customer.str().length() + + sizeof(BuyerGeneric_Struct) + ); + auto emu = (BuyerGeneric_Struct *) packet->pBuffer; + + emu->action = Barter_BuyerInspectBegin; + memcpy(emu->payload, ss_customer.str().data(), ss_customer.str().length()); + + customer->QueuePacket(packet.get()); + + ss_customer.str(""); + ss_customer.clear(); } - if (Count == MAX_BUYER_ITEMSEARCH_RESULTS) - break; } - if (Count == MAX_BUYER_ITEMSEARCH_RESULTS) - Message(Chat::Yellow, "Your search returned more than %i results. Only the first %i are displayed.", - MAX_BUYER_ITEMSEARCH_RESULTS, MAX_BUYER_ITEMSEARCH_RESULTS); + return; +} - bisr->Action = Barter_BuyerSearch; - bisr->ResultCount = Count; +void Client::BuyerItemSearch(const EQApplicationPacket *app) +{ + auto bis = (BuyerItemSearch_Struct *) app->pBuffer; + const EQ::ItemData *item = 0; + uint32 it = 0; - QueuePacket(outapp); - safe_delete(outapp); + BuyerItemSearchResults_Struct bisr{}; + + while ((item = database.IterateItems(&it)) && bisr.results.size() < RuleI(Bazaar, MaxBuyerInventorySearchResults)) { + if (!item->NoDrop) { + continue; + } + + auto item_name_match = std::strstr( + Strings::ToLower(item->Name).c_str(), + Strings::ToLower(bis->search_string).c_str() + ); + + if (item_name_match) { + BuyerItemSearchResultEntry_Struct bisre{}; + bisre.item_id = item->ID; + bisre.item_icon = item->Icon; + strn0cpy(bisre.item_name, item->Name, sizeof(bisre.item_name)); + bisr.results.push_back(bisre); + } + } + + bisr.action = Barter_BuyerSearchResults; + bisr.result_count = bisr.results.size(); + + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + { ar(bisr); } + + uint32 packet_size = sizeof(BuyerGeneric_Struct) + ss.str().length(); + auto outapp = std::make_unique(OP_Barter, packet_size); + auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; + + emu->action = Barter_BuyerSearchResults; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + QueuePacket(outapp.get()); + + ss.str(""); + ss.clear(); } const std::string &Client::GetMailKeyFull() const @@ -3379,7 +3308,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati tbs->trader_id, tbs->serial_number ); - in->method = ByParcel; + in->method = BazaarByParcel; in->sub_action = DataOutDated; TradeRequestFailed(app); return; @@ -3391,7 +3320,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati tbs->trader_id, tbs->serial_number ); - in->method = ByParcel; + in->method = BazaarByParcel; in->sub_action = DataOutDated; TradeRequestFailed(app); return; @@ -3417,7 +3346,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati trader_item.item_id, trader_item.item_sn ); - in->method = ByParcel; + in->method = BazaarByParcel; in->sub_action = Failed; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); @@ -3473,7 +3402,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati ) } ); - in->method = ByParcel; + in->method = BazaarByParcel; in->sub_action = InsufficientFunds; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); @@ -3507,7 +3436,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati GetCleanName(), buy_item->GetItem()->Name ); - in->method = ByParcel; + in->method = BazaarByParcel; in->sub_action = TooManyParcels; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); @@ -3537,7 +3466,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati parcel_out.quantity ); Message(Chat::Yellow, "Unable to save parcel to the database. Please contact an administrator."); - in->method = ByParcel; + in->method = BazaarByParcel; in->sub_action = Failed; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); @@ -3600,3 +3529,732 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati worldserver.SendPacket(out_server.release()); } + +void Client::SetBuyerWelcomeMessage(const char *welcome_message) +{ + BuyerRepository::UpdateWelcomeMessage(database, CharacterID(), welcome_message); +} + +void Client::SendBuyerGreeting(uint32 buyer_id) +{ + auto buyer = BuyerRepository::GetWhere(database, fmt::format("`char_id` = '{}'", buyer_id)); + if (buyer.empty()) { + Message(Chat::White, "Welcome!"); + return; + } + Message(Chat::White, buyer.front().welcome_message.c_str()); +} + +void Client::SendSellerBrowsing(const std::string &browser) +{ + auto outapp = std::make_unique(OP_Barter, sizeof(BuyerBrowsing_Struct)); + auto eq = (BuyerBrowsing_Struct *) outapp->pBuffer; + + eq->action = Barter_SellerBrowsing; + strn0cpy(eq->char_name, browser.c_str(), sizeof(eq->char_name)); + + QueuePacket(outapp.get()); +} + +void Client::SendBuyerMode(bool status) +{ + auto outapp = std::make_unique(OP_Barter, 4); + auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; + + emu->action = status ? Barter_BuyerModeOn : Barter_BuyerModeOff; + + QueuePacket(outapp.get()); +} + +bool Client::IsInBuyerSpace() +{ +#define BUYER_DOOR_ARC_RADIUS_HIGH 91 +#define BUYER_DOOR_ARC_RADIUS_LOW 71 +#define BUYER_DOOR_OPEN_TYPE 155 +#define TRADER_DOOR_OPEN_TYPE 153 + + struct BuyerDoorDataStruct { + uint32 door_id; + uint32 arc_offset; + }; + + std::vector buyer_door_data = { + {.door_id = 2}, {.arc_offset = 90},{.door_id = 3} ,{.arc_offset = 0} ,{.door_id = 4}, {.arc_offset = 0}, + {.door_id = 5}, {.arc_offset = 0} ,{.door_id = 6} ,{.arc_offset = 90},{.door_id = 7}, {.arc_offset = 0}, + {.door_id = 8}, {.arc_offset = 0} ,{.door_id = 9} ,{.arc_offset = 0} ,{.door_id = 10}, {.arc_offset = 0}, + {.door_id = 11},{.arc_offset = 0} ,{.door_id = 12},{.arc_offset = 0} ,{.door_id = 13}, {.arc_offset = 0}, + {.door_id = 14},{.arc_offset = 0} ,{.door_id = 15},{.arc_offset = 0} ,{.door_id = 16}, {.arc_offset = 90}, + {.door_id = 17},{.arc_offset = 0} ,{.door_id = 18},{.arc_offset = 0} ,{.door_id = 19}, {.arc_offset = 0}, + {.door_id = 20},{.arc_offset = 0} ,{.door_id = 21},{.arc_offset = 0} ,{.door_id = 22}, {.arc_offset = 0}, + {.door_id = 23},{.arc_offset = 0} ,{.door_id = 24},{.arc_offset = 0} ,{.door_id = 25}, {.arc_offset = 0}, + {.door_id = 26},{.arc_offset = 0} ,{.door_id = 27},{.arc_offset = 0} ,{.door_id = 28}, {.arc_offset = 0}, + {.door_id = 29},{.arc_offset = 90},{.door_id = 30},{.arc_offset = 0} ,{.door_id = 31}, {.arc_offset = 0}, + {.door_id = 32},{.arc_offset = 0} ,{.door_id = 33},{.arc_offset = 0} ,{.door_id = 34}, {.arc_offset = 0}, + {.door_id = 35},{.arc_offset = 0} ,{.door_id = 36},{.arc_offset = 90},{.door_id = 37}, {.arc_offset = 0}, + {.door_id = 38},{.arc_offset = 0} ,{.door_id = 39},{.arc_offset = 0} ,{.door_id = 40}, {.arc_offset = 0}, + {.door_id = 41},{.arc_offset = 0} ,{.door_id = 42},{.arc_offset = 0} ,{.door_id = 43}, {.arc_offset = 90}, + {.door_id = 44},{.arc_offset = 0} ,{.door_id = 45},{.arc_offset = 0} ,{.door_id = 46}, {.arc_offset = 0}, + {.door_id = 47},{.arc_offset = 0} ,{.door_id = 48},{.arc_offset = 0} ,{.door_id = 49}, {.arc_offset = 0}, + {.door_id = 50},{.arc_offset = 90},{.door_id = 51},{.arc_offset = 90},{.door_id = 52}, {.arc_offset = 0}, + {.door_id = 53},{.arc_offset = 0} ,{.door_id = 54},{.arc_offset = 0}, {.door_id = 55}, {.arc_offset = 0}, + {.door_id = 56},{.arc_offset = 0} ,{.door_id = 57},{.arc_offset = 0}, {.door_id = 122},{.arc_offset = 0} + }; + + auto m_location = GetPosition(); + + for (auto const &d: buyer_door_data) { + auto door = entity_list.GetDoorsByDoorID(d.door_id); + if (door && IsWithinCircularArc( + door->GetPosition(), + m_location, + d.arc_offset, + BUYER_DOOR_ARC_RADIUS_HIGH, + BUYER_DOOR_ARC_RADIUS_LOW + ) + ) { + return true; + } + } + + for (auto const& d:entity_list.GetDoorsList()) { + if (d.second->GetOpenType() == DoorType::BuyerStall) { + if (IsWithinSquare(d.second->GetPosition(), d.second->GetSize(), GetPosition())) { + return true; + } + } + } + + return false; +} + +void Client::CreateStartingBuyLines(const EQApplicationPacket *app) +{ + if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { + BuyerBuyLines_Struct bl{}; + auto in = (BuyerGeneric_Struct *) app->pBuffer; + EQ::Util::MemoryStreamReader ss_in( + reinterpret_cast(in->payload), + app->size - sizeof(BuyerGeneric_Struct)); + cereal::BinaryInputArchive ar(ss_in); + ar(bl); + + if (bl.buy_lines.empty()) { + return; + } + + std::map item_map{}; + + if (!BuildBuyLineMap(item_map, bl)) { + ToggleBuyerMode(false); + return; + } + + auto proposed_total_cost = ValidateBuyLineCost(item_map); + if (proposed_total_cost == 0) { + ToggleBuyerMode(false); + return; + } + + if (!ValidateBuyLineItems(item_map)) { + ToggleBuyerMode(false); + return; + } + + std::stringstream ss_out{}; + cereal::BinaryOutputArchive ar_out(ss_out); + + for (auto &b: bl.buy_lines) { + BuyerBuyLinesRepository::CreateBuyLine(database, b, CharacterID()); + + { ar_out(b); } + + uint32 packet_size = ss_out.str().length() + sizeof(BuyerGeneric_Struct); + auto out = std::make_unique(OP_BuyerItems, packet_size); + auto data = (BazaarSearchMessaging_Struct *) out->pBuffer; + + data->action = Barter_BuyerItemUpdate; + memcpy(data->payload, ss_out.str().data(), ss_out.str().length()); + QueuePacket(out.get()); + + ss_out.str(""); + ss_out.clear(); + } + + Message(Chat::Yellow, fmt::format("{} buy lines enabled.", bl.buy_lines.size()).c_str()); + } +} + +void Client::SendBuyLineUpdate(const BuyerLineItems_Struct &buy_line) +{ + std::stringstream ss_out{}; + cereal::BinaryOutputArchive ar_out(ss_out); + + { ar_out(buy_line); } + + uint32 packet_size = ss_out.str().length() + sizeof(BuyerGeneric_Struct); + auto out = std::make_unique(OP_BuyerItems, packet_size); + auto data = (BazaarSearchMessaging_Struct *) out->pBuffer; + + data->action = Barter_BuyerItemUpdate; + memcpy(data->payload, ss_out.str().data(), ss_out.str().length()); + QueuePacket(out.get()); + + ss_out.str(""); + ss_out.clear(); +} + +void Client::CheckIfMovedItemIsPartOfBuyLines(uint32 item_id) +{ + auto b_trade_items = BuyerTradeItemsRepository::GetTradeItems(database, GetBuyerID()); + if (b_trade_items.empty()) { + return; + } + + auto it = std::find_if( + b_trade_items.cbegin(), + b_trade_items.cend(), + [&](const BaseBuyerTradeItemsRepository::BuyerTradeItems bti) { + return bti.item_id == item_id; + } + ); + if (it != std::end(b_trade_items)) { + auto item = GetInv().GetItem(GetInv().HasItem(item_id, 1, invWherePersonal)); + if (!item) { + return; + } + + Message( + Chat::Red, + fmt::format( + "You moved an item ({}) that is part of an active buy line.", + item->GetItem()->Name + ).c_str() + ); + ToggleBuyerMode(false); + } +} + +void Client::SendWindowUpdatesToSellerAndBuyer(BuyerLineSellItem_Struct &blsi) +{ + auto buyer = entity_list.GetClientByID(blsi.buyer_entity_id); + auto seller = this; + if (!buyer || !seller) { + return; + } + + if (blsi.item_quantity - blsi.seller_quantity <= 0) { + auto outapp = std::make_unique( + OP_BuyerItems, + sizeof(BuyerRemoveItemFromMerchantWindow_Struct) + ); + auto data = (BuyerRemoveItemFromMerchantWindow_Struct *) outapp->pBuffer; + + data->action = Barter_RemoveFromMerchantWindow; + data->buy_slot_id = blsi.slot; + QueuePacket(outapp.get()); + + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + + BuyerLineItems_Struct bl{}; + bl.enabled = 0; + bl.item_cost = blsi.item_cost; + bl.item_icon = blsi.item_icon; + bl.item_id = blsi.item_id; + bl.item_quantity = blsi.item_quantity - blsi.seller_quantity; + bl.item_name = blsi.item_name; + bl.item_toggle = 0; + bl.slot = blsi.slot; + + for (auto const &b: blsi.trade_items) { + BuyerLineTradeItems_Struct blti{}; + blti.item_icon = b.item_icon; + blti.item_id = b.item_id; + blti.item_quantity = b.item_quantity; + blti.item_name = b.item_name; + bl.trade_items.push_back(blti); + } + + { ar(bl); } + + uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct); + outapp = std::make_unique(OP_BuyerItems, packet_size); + auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; + + emu->action = Barter_BuyerItemUpdate; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + buyer->QueuePacket(outapp.get()); + BuyerBuyLinesRepository::DeleteBuyLine(database, buyer->CharacterID(), blsi.slot); + } + else { + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + + BuyerLineItems_Struct bli{}; + bli.enabled = 1; + bli.item_cost = blsi.item_cost; + bli.item_icon = blsi.item_icon; + bli.item_id = blsi.item_id; + bli.item_quantity = blsi.item_quantity - blsi.seller_quantity; + bli.item_toggle = 1; + bli.slot = blsi.slot; + bli.item_name = blsi.item_name; + for (auto const &b: blsi.trade_items) { + BuyerLineTradeItems_Struct blti{}; + blti.item_id = b.item_id; + blti.item_icon = b.item_icon; + blti.item_quantity = b.item_quantity; + blti.item_name = b.item_name; + bli.trade_items.push_back(blti); + } + { ar(bli); } + + uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct); + auto outapp = std::make_unique(OP_BuyerItems, packet_size); + auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; + + emu->action = Barter_BuyerInspectBegin; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + QueuePacket(outapp.get()); + + outapp = std::make_unique(OP_BuyerItems, packet_size); + emu = (BuyerGeneric_Struct *) outapp->pBuffer; + + emu->action = Barter_BuyerItemUpdate; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + buyer->QueuePacket(outapp.get()); + + BuyerBuyLinesRepository::ModifyBuyLine(database, bli, buyer->GetBuyerID()); + } +} + +void Client::SendBuyerToBarterWindow(Client *buyer, uint32 action) +{ + auto server_packet = std::make_unique( + ServerOP_BuyerMessaging, + sizeof(BuyerMessaging_Struct) + ); + auto data = (BuyerMessaging_Struct *) server_packet->pBuffer; + + data->action = action; + data->zone_id = buyer->GetZoneID(); + data->buyer_id = buyer->GetBuyerID(); + data->buyer_entity_id = buyer->GetID(); + strn0cpy(data->buyer_name, buyer->GetCleanName(), sizeof(data->buyer_name)); + + worldserver.SendPacket(server_packet.get()); +} + +void Client::SendBulkBazaarBuyers() +{ + auto results = BuyerRepository::All(database); + + if (results.empty()) { + return; + } + + auto outapp = std::make_unique(OP_Barter, sizeof(BuyerAddBuyertoBarterWindow_Struct)); + auto emu = (BuyerAddBuyertoBarterWindow_Struct *) outapp->pBuffer; + + for (auto const &b: results) { + auto buyer = entity_list.GetClientByCharID(b.char_id); + emu->action = Barter_AddToBarterWindow; + emu->buyer_id = b.char_id; + emu->buyer_entity_id = buyer ? buyer->GetID() : 0; + emu->zone_id = buyer ? buyer->GetZoneID() : 0; + strn0cpy(emu->buyer_name, b.char_name.c_str(), sizeof(emu->buyer_name)); + + QueuePacket(outapp.get()); + } +} + +void Client::SendBarterBuyerClientMessage( + BuyerLineSellItem_Struct &blsi, + BarterBuyerActions action, + BarterBuyerSubActions sub_action, + BarterBuyerSubActions error_code +) +{ + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + + blsi.sub_action = sub_action; + blsi.error_code = error_code; + + { ar(blsi); } + + uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct); + auto outapp = std::make_unique(OP_BuyerItems, packet_size); + auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; + + emu->action = action; + memcpy(emu->payload, ss.str().data(), ss.str().length()); + + QueuePacket(outapp.get()); +} + +bool Client::BuildBuyLineMap(std::map &item_map, BuyerBuyLines_Struct &bl) +{ + bool buyer_error = false; + + for (auto const &b: bl.buy_lines) { + if (item_map.contains(b.item_id) && item_map[b.item_id].item_cost > 0) { + Message( + Chat::Red, + fmt::format( + "You cannot have two buy lines for the same item {}. Buy line not possible.", + b.item_name + ).c_str() + ); + buyer_error = true; + break; + } + BuylineItemDetails_Struct t = {b.item_quantity * b.item_cost, b.item_quantity}; + item_map.emplace(b.item_id, t); + for (auto const &i: b.trade_items) { + if (item_map.contains(i.item_id) && item_map[i.item_id].item_cost > 0) { + Message( + Chat::Red, + fmt::format( + "You cannot buy {} and offer the same item as compensation. Buy line not possible.", + i.item_name + ).c_str() + ); + buyer_error = true; + break; + } + if (item_map.contains(i.item_id)) { + item_map[i.item_id].item_quantity += i.item_quantity * b.item_quantity; + continue; + } + t = {0, i.item_quantity * b.item_quantity}; + item_map.emplace(i.item_id, t); + } + } + + if (buyer_error) { + return false; + } + + return true; +} + +bool Client::BuildBuyLineMapFromVector( + std::map &item_map, + std::vector &bl +) +{ + + bool buyer_error = false; + + for (auto const &b: bl) { + if (item_map.contains(b.item_id) && item_map[b.item_id].item_cost > 0) { + Message( + Chat::Red, + fmt::format( + "You cannot have two buy lines for the same item {}. Buy line not possible.", + b.item_name + ).c_str() + ); + buyer_error = true; + break; + } + BuylineItemDetails_Struct t = {b.item_quantity * b.item_cost, b.item_quantity}; + item_map.emplace(b.item_id, t); + for (auto const &i: b.trade_items) { + if (item_map.contains(i.item_id) && item_map[i.item_id].item_cost > 0) { + Message( + Chat::Red, + fmt::format( + "You cannot buy {} and offer the same item as compensation. Buy line not possible.", + i.item_name + ).c_str() + ); + buyer_error = true; + break; + } + if (item_map.contains(i.item_id)) { + item_map[i.item_id].item_quantity += i.item_quantity * b.item_quantity; + continue; + } + t = {0, i.item_quantity * b.item_quantity}; + item_map.emplace(i.item_id, t); + } + } + + if (buyer_error) { + return false; + } + + return true; +} + +void +Client::RemoveItemFromBuyLineMap(std::map &item_map, const BuyerLineItems_Struct &bl) +{ + if (item_map.contains(bl.item_id) && item_map[bl.item_id].item_cost > 0) { + item_map.erase(bl.item_id); + } + + for (auto const &i: bl.trade_items) { + if (item_map.contains(i.item_id) && + (item_map[i.item_id].item_quantity - (i.item_quantity * bl.item_quantity)) == 0) { + item_map.erase(i.item_id); + } + else if (item_map.contains(i.item_id)) { + item_map[i.item_id].item_quantity -= i.item_quantity * bl.item_quantity; + } + } +} + +bool Client::ValidateBuyLineItems(std::map &item_map) +{ + bool buyer_error = false; + + for (auto const &i: item_map) { + auto item = database.GetItem(i.first); + if (!item) { + buyer_error = true; + break; + } + + if (i.second.item_cost > 0) { + auto buy_item_slot_id = GetInv().HasItem(i.first, i.second.item_quantity, invWherePersonal); + auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id); + if (buy_item && CheckLoreConflict(buy_item->GetItem())) { + Message( + Chat::Red, + fmt::format( + "You already have a {}. Purchasing another will cause a lore conflict. Buy line not possible.", + buy_item->GetItem()->Name + ).c_str() + ); + buyer_error = true; + break; + } + } + if (i.second.item_cost == 0) { + if (i.second.item_quantity > 1 && CheckLoreConflict(item)) { + Message( + Chat::Red, + fmt::format( + "Your buy line requires {} {}s however the item is LORE. Buy line not possible.", + i.second.item_quantity, + item->Name + ).c_str() + ); + buyer_error = true; + break; + } + + auto buy_item_slot_id = GetInv().HasItem(i.first, i.second.item_quantity, invWherePersonal); + auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id); + + if (!buy_item) { + Message( + Chat::Red, + fmt::format( + "Your buy line(s) require a total of {} {}{} which could not be found. Buy line not possible.", + i.second.item_quantity, + item->Name, + i.second.item_quantity > 1 ? "s" : "" + ).c_str() + ); + buyer_error = true; + break; + } + + if (buy_item->IsAugmentable() && buy_item->IsAugmented()) { + Message( + Chat::Red, + fmt::format( + "You cannot offer {} because it is augmented. Buy line not possible.", + buy_item->GetItem()->Name + ).c_str() + ); + buyer_error = true; + break; + } + + if (!buy_item->IsDroppable()) { + Message( + Chat::Red, + fmt::format( + "You cannot offer {} because it is NoTrade. Buy line not possible.", + buy_item->GetItem()->Name + ).c_str()); + buyer_error = true; + break; + } + + buyer_error = false; + } + } + + return !buyer_error; +} + +int64 Client::ValidateBuyLineCost(std::map &item_map) +{ + int64 proposed_total_cost = std::accumulate( + item_map.cbegin(), + item_map.cend(), + 0, + [](auto prev_sum, const std::pair &x) { + return prev_sum + x.second.item_cost; + } + ); + + if (proposed_total_cost > GetCarriedMoney()) { + Message( + Chat::Red, + fmt::format( + "You currently do not have sufficient funds to support your buy lines. You have {} and need {}", + DetermineMoneyString(GetCarriedMoney()), + DetermineMoneyString(proposed_total_cost)).c_str() + ); + return 0; + } + + return proposed_total_cost; +} + +bool Client::DoBarterBuyerChecks(BuyerLineSellItem_Struct &sell_line) +{ + bool buyer_error = false; + auto buyer = entity_list.GetClientByID(sell_line.buyer_entity_id); + + if (!buyer) { + return false; + } + + auto buyer_time = BuyerRepository::GetTransactionDate(database, buyer->CharacterID()); + if (buyer_time > GetBarterTime()) { + if (sell_line.purchase_method == BarterByVendor) { + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Success, + Barter_DataOutOfDate + ); + return false; + } + SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_DataOutOfDate); + return false; + } + + for (auto const &ti: sell_line.trade_items) { + auto ti_slot_id = buyer->GetInv().HasItem( + ti.item_id, + ti.item_quantity * sell_line.seller_quantity, + invWherePersonal + ); + if (ti_slot_id == INVALID_INDEX) { + LogTradingDetail( + "Seller attempting to sell item [{}] to buyer [{}] though buyer no longer has compensation item [{}]", + sell_line.item_name, + buyer->GetCleanName(), + ti.item_name + ); + buyer->Message( + Chat::Red, + fmt::format( + "{} wanted to sell you {} however you no longer have compensation item {}", + sell_line.seller_name, + sell_line.item_name, + ti.item_name + ).c_str()); + buyer_error = true; + break; + } + } + + uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; + if (!buyer->HasMoney(total_cost)) { + LogTradingDetail( + "Seller attempting to sell item [{}] to buyer [{}] though buyer does not have enough money [{}]", + sell_line.item_name, + buyer->GetCleanName(), + total_cost + ); + buyer->Message( + Chat::Red, + fmt::format( + "{} wanted to sell you {} however you have insufficient funds.", + sell_line.seller_name, + sell_line.item_name + ).c_str() + ); + buyer_error = true; + } + + auto buy_item_slot_id = buyer->GetInv().HasItem( + sell_line.item_id, + sell_line.seller_quantity, + invWherePersonal + ); + auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : buyer->GetInv().GetItem(buy_item_slot_id); + if (buy_item && buyer->CheckLoreConflict(buy_item->GetItem())) { + LogTradingDetail( + "Seller attempting to sell item [{}] to buyer [{}] though buyer already has the item which is LORE.", + sell_line.item_name, + buyer->GetCleanName() + ); + buyer->Message( + Chat::Red, + fmt::format( + "{} wanted to sell you {} however you already have the LORE item.", + sell_line.seller_name, + sell_line.item_name + ).c_str() + ); + buyer_error = true; + } + + if (buyer_error) { + LogTradingDetail("Buyer error [{}] Barter Sell/Buy Transaction Failed.", buyer_error); + SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure); + return false; + } + + return true; +} + +bool Client::DoBarterSellerChecks(BuyerLineSellItem_Struct &sell_line) +{ + bool seller_error = false; + auto sell_item_slot_id = GetInv().HasItem(sell_line.item_id, sell_line.seller_quantity, invWherePersonal); + auto sell_item = sell_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(sell_item_slot_id); + if (!sell_item) { + seller_error = true; + LogTradingDetail("Seller no longer has item [{}] to sell to buyer [{}]", + sell_line.item_name, + sell_line.buyer_name + ); + SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_SellerDoesNotHaveItem + ); + } + + if (sell_item && sell_item->IsAugmentable() && sell_item->IsAugmented()) { + seller_error = true; + LogTradingDetail("Seller item [{}] is augmented therefore cannot be sold.", + sell_line.item_name + ); + Message(Chat::Red, "The item that you are trying to sell is augmented. Please remove augments first"); + } + + if (seller_error) { + LogTradingDetail("Seller Error [{}] Barter Sell/Buy Transaction Failed.", seller_error); + SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure); + return false; + } + + return true; +} \ No newline at end of file diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 4e1a75e9c..76e24c962 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -3997,6 +3997,310 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } + case ServerOP_BuyerMessaging: { + auto in = (BuyerMessaging_Struct *) pack->pBuffer; + + switch (in->action) { + case Barter_AddToBarterWindow: { + auto outapp = std::make_unique( + OP_Barter, + sizeof(BuyerAddBuyertoBarterWindow_Struct) + ); + auto emu = (BuyerAddBuyertoBarterWindow_Struct *) outapp->pBuffer; + + emu->action = Barter_AddToBarterWindow; + emu->buyer_entity_id = in->buyer_entity_id; + emu->buyer_id = in->buyer_id; + emu->zone_id = in->zone_id; + strn0cpy(emu->buyer_name, in->buyer_name, sizeof(emu->buyer_name)); + + entity_list.QueueClients(nullptr, outapp.get()); + + break; + } + case Barter_RemoveFromBarterWindow: { + auto outapp = std::make_unique( + OP_Barter, + sizeof(BuyerRemoveBuyerFromBarterWindow_Struct) + ); + auto emu = (BuyerRemoveBuyerFromBarterWindow_Struct *) outapp->pBuffer; + + emu->action = Barter_RemoveFromBarterWindow; + emu->buyer_id = in->buyer_id; + + entity_list.QueueClients(nullptr, outapp.get()); + + break; + } + case Barter_FailedTransaction: { + auto seller = entity_list.GetClientByID(in->seller_entity_id); + auto buyer = entity_list.GetClientByID(in->buyer_entity_id); + + BuyerLineSellItem_Struct sell_line{}; + sell_line.item_id = in->buy_item_id; + sell_line.item_quantity = in->buy_item_qty; + sell_line.item_cost = in->buy_item_cost; + sell_line.seller_name = in->seller_name; + sell_line.buyer_name = in->buyer_name; + sell_line.seller_quantity = in->seller_quantity; + sell_line.slot = in->slot; + strn0cpy(sell_line.item_name, in->item_name, sizeof(sell_line.item_name)); + + uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; + std::unique_ptr inst(database.CreateItem(in->buy_item_id, in->seller_quantity)); + + switch (in->sub_action) { + case Barter_FailedBuyerChecks: + case Barter_FailedSellerChecks: { + if (seller) { + LogTradingDetail("Significant barter transaction failure."); + seller->Message( + Chat::Red, + "Significant barter transaction error. Transaction rolled back." + ); + seller->SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) { + PlayerEvent::BarterTransaction e{}; + e.status = "Failed Barter Transaction"; + e.item_id = sell_line.item_id; + e.item_quantity = sell_line.seller_quantity; + e.item_name = sell_line.item_name; + e.trade_items = sell_line.trade_items; + for (auto &i: e.trade_items) { + i *= sell_line.seller_quantity; + } + e.total_cost = (uint64) sell_line.item_cost * (uint64) in->seller_quantity; + e.buyer_name = sell_line.buyer_name; + e.seller_name = sell_line.seller_name; + RecordPlayerEventLogWithClient(seller, PlayerEvent::BARTER_TRANSACTION, e); + } + } + + if (buyer) { + LogError("Significant barter transaction failure. Replacing {} and {} {} to {}", + buyer->DetermineMoneyString(total_cost), + sell_line.seller_quantity, + sell_line.item_name, + buyer->GetCleanName()); + buyer->AddMoneyToPPWithOverflow(total_cost, true); + buyer->RemoveItem(sell_line.item_id, sell_line.seller_quantity); + + buyer->Message( + Chat::Red, + "Significant barter transaction error. Transaction rolled back." + ); + buyer->SendBarterBuyerClientMessage( + sell_line, + Barter_BuyerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) { + PlayerEvent::BarterTransaction e{}; + e.status = "Failed Barter Transaction"; + e.item_id = sell_line.item_id; + e.item_quantity = sell_line.seller_quantity; + e.item_name = sell_line.item_name; + e.trade_items = sell_line.trade_items; + for (auto &i: e.trade_items) { + i *= sell_line.seller_quantity; + } + e.total_cost = (uint64) sell_line.item_cost * (uint64) in->seller_quantity; + e.buyer_name = sell_line.buyer_name; + e.seller_name = sell_line.seller_name; + RecordPlayerEventLogWithClient(buyer, PlayerEvent::BARTER_TRANSACTION, e); + } + } + break; + } + default: { + if (seller) { + seller->SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + } + + if (buyer) { + buyer->SendBarterBuyerClientMessage( + sell_line, + Barter_BuyerTransactionComplete, + Barter_Failure, + Barter_Failure + ); + } + } + } + break; + } + case Barter_SellItem: { + auto buyer = entity_list.GetClientByID(in->buyer_entity_id); + if (!buyer) { + in->action = Barter_FailedTransaction; + in->sub_action = Barter_BuyerCouldNotBeFound; + worldserver.SendPacket(pack); + return; + } + + BuyerLineSellItem_Struct sell_line{}; + sell_line.item_id = in->buy_item_id; + sell_line.item_quantity = in->buy_item_qty; + sell_line.item_cost = in->buy_item_cost; + sell_line.seller_name = in->seller_name; + sell_line.buyer_name = in->buyer_name; + sell_line.buyer_entity_id = in->buyer_entity_id; + sell_line.seller_quantity = in->seller_quantity; + sell_line.slot = in->slot; + strn0cpy(sell_line.item_name, in->item_name, sizeof(sell_line.item_name)); + + if (!buyer->DoBarterBuyerChecks(sell_line)) { + in->action = Barter_FailedTransaction; + in->sub_action = Barter_FailedBuyerChecks; + worldserver.SendPacket(pack); + break; + } + + BuyerLineSellItem_Struct blis{}; + blis.enabled = 1; + blis.item_toggle = 1; + blis.item_cost = in->buy_item_cost; + blis.item_id = in->buy_item_id; + blis.item_quantity = in->buy_item_qty; + blis.item_icon = in->buy_item_icon; + blis.slot = in->slot; + blis.seller_quantity = in->seller_quantity; + blis.buyer_entity_id = in->buyer_entity_id; + strn0cpy(blis.item_name, in->item_name, sizeof(blis.item_name)); + + uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; + std::unique_ptr inst(database.CreateItem(in->buy_item_id, in->seller_quantity)); + + if (inst->IsStackable()) { + if (!buyer->PutItemInInventoryWithStacking(inst.get())) { + buyer->Message(Chat::Red, "Error putting item in your inventory."); + buyer->AddMoneyToPPWithOverflow(total_cost, true); + in->action = Barter_FailedTransaction; + in->sub_action = Barter_FailedBuyerChecks; + worldserver.SendPacket(pack); + break; + } + } + else { + for (int i = 1; i <= sell_line.seller_quantity; i++) { + inst->SetCharges(1); + if (!buyer->PutItemInInventoryWithStacking(inst.get())) { + buyer->Message(Chat::Red, "Error putting item in your inventory."); + buyer->AddMoneyToPPWithOverflow(total_cost, true); + in->action = Barter_FailedTransaction; + in->sub_action = Barter_FailedBuyerChecks; + worldserver.SendPacket(pack); + goto exit_loop; + } + } + } + + if (!buyer->TakeMoneyFromPPWithOverFlow(total_cost, false)) { + in->action = Barter_FailedTransaction; + in->sub_action = Barter_FailedBuyerChecks; + worldserver.SendPacket(pack); + break; + } + + buyer->SendWindowUpdatesToSellerAndBuyer(blis); + buyer->SendBarterBuyerClientMessage( + sell_line, + Barter_BuyerTransactionComplete, + Barter_Success, + Barter_Success + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) { + PlayerEvent::BarterTransaction e{}; + e.status = "Successful Barter Transaction"; + e.item_id = sell_line.item_id; + e.item_quantity = sell_line.seller_quantity; + e.item_name = sell_line.item_name; + e.trade_items = sell_line.trade_items; + for (auto &i: e.trade_items) { + i *= sell_line.seller_quantity; + } + e.total_cost = (uint64) sell_line.item_cost * (uint64) in->seller_quantity; + e.buyer_name = sell_line.buyer_name; + e.seller_name = sell_line.seller_name; + RecordPlayerEventLogWithClient(buyer, PlayerEvent::BARTER_TRANSACTION, e); + } + + in->action = Barter_BuyerTransactionComplete; + worldserver.SendPacket(pack); + + exit_loop: + break; + } + case Barter_BuyerTransactionComplete: { + auto seller = entity_list.GetClientByID(in->seller_entity_id); + if (!seller) { + in->action = Barter_FailedTransaction; + in->sub_action = Barter_SellerCouldNotBeFound; + worldserver.SendPacket(pack); + return; + } + + BuyerLineSellItem_Struct sell_line{}; + sell_line.item_id = in->buy_item_id; + sell_line.item_quantity = in->buy_item_qty; + sell_line.item_cost = in->buy_item_cost; + sell_line.seller_name = in->seller_name; + sell_line.buyer_name = in->buyer_name; + sell_line.seller_quantity = in->seller_quantity; + sell_line.slot = in->slot; + strn0cpy(sell_line.item_name, in->item_name, sizeof(sell_line.item_name)); + + if (!seller->DoBarterSellerChecks(sell_line)) { + in->action = Barter_FailedTransaction; + in->action = Barter_FailedSellerChecks; + worldserver.SendPacket(pack); + return; + } + + uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; + seller->RemoveItem(in->buy_item_id, in->seller_quantity); + seller->AddMoneyToPPWithOverflow(total_cost, false); + seller->SendBarterBuyerClientMessage( + sell_line, + Barter_SellerTransactionComplete, + Barter_Success, + Barter_Success + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) { + PlayerEvent::BarterTransaction e{}; + e.status = "Successful Barter Transaction"; + e.item_id = sell_line.item_id; + e.item_quantity = sell_line.seller_quantity; + e.item_name = sell_line.item_name; + e.trade_items = sell_line.trade_items; + for (auto &i: e.trade_items) { + i *= sell_line.seller_quantity; + } + e.total_cost = (uint64) sell_line.item_cost * (uint64) in->seller_quantity; + e.buyer_name = sell_line.buyer_name; + e.seller_name = sell_line.seller_name; + RecordPlayerEventLogWithClient(seller, PlayerEvent::BARTER_TRANSACTION, 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 1cb26640c..0e08b665b 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -69,6 +69,7 @@ #include "../common/repositories/alternate_currency_repository.h" #include "../common/repositories/graveyard_repository.h" #include "../common/repositories/trader_repository.h" +#include "../common/repositories/buyer_repository.h" #include diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 56c8b62e4..3e26e19cd 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -416,24 +416,6 @@ void ZoneDatabase::UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 cha } } -void ZoneDatabase::DeleteBuyLines(uint32 CharID) { - - if(CharID==0) { - const std::string query = "DELETE FROM buyer"; - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to delete all buyer items data, the error was: [{}]\n",results.ErrorMessage().c_str()); - - return; - } - - std::string query = StringFormat("DELETE FROM buyer WHERE charid = %i", CharID); - auto results = QueryDatabase(query); - if (!results.Success()) - LogDebug("[CLIENT] Failed to delete buyer item data for charid: [{}], the error was: [{}]\n",CharID,results.ErrorMessage().c_str()); - -} - void ZoneDatabase::AddBuyLine(uint32 CharID, uint32 BuySlot, uint32 ItemID, const char* ItemName, uint32 Quantity, uint32 Price) { std::string query = StringFormat("REPLACE INTO buyer VALUES(%i, %i, %i, \"%s\", %i, %i)", CharID, BuySlot, ItemID, ItemName, Quantity, Price); diff --git a/zone/zonedb.h b/zone/zonedb.h index 52b6c8504..61cefecee 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -400,7 +400,6 @@ public: /* Buyer/Barter */ void AddBuyLine(uint32 CharID, uint32 BuySlot, uint32 ItemID, const char *ItemName, uint32 Quantity, uint32 Price); void RemoveBuyLine(uint32 CharID, uint32 BuySlot); - void DeleteBuyLines(uint32 CharID); void UpdateBuyLine(uint32 CharID, uint32 BuySlot, uint32 Quantity);