[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 <kinglykrab@gmail.com>
Co-authored-by: Akkadius <akkadius1@gmail.com>
This commit is contained in:
Fryguy 2025-02-05 21:56:57 -05:00 committed by GitHub
parent 752ac78c56
commit 7021602bf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 128 additions and 51 deletions

View File

@ -6394,7 +6394,7 @@ ALTER TABLE `trader`
ManifestEntry{ ManifestEntry{
.version = 9297, .version = 9297,
.description = "2024_01_22_sharedbank_guid_primary_key.sql", .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", .condition = "empty",
.match = "", .match = "",
.sql = R"( .sql = R"(
@ -6423,7 +6423,7 @@ ADD PRIMARY KEY (`account_id`, `slot_id`);
.version = 9298, .version = 9298,
.description = "2024_10_24_inventory_changes.sql", .description = "2024_10_24_inventory_changes.sql",
.check = "SHOW COLUMNS FROM `inventory` LIKE 'character_id'", .check = "SHOW COLUMNS FROM `inventory` LIKE 'character_id'",
.condition = "missing", .condition = "empty",
.match = "", .match = "",
.sql = R"( .sql = R"(
ALTER TABLE `inventory` 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` - 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 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{ ManifestEntry{
.version = 9299, .version = 9299,

View File

@ -13543,9 +13543,22 @@ std::string Client::GetBandolierItemName(uint8 bandolier_slot, uint8 slot_id)
if ( if (
!EQ::ValueWithin(bandolier_slot, 0, 3) || !EQ::ValueWithin(bandolier_slot, 0, 3) ||
!EQ::ValueWithin(slot_id, 0, 3) !EQ::ValueWithin(slot_id, 0, 3)
) { ) {
return std::string(); return std::string();
} }
return GetPP().bandoliers[bandolier_slot].Items[slot_id].Name; 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);
}

View File

@ -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. 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; Raid *p_raid_instance;
inline uint32 GetPotionBeltItemIcon(uint8 slot_id) inline uint32 GetPotionBeltItemIcon(uint8 slot_id)

View File

@ -13952,14 +13952,7 @@ void Client::Handle_OP_Shielding(const EQApplicationPacket *app)
void Client::Handle_OP_ShopEnd(const EQApplicationPacket *app) void Client::Handle_OP_ShopEnd(const EQApplicationPacket *app)
{ {
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { SendMerchantEnd();
DoParcelCancel();
SetEngagedWithParcelMerchant(false);
}
EQApplicationPacket empty(OP_ShopEndConfirm);
QueuePacket(&empty);
return;
} }
void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
@ -13980,14 +13973,16 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
bool tmpmer_used = false; bool tmpmer_used = false;
Mob* tmp = entity_list.GetMob(mp->npcid); Mob* tmp = entity_list.GetMob(mp->npcid);
if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != Class::Merchant) if (
return; tmp == 0 ||
!tmp->IsNPC() ||
if (mp->quantity < 1) return; tmp->GetClass() != Class::Merchant ||
mp->quantity < 1 ||
//you have to be somewhat close to them to be properly using them DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2
if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) ) {
SendMerchantEnd();
return; return;
}
merchantid = tmp->CastToNPC()->MerchantType; merchantid = tmp->CastToNPC()->MerchantType;
@ -14021,36 +14016,34 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
} }
} }
} }
item = database.GetItem(item_id); item = database.GetItem(item_id);
if (!item) { if (!item) {
//error finding item, client didnt get the update packet for whatever reason, roleplay a tad //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()); MessageString(Chat::White, ALREADY_SOLD);
auto delitempacket = new EQApplicationPacket(OP_ShopDelItem, sizeof(Merchant_DelItem_Struct)); entity_list.SendMerchantInventory(tmp, mp->itemslot, true);
Merchant_DelItem_Struct* delitem = (Merchant_DelItem_Struct*)delitempacket->pBuffer; SendMerchantEnd();
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);
return; return;
} }
if (CheckLoreConflict(item))
{ if (CheckLoreConflict(item)) {
Message(Chat::Yellow, "You can only have one of a lore item."); MessageString(Chat::White, DUPE_LORE_MERCHANT,tmp->GetCleanName(),item->Name);
return; return;
} }
if (tmpmer_used && (mp->quantity > prevcharges || item->MaxCharges > 1))
{ if (tmpmer_used && (mp->quantity > prevcharges || item->MaxCharges > 1)) {
if (prevcharges > item->MaxCharges && item->MaxCharges > 1) if (prevcharges > item->MaxCharges && item->MaxCharges > 1) {
mp->quantity = item->MaxCharges; mp->quantity = item->MaxCharges;
else } else {
mp->quantity = prevcharges; mp->quantity = prevcharges;
}
} }
// Item's stackable, but the quantity they want to buy exceeds the max stackable quantity. // 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; mp->quantity = item->StackSize;
}
auto outapp = new EQApplicationPacket(OP_ShopPlayerBuy, sizeof(Merchant_Sell_Struct)); auto outapp = new EQApplicationPacket(OP_ShopPlayerBuy, sizeof(Merchant_Sell_Struct));
Merchant_Sell_Struct* mpo = (Merchant_Sell_Struct*)outapp->pBuffer; 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 freeslotid = INVALID_INDEX;
int16 charges = 0; int16 charges = 0;
if (item->Stackable || tmpmer_used) if (item->Stackable || tmpmer_used) {
charges = mp->quantity; charges = mp->quantity;
else if ( item->MaxCharges >= 1) } else if ( item->MaxCharges >= 1) {
charges = item->MaxCharges; charges = item->MaxCharges;
}
EQ::ItemInstance* inst = database.CreateItem(item, charges); 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); single_price *= Client::CalcPriceMod(tmp, false);
} }
if (item->MaxCharges > 1) if (item->MaxCharges > 1) {
mpo->price = single_price; mpo->price = single_price;
else } else {
mpo->price = single_price * mp->quantity; mpo->price = single_price * mp->quantity;
}
if (mpo->price < 0) if (mpo->price < 0) {
{ MessageString(Chat::White, ALREADY_SOLD);
safe_delete(outapp); safe_delete(outapp);
safe_delete(inst); safe_delete(inst);
SendMerchantEnd();
return; return;
} }
// this area needs some work..two inventory insertion check failure points // this area needs some work..two inventory insertion check failure points
// below do not return player's money..is this the intended behavior? // below do not return player's money..is this the intended behavior?
if (!TakeMoneyFromPP(mpo->price)) if (!TakeMoneyFromPP(mpo->price)) {
{
auto message = fmt::format( 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 [{}]", "Vendor Cheat attempted to buy qty [{}] of item_id [{}] item_name[{}] that cost [{}] copper but only has platinum [{}] gold [{}] silver [{}] copper [{}]",
mpo->quantity, mpo->quantity,
@ -14115,8 +14110,10 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
} }
bool stacked = TryStacking(inst); bool stacked = TryStacking(inst);
if (!stacked)
if (!stacked) {
freeslotid = m_inv.FindFreeSlot(false, true, item->Size); freeslotid = m_inv.FindFreeSlot(false, true, item->Size);
}
// shouldn't we be reimbursing if these two fail? // 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."); Message(Chat::Red, "You do not have room for any more items.");
safe_delete(outapp); safe_delete(outapp);
safe_delete(inst); safe_delete(inst);
@ -14142,11 +14138,12 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
if (!stacked && inst) { if (!stacked && inst) {
PutItemInInventory(freeslotid, *inst); PutItemInInventory(freeslotid, *inst);
SendItemPacket(freeslotid, inst, ItemPacketTrade); SendItemPacket(freeslotid, inst, ItemPacketTrade);
} } else if (!stacked) {
else if (!stacked) {
LogError("OP_ShopPlayerBuy: item->ItemClass Unknown! Type: [{}]", item->ItemClass); LogError("OP_ShopPlayerBuy: item->ItemClass Unknown! Type: [{}]", item->ItemClass);
} }
QueuePacket(outapp); QueuePacket(outapp);
if (inst && tmpmer_used) { if (inst && tmpmer_used) {
int32 new_charges = prevcharges - mp->quantity; int32 new_charges = prevcharges - mp->quantity;
zone->SaveTempItem(merchantid, tmp->GetNPCTypeID(), item_id, new_charges); zone->SaveTempItem(merchantid, tmp->GetNPCTypeID(), item_id, new_charges);
@ -14159,8 +14156,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
delitempacket->priority = 6; delitempacket->priority = 6;
entity_list.QueueClients(tmp, delitempacket); //que for anyone that could be using the merchant so they see the update entity_list.QueueClients(tmp, delitempacket); //que for anyone that could be using the merchant so they see the update
safe_delete(delitempacket); safe_delete(delitempacket);
} } else {
else {
// Update the charges/quantity in the merchant window // Update the charges/quantity in the merchant window
inst->SetCharges(new_charges); inst->SetCharges(new_charges);
inst->SetPrice(single_price); inst->SetPrice(single_price);
@ -14219,6 +14215,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
safe_delete(inst); safe_delete(inst);
safe_delete(outapp); safe_delete(outapp);
} }
void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app)
{ {
if (app->size != sizeof(Merchant_Purchase_Struct)) { if (app->size != sizeof(Merchant_Purchase_Struct)) {
@ -14522,6 +14519,8 @@ void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app)
if (action == MerchantActions::Open) { if (action == MerchantActions::Open) {
BulkSendMerchantInventory(merchant_id, tmp->GetNPCTypeID()); BulkSendMerchantInventory(merchant_id, tmp->GetNPCTypeID());
SetMerchantSessionEntityID(tmp->GetID());
if ((tabs_to_display & Parcel) == Parcel) { if ((tabs_to_display & Parcel) == Parcel) {
SendBulkParcels(); SendBulkParcels();
} }

View File

@ -5950,3 +5950,53 @@ std::vector<NPC*> EntityList::GetExcludedNPCsByIDs(std::vector<uint32> npc_ids)
return v; 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;
}

View File

@ -578,6 +578,9 @@ public:
int MovePlayerCorpsesToGraveyard(bool force_move_from_instance = false); 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: protected:
friend class Zone; friend class Zone;
void Depop(bool StartSpawnTimer = false); void Depop(bool StartSpawnTimer = false);

View File

@ -493,6 +493,8 @@ Mob::Mob(
m_AllowBeneficial = false; m_AllowBeneficial = false;
m_DisableMelee = false; m_DisableMelee = false;
SetMerchantSessionEntityID(0);
for (int i = 0; i < EQ::skills::HIGHEST_SKILL + 2; i++) { for (int i = 0; i < EQ::skills::HIGHEST_SKILL + 2; i++) {
SkillDmgTaken_Mod[i] = 0; SkillDmgTaken_Mod[i] = 0;
} }

View File

@ -1387,6 +1387,9 @@ public:
std::string GetRacePlural(); std::string GetRacePlural();
std::string GetClassPlural(); 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 //Command #Tune functions
void TuneGetStats(Mob* defender, Mob *attacker); 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); 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; MobMovementManager *mMovementManager;
uint16 m_merchant_session_entity_id;
private: private:
Mob* target; Mob* target;
EQ::InventoryProfile m_inv; EQ::InventoryProfile m_inv;

View File

@ -286,6 +286,7 @@
#define BEGINS_TO_SHINE 1238 //Your %1 begins to shine. #define BEGINS_TO_SHINE 1238 //Your %1 begins to shine.
#define CANT_FIND_PLAYER 1276 //I can't find a player named %1! #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 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 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_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. #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 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 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 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 QUEUED_TELL 2458 //[queued]
#define QUEUE_TELL_FULL 2459 //[zoing and queue is full] #define QUEUE_TELL_FULL 2459 //[zoing and queue is full]
#define TRADER_BUSY_TWO 3192 //Sorry, that action cannot be performed while trading. #define TRADER_BUSY_TWO 3192 //Sorry, that action cannot be performed while trading.