diff --git a/dGame/dComponents/Vendor/BaseVendorComponent.cpp b/dGame/dComponents/Vendor/BaseVendorComponent.cpp index b7b11cc97..448f5d430 100644 --- a/dGame/dComponents/Vendor/BaseVendorComponent.cpp +++ b/dGame/dComponents/Vendor/BaseVendorComponent.cpp @@ -15,6 +15,7 @@ #include "UserManager.h" #include "CheatDetection.h" #include "VendorMessages.h" +#include "InventoryMessages.h" #include "Item.h" #include "EconomyLedger.h" @@ -355,10 +356,36 @@ void BaseVendorComponent::MakeRoomInBuyback(InventoryComponent& inventory, const // added. Each item gets a new, higher object ID when it is sold, so the oldest is the one with the lowest ID. // The ledger counted these items as gone when they were sold EconomyLedger::ScopedItemTransfer alreadyCounted; + bool dropped = false; while (buyback->GetItems().size() >= BUYBACK_SIZE) { const auto items = buyback->GetItems().size(); const auto oldest = std::ranges::min_element(buyback->GetItems(), {}, [](const auto& entry) { return entry.first; }); oldest->second->SetCount(0, false, false, false, eLootSourceType::VENDOR); if (buyback->GetItems().size() >= items) break; // not removed (an empty stack) + dropped = true; + } + if (dropped) CompactBuyback(inventory); +} + +void BaseVendorComponent::CompactBuyback(InventoryComponent& inventory) { + auto* const buyback = inventory.GetInventory(eInventoryType::VENDOR_BUYBACK); + if (!buyback) return; + + // Live moved what was left up, in order, before the sold item went in after it: the 2014 capture of a 28th sale + // has MoveItemInInventory (inventory 11) for each of the 26 items left, to slots 0 to 25, then the sold item added + const auto& player = *inventory.GetParent(); + const auto slots = buyback->GetSlots(); + uint32_t slot = 0; + for (auto* const item : slots | std::views::values) { + if (item->GetSlot() != slot) { + item->SetSlot(slot); + GameMessages::MoveItemInInventory move; + move.target = player.GetObjectID(); + move.iObjID = item->GetId(); + move.inventoryType = eInventoryType::VENDOR_BUYBACK; + move.slot = static_cast(slot); + move.SendToClient(player.GetSystemAddress()); + } + slot++; } } diff --git a/dGame/dComponents/Vendor/BaseVendorComponent.h b/dGame/dComponents/Vendor/BaseVendorComponent.h index 232626028..371fef081 100644 --- a/dGame/dComponents/Vendor/BaseVendorComponent.h +++ b/dGame/dComponents/Vendor/BaseVendorComponent.h @@ -75,6 +75,9 @@ public: // a slot of their own. static void MakeRoomInBuyback(InventoryComponent& inventory, const Item& sold, uint32_t count); + // Moves the buyback items up to slots 0, 1, ... in their order, telling the player of each one that moved + static void CompactBuyback(InventoryComponent& inventory); + private: void HandleMrReeCameras(); bool SetupItem(LOT item); diff --git a/tests/dGameTests/dComponentsTests/VendorBuybackTests.cpp b/tests/dGameTests/dComponentsTests/VendorBuybackTests.cpp index 043ca2f48..1fdf2fac7 100644 --- a/tests/dGameTests/dComponentsTests/VendorBuybackTests.cpp +++ b/tests/dGameTests/dComponentsTests/VendorBuybackTests.cpp @@ -8,6 +8,7 @@ #include "Item.h" #include "VendorComponent.h" #include "eReplicaComponentType.h" +#include "InventoryMessages.h" #include @@ -77,3 +78,50 @@ TEST_F(VendorBuybackTests, RoomLeftOnAStackKeepsEverything) { VendorComponent::MakeRoomInBuyback(*inventory, *sold, 6); EXPECT_EQ(buyback->GetItems().size(), VendorComponent::BUYBACK_SIZE - 1); } + +TEST_F(VendorBuybackTests, DroppingTheOldestMovesTheRestUp) { + const auto oldest = nextId; + for (size_t i = 0; i < VendorComponent::BUYBACK_SIZE; i++) Give(buyback, 3001); + auto* const sold = Give(inventory->GetInventory(eInventoryType::ITEMS), 3001); + auto* const server = static_cast(Game::server); + server->ClearSentPackets(); + + VendorComponent::MakeRoomInBuyback(*inventory, *sold, 1); + + // The 26 left in slots 0 to 25 in their order, the sold item's slot free after them + for (LWOOBJID id = oldest + 1; id < oldest + static_cast(VendorComponent::BUYBACK_SIZE); id++) { + EXPECT_EQ(buyback->FindItemById(id)->GetSlot(), static_cast(id - oldest - 1)); + } + EXPECT_EQ(buyback->FindEmptySlot(), static_cast(VendorComponent::BUYBACK_SIZE - 1)); + + // A MoveItemInInventory for each, in the buyback inventory, as live sent them + std::vector> moves; + for (const auto& packet : server->GetSentPackets()) { + RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); + LWOOBJID target{}; + MessageType::Game id{}; + if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, id) || id != MessageType::Game::MOVE_ITEM_IN_INVENTORY) continue; + GameMessages::MoveItemInInventory move; + ASSERT_TRUE(move.Deserialize(bitStream)); + EXPECT_EQ(target, entity->GetObjectID()); + EXPECT_EQ(move.inventoryType, eInventoryType::VENDOR_BUYBACK); + EXPECT_EQ(move.destInvType, eInventoryType::INVALID); + EXPECT_EQ(move.responseCode, 0); + moves.emplace_back(move.iObjID, move.slot); + } + ASSERT_EQ(moves.size(), VendorComponent::BUYBACK_SIZE - 1); + for (size_t i = 0; i < moves.size(); i++) { + EXPECT_EQ(moves[i].first, oldest + 1 + static_cast(i)); + EXPECT_EQ(moves[i].second, static_cast(i)); + } +} + +TEST_F(VendorBuybackTests, NothingMovesWhenNothingIsDropped) { + Give(buyback, 3001); + auto* const sold = Give(inventory->GetInventory(eInventoryType::ITEMS), 3001); + auto* const server = static_cast(Game::server); + server->ClearSentPackets(); + + VendorComponent::MakeRoomInBuyback(*inventory, *sold, 1); + EXPECT_TRUE(server->GetSentPackets().empty()); +}