From e49ab924cc588fbd3cbc40b42331a6f1f579ec68 Mon Sep 17 00:00:00 2001 From: Mitch Freeman <65987027+neckkola@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:23:37 -0300 Subject: [PATCH] [Feature] Add Barter/Buyer Features (#4405) * Add Barter/Buyer Features Adds barter and buyer features, for ROF2 only at this time including item compensation * Remove FKs from buyer tables Remove FKs from buyer tables * Bug fix for Find Buyer and mutli item selling Update for quantity purchases not correctly providing multi items. Update for Find Buyer functionality based on zone instancing. Update buyer messaging Update buyer LORE duplicate check * Revert zone instance comment * Revert zone_id packet size field * Add zone instancing to barter/buyer --------- Co-authored-by: Akkadius --- common/CMakeLists.txt | 4 +- common/database.cpp | 6 + common/database.h | 1 + common/database/database_update_manifest.cpp | 57 + common/database_schema.h | 6 +- common/emu_oplist.h | 1 + common/eq_constants.h | 3 + common/eq_packet_structs.h | 425 +++- common/events/player_event_logs.cpp | 1 + common/events/player_events.h | 32 +- common/inventory_profile.cpp | 65 + common/inventory_profile.h | 2 + common/patches/rof2.cpp | 553 ++++- common/patches/rof2_ops.h | 3 + common/patches/rof2_structs.h | 164 +- .../base/base_buyer_buy_lines_repository.h | 475 +++++ .../repositories/base/base_buyer_repository.h | 175 +- .../base/base_buyer_trade_items_repository.h | 439 ++++ .../repositories/buyer_buy_lines_repository.h | 356 ++++ common/repositories/buyer_repository.h | 89 + .../buyer_trade_items_repository.h | 81 + common/ruletypes.h | 1 + common/servertalk.h | 1 + common/version.h | 2 +- utils/patches/patch_RoF2.conf | 1 + world/world_boot.cpp | 2 + world/zoneserver.cpp | 32 +- zone/api_service.cpp | 1 - zone/client.cpp | 122 +- zone/client.h | 42 +- zone/client_packet.cpp | 182 +- zone/inventory.cpp | 64 + zone/position.cpp | 53 + zone/position.h | 4 + zone/string_ids.h | 2 + zone/trading.cpp | 1890 +++++++++++------ zone/worldserver.cpp | 304 +++ zone/zone.cpp | 1 + zone/zonedb.cpp | 18 - zone/zonedb.h | 1 - 40 files changed, 4715 insertions(+), 946 deletions(-) create mode 100644 common/repositories/base/base_buyer_buy_lines_repository.h create mode 100644 common/repositories/base/base_buyer_trade_items_repository.h create mode 100644 common/repositories/buyer_buy_lines_repository.h create mode 100644 common/repositories/buyer_trade_items_repository.h 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);