From 7021602bf47c6a67beeaced88b0698a0e9dac941 Mon Sep 17 00:00:00 2001 From: Fryguy Date: Wed, 5 Feb 2025 21:56:57 -0500 Subject: [PATCH] [Bug] Item Purchase Offset when multiple buyers are buying at the same time. (#4628) * [Bug] Item Purchase Offset when multiple buyers are buying at the same time. - Much of the code lifted from TAKP/P2002 and adapted - Needs Testing - This should help prevent situations where multiple people are purchasing items from a merchant and both attempt to purchase the same temp merchant or limited item, it should result in the later person recieving a notice that the item no longer exists and refreshes the merchant table. - Updated strings * fix formatting * Push * Update client.cpp * Update database_update_manifest.cpp * Update database_update_manifest.cpp * Update database_update_manifest.cpp * Update client.cpp --------- Co-authored-by: Kinglykrab Co-authored-by: Akkadius --- common/database/database_update_manifest.cpp | 7 +- zone/client.cpp | 15 +++- zone/client.h | 2 + zone/client_packet.cpp | 93 ++++++++++---------- zone/entity.cpp | 50 +++++++++++ zone/entity.h | 3 + zone/mob.cpp | 2 + zone/mob.h | 5 ++ zone/string_ids.h | 2 + 9 files changed, 128 insertions(+), 51 deletions(-) diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 5d68e717a..05be7fdf4 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -6394,7 +6394,7 @@ ALTER TABLE `trader` ManifestEntry{ .version = 9297, .description = "2024_01_22_sharedbank_guid_primary_key.sql", - .check = "SHOW COLUMN FROM `sharedbank` LIKE 'guid'", + .check = "SHOW COLUMNS FROM `sharedbank` LIKE 'guid'", .condition = "empty", .match = "", .sql = R"( @@ -6423,7 +6423,7 @@ ADD PRIMARY KEY (`account_id`, `slot_id`); .version = 9298, .description = "2024_10_24_inventory_changes.sql", .check = "SHOW COLUMNS FROM `inventory` LIKE 'character_id'", - .condition = "missing", + .condition = "empty", .match = "", .sql = R"( ALTER TABLE `inventory` @@ -6480,7 +6480,8 @@ UPDATE `inventory` SET `slot_id` = ((`slot_id` - 2261) + 10810) WHERE `slot_id` UPDATE `sharedbank` SET `slot_id` = ((`slot_id` - 2531) + 11010) WHERE `slot_id` BETWEEN 2531 AND 2540; -- Shared Bank Bag 1 UPDATE `sharedbank` SET `slot_id` = ((`slot_id` - 2541) + 11210) WHERE `slot_id` BETWEEN 2541 AND 2550; -- Shared Bank Bag 2 )", - .content_schema_update = false + .content_schema_update = false, + .force_interactive = true }, ManifestEntry{ .version = 9299, diff --git a/zone/client.cpp b/zone/client.cpp index f912dea0b..5470d716b 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -13543,9 +13543,22 @@ std::string Client::GetBandolierItemName(uint8 bandolier_slot, uint8 slot_id) if ( !EQ::ValueWithin(bandolier_slot, 0, 3) || !EQ::ValueWithin(slot_id, 0, 3) - ) { + ) { return std::string(); } return GetPP().bandoliers[bandolier_slot].Items[slot_id].Name; } + +void Client::SendMerchantEnd() +{ + SetMerchantSessionEntityID(0); + + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { + DoParcelCancel(); + SetEngagedWithParcelMerchant(false); + } + + EQApplicationPacket empty(OP_ShopEndConfirm); + QueuePacket(&empty); +} diff --git a/zone/client.h b/zone/client.h index 9d576b5ef..848e66931 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1828,6 +1828,8 @@ public: uint32 trapid; //ID of trap player has triggered. This is cleared when the player leaves the trap's radius, or it despawns. + void SendMerchantEnd(); + Raid *p_raid_instance; inline uint32 GetPotionBeltItemIcon(uint8 slot_id) diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 476e116a1..ae38c7bd2 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -13952,14 +13952,7 @@ void Client::Handle_OP_Shielding(const EQApplicationPacket *app) void Client::Handle_OP_ShopEnd(const EQApplicationPacket *app) { - if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { - DoParcelCancel(); - SetEngagedWithParcelMerchant(false); - } - - EQApplicationPacket empty(OP_ShopEndConfirm); - QueuePacket(&empty); - return; + SendMerchantEnd(); } void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) @@ -13980,14 +13973,16 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) bool tmpmer_used = false; Mob* tmp = entity_list.GetMob(mp->npcid); - if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != Class::Merchant) - return; - - if (mp->quantity < 1) return; - - //you have to be somewhat close to them to be properly using them - if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) + if ( + tmp == 0 || + !tmp->IsNPC() || + tmp->GetClass() != Class::Merchant || + mp->quantity < 1 || + DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2 + ) { + SendMerchantEnd(); return; + } merchantid = tmp->CastToNPC()->MerchantType; @@ -14021,36 +14016,34 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) } } } + item = database.GetItem(item_id); + if (!item) { //error finding item, client didnt get the update packet for whatever reason, roleplay a tad - Message(Chat::Yellow, "%s tells you 'Sorry, that item is for display purposes only.' as they take the item off the shelf.", tmp->GetCleanName()); - auto delitempacket = new EQApplicationPacket(OP_ShopDelItem, sizeof(Merchant_DelItem_Struct)); - Merchant_DelItem_Struct* delitem = (Merchant_DelItem_Struct*)delitempacket->pBuffer; - delitem->itemslot = mp->itemslot; - delitem->npcid = mp->npcid; - delitem->playerid = mp->playerid; - delitempacket->priority = 6; - entity_list.QueueCloseClients(tmp, delitempacket); //que for anyone that could be using the merchant so they see the update - safe_delete(delitempacket); + MessageString(Chat::White, ALREADY_SOLD); + entity_list.SendMerchantInventory(tmp, mp->itemslot, true); + SendMerchantEnd(); return; } - if (CheckLoreConflict(item)) - { - Message(Chat::Yellow, "You can only have one of a lore item."); + + if (CheckLoreConflict(item)) { + MessageString(Chat::White, DUPE_LORE_MERCHANT,tmp->GetCleanName(),item->Name); return; } - if (tmpmer_used && (mp->quantity > prevcharges || item->MaxCharges > 1)) - { - if (prevcharges > item->MaxCharges && item->MaxCharges > 1) + + if (tmpmer_used && (mp->quantity > prevcharges || item->MaxCharges > 1)) { + if (prevcharges > item->MaxCharges && item->MaxCharges > 1) { mp->quantity = item->MaxCharges; - else + } else { mp->quantity = prevcharges; + } } // Item's stackable, but the quantity they want to buy exceeds the max stackable quantity. - if (item->Stackable && mp->quantity > item->StackSize) + if (item->Stackable && mp->quantity > item->StackSize) { mp->quantity = item->StackSize; + } auto outapp = new EQApplicationPacket(OP_ShopPlayerBuy, sizeof(Merchant_Sell_Struct)); Merchant_Sell_Struct* mpo = (Merchant_Sell_Struct*)outapp->pBuffer; @@ -14061,10 +14054,11 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) int16 freeslotid = INVALID_INDEX; int16 charges = 0; - if (item->Stackable || tmpmer_used) + if (item->Stackable || tmpmer_used) { charges = mp->quantity; - else if ( item->MaxCharges >= 1) + } else if ( item->MaxCharges >= 1) { charges = item->MaxCharges; + } EQ::ItemInstance* inst = database.CreateItem(item, charges); @@ -14079,23 +14073,24 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) single_price *= Client::CalcPriceMod(tmp, false); } - if (item->MaxCharges > 1) + if (item->MaxCharges > 1) { mpo->price = single_price; - else + } else { mpo->price = single_price * mp->quantity; + } - if (mpo->price < 0) - { + if (mpo->price < 0) { + MessageString(Chat::White, ALREADY_SOLD); safe_delete(outapp); safe_delete(inst); + SendMerchantEnd(); return; } // this area needs some work..two inventory insertion check failure points // below do not return player's money..is this the intended behavior? - if (!TakeMoneyFromPP(mpo->price)) - { + if (!TakeMoneyFromPP(mpo->price)) { auto message = fmt::format( "Vendor Cheat attempted to buy qty [{}] of item_id [{}] item_name[{}] that cost [{}] copper but only has platinum [{}] gold [{}] silver [{}] copper [{}]", mpo->quantity, @@ -14115,8 +14110,10 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) } bool stacked = TryStacking(inst); - if (!stacked) + + if (!stacked) { freeslotid = m_inv.FindFreeSlot(false, true, item->Size); + } // shouldn't we be reimbursing if these two fail? @@ -14130,8 +14127,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) } } - if (!stacked && freeslotid == INVALID_INDEX) - { + if (!stacked && freeslotid == INVALID_INDEX) { Message(Chat::Red, "You do not have room for any more items."); safe_delete(outapp); safe_delete(inst); @@ -14142,11 +14138,12 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) if (!stacked && inst) { PutItemInInventory(freeslotid, *inst); SendItemPacket(freeslotid, inst, ItemPacketTrade); - } - else if (!stacked) { + } else if (!stacked) { LogError("OP_ShopPlayerBuy: item->ItemClass Unknown! Type: [{}]", item->ItemClass); } + QueuePacket(outapp); + if (inst && tmpmer_used) { int32 new_charges = prevcharges - mp->quantity; zone->SaveTempItem(merchantid, tmp->GetNPCTypeID(), item_id, new_charges); @@ -14159,8 +14156,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) delitempacket->priority = 6; entity_list.QueueClients(tmp, delitempacket); //que for anyone that could be using the merchant so they see the update safe_delete(delitempacket); - } - else { + } else { // Update the charges/quantity in the merchant window inst->SetCharges(new_charges); inst->SetPrice(single_price); @@ -14219,6 +14215,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) safe_delete(inst); safe_delete(outapp); } + void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) { if (app->size != sizeof(Merchant_Purchase_Struct)) { @@ -14522,6 +14519,8 @@ void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app) if (action == MerchantActions::Open) { BulkSendMerchantInventory(merchant_id, tmp->GetNPCTypeID()); + SetMerchantSessionEntityID(tmp->GetID()); + if ((tabs_to_display & Parcel) == Parcel) { SendBulkParcels(); } diff --git a/zone/entity.cpp b/zone/entity.cpp index 198ddf710..e2c00a5bd 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -5950,3 +5950,53 @@ std::vector EntityList::GetExcludedNPCsByIDs(std::vector npc_ids) return v; } + +void EntityList::SendMerchantEnd(Mob* merchant) +{ + for (const auto& e : client_list) { + Client* c = e.second; + + if (!c) { + continue; + } + + if (c->GetMerchantSessionEntityID() == merchant->GetID()) { + c->SendMerchantEnd(); + } + } +} + +void EntityList::SendMerchantInventory(Mob* m, int32 slot_id, bool is_delete) +{ + if (!m || !m->IsNPC()) { + return; + } + + for (const auto& e : client_list) { + Client* c = e.second; + + if (!c) { + continue; + } + + if (c->GetMerchantSessionEntityID() == m->GetID()) { + if (!is_delete) { + c->BulkSendMerchantInventory(m->CastToNPC()->MerchantType, m->GetNPCTypeID()); + } else { + auto app = new EQApplicationPacket(OP_ShopDelItem, sizeof(Merchant_DelItem_Struct)); + auto d = (Merchant_DelItem_Struct*)app->pBuffer; + + d->itemslot = slot_id; + d->npcid = m->GetID(); + d->playerid = c->GetID(); + + app->priority = 6; + + c->QueuePacket(app); + safe_delete(app); + } + } + } + + return; +} diff --git a/zone/entity.h b/zone/entity.h index d2bf8eb63..86a8a0220 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -578,6 +578,9 @@ public: int MovePlayerCorpsesToGraveyard(bool force_move_from_instance = false); + void SendMerchantEnd(Mob* merchant); + void SendMerchantInventory(Mob* m, int32 slot_id = -1, bool is_delete = false); + protected: friend class Zone; void Depop(bool StartSpawnTimer = false); diff --git a/zone/mob.cpp b/zone/mob.cpp index ca554bcd2..fbcd53267 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -493,6 +493,8 @@ Mob::Mob( m_AllowBeneficial = false; m_DisableMelee = false; + SetMerchantSessionEntityID(0); + for (int i = 0; i < EQ::skills::HIGHEST_SKILL + 2; i++) { SkillDmgTaken_Mod[i] = 0; } diff --git a/zone/mob.h b/zone/mob.h index 8ee41d0c3..c03914202 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1387,6 +1387,9 @@ public: std::string GetRacePlural(); std::string GetClassPlural(); + inline void SetMerchantSessionEntityID(uint16 value) { m_merchant_session_entity_id = value; } + inline uint16 GetMerchantSessionEntityID() { return m_merchant_session_entity_id; } + //Command #Tune functions void TuneGetStats(Mob* defender, Mob *attacker); void TuneGetACByPctMitigation(Mob* defender, Mob *attacker, float pct_mitigation, int interval = 10, int max_loop = 1000, int atk_override = 0, int Msg = 0); @@ -1920,6 +1923,8 @@ protected: MobMovementManager *mMovementManager; + uint16 m_merchant_session_entity_id; + private: Mob* target; EQ::InventoryProfile m_inv; diff --git a/zone/string_ids.h b/zone/string_ids.h index 0da8fc03e..34efb5ed9 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -286,6 +286,7 @@ #define BEGINS_TO_SHINE 1238 //Your %1 begins to shine. #define CANT_FIND_PLAYER 1276 //I can't find a player named %1! #define SURNAME_REJECTED 1374 //Your new surname was rejected. Please try a different name. +#define ALREADY_SOLD 1376 //The item you were interested in has already been sold. #define GUILD_DISBANDED 1377 //Your guild has been disbanded! You are no longer a member of any guild. #define DUEL_DECLINE 1383 //%1 has declined your challenge to duel to the death. #define DUEL_ACCEPTED 1384 //%1 has already accepted a duel with someone else. @@ -308,6 +309,7 @@ #define PLAYER_CHARMED 1461 //You lose control of yourself! #define TRADER_BUSY 1468 //That Trader is currently with a customer. Please wait until their transaction is finished. #define SENSE_CORPSE_DIRECTION 1563 //You sense a corpse in this direction. +#define DUPE_LORE_MERCHANT 1573 //%1 tells you, 'You already have the lore item, %2, on your person, on your shroud, in the bank, in a real estate, or as an augment in another item. You cannot have more than one of a particular lore item at a time.' #define QUEUED_TELL 2458 //[queued] #define QUEUE_TELL_FULL 2459 //[zoing and queue is full] #define TRADER_BUSY_TWO 3192 //Sorry, that action cannot be performed while trading.