diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7ef14f76..f21c0bdf 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2029,6 +2029,7 @@ public: void openItemBySlot(int backpackIndex); void openItemInBag(int bagIndex, int slotIndex); void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1); + void splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count); void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot); void swapBagSlots(int srcBagIndex, int dstBagIndex); void useItemById(uint32_t itemId); diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c2aa581f..e5492eb4 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2046,6 +2046,13 @@ public: static network::Packet build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot); }; +/** CMSG_SPLIT_ITEM packet builder */ +class SplitItemPacket { +public: + static network::Packet build(uint8_t srcBag, uint8_t srcSlot, + uint8_t dstBag, uint8_t dstSlot, uint8_t count); +}; + /** CMSG_SWAP_INV_ITEM packet builder */ class SwapInvItemPacket { public: diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 21ccdc00..dca0e5a5 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -187,6 +187,14 @@ private: uint8_t destroyCount_ = 1; std::string destroyItemName_; + // Stack split popup state + bool splitConfirmOpen_ = false; + uint8_t splitBag_ = 0xFF; + uint8_t splitSlot_ = 0; + int splitMax_ = 1; + int splitCount_ = 1; + std::string splitItemName_; + // Pending chat item link from shift-click std::string pendingChatItemLink_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 821f337a..5ffb2028 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -21177,6 +21177,40 @@ void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { socket->send(packet); } +void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) { + if (state != WorldState::IN_WORLD || !socket) return; + if (count == 0) return; + + // Find a free slot for the split destination: try backpack first, then bags + int freeBp = inventory.findFreeBackpackSlot(); + if (freeBp >= 0) { + uint8_t dstBag = 0xFF; + uint8_t dstSlot = static_cast(23 + freeBp); + LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot, + ") count=", (int)count, " -> dst(bag=0xFF slot=", (int)dstSlot, ")"); + auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count); + socket->send(packet); + return; + } + // Try equipped bags + for (int b = 0; b < inventory.NUM_BAG_SLOTS; b++) { + int bagSize = inventory.getBagSize(b); + for (int s = 0; s < bagSize; s++) { + if (inventory.getBagSlot(b, s).empty()) { + uint8_t dstBag = static_cast(19 + b); + uint8_t dstSlot = static_cast(s); + LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot, + ") count=", (int)count, " -> dst(bag=", (int)dstBag, + " slot=", (int)dstSlot, ")"); + auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count); + socket->send(packet); + return; + } + } + } + addSystemChatMessage("Cannot split: no free inventory slots."); +} + void GameHandler::useItemBySlot(int backpackIndex) { if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return; const auto& slot = inventory.getBackpackSlot(backpackIndex); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index d9f40091..77ccf49c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -4358,6 +4358,17 @@ network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t s return packet; } +network::Packet SplitItemPacket::build(uint8_t srcBag, uint8_t srcSlot, + uint8_t dstBag, uint8_t dstSlot, uint8_t count) { + network::Packet packet(wireOpcode(Opcode::CMSG_SPLIT_ITEM)); + packet.writeUInt8(srcBag); + packet.writeUInt8(srcSlot); + packet.writeUInt8(dstBag); + packet.writeUInt8(dstSlot); + packet.writeUInt8(count); + return packet; +} + network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) { network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM)); packet.writeUInt8(srcSlot); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index b4e2ac89..366e9fa0 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -871,6 +871,35 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { ImGui::EndPopup(); } + // Stack split popup + if (splitConfirmOpen_) { + ImVec2 mousePos = ImGui::GetIO().MousePos; + ImGui::SetNextWindowPos(ImVec2(mousePos.x - 80.0f, mousePos.y - 20.0f), ImGuiCond_Always); + ImGui::OpenPopup("##SplitStack"); + splitConfirmOpen_ = false; + } + if (ImGui::BeginPopup("##SplitStack", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { + ImGui::Text("Split %s", splitItemName_.c_str()); + ImGui::Spacing(); + ImGui::SetNextItemWidth(120.0f); + ImGui::SliderInt("##splitcount", &splitCount_, 1, splitMax_ - 1); + ImGui::Spacing(); + if (ImGui::Button("OK", ImVec2(55, 0))) { + if (gameHandler_ && splitCount_ > 0 && splitCount_ < splitMax_) { + gameHandler_->splitItem(splitBag_, splitSlot_, static_cast(splitCount_)); + } + splitItemName_.clear(); + inventoryDirty = true; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(55, 0))) { + splitItemName_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + // Draw held item at cursor renderHeldItem(); } @@ -2302,22 +2331,39 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite } } - // Shift+right-click: open destroy confirmation for non-quest items + // Shift+right-click: split stack (if stackable >1) or destroy confirmation if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && - !holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0 && item.bindType != 4) { - destroyConfirmOpen_ = true; - destroyItemName_ = item.name; - destroyCount_ = static_cast(std::clamp( - std::max(1u, item.stackCount), 1u, 255u)); - if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { - destroyBag_ = 0xFF; - destroySlot_ = static_cast(23 + backpackIndex); - } else if (kind == SlotKind::BACKPACK && isBagSlot) { - destroyBag_ = static_cast(19 + bagIndex); - destroySlot_ = static_cast(bagSlotIndex); - } else if (kind == SlotKind::EQUIPMENT) { - destroyBag_ = 0xFF; - destroySlot_ = static_cast(equipSlot); + !holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0) { + if (item.stackCount > 1 && item.maxStack > 1) { + // Open split popup for stackable items + splitConfirmOpen_ = true; + splitItemName_ = item.name; + splitMax_ = static_cast(item.stackCount); + splitCount_ = splitMax_ / 2; + if (splitCount_ < 1) splitCount_ = 1; + if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + splitBag_ = 0xFF; + splitSlot_ = static_cast(23 + backpackIndex); + } else if (kind == SlotKind::BACKPACK && isBagSlot) { + splitBag_ = static_cast(19 + bagIndex); + splitSlot_ = static_cast(bagSlotIndex); + } + } else if (item.bindType != 4) { + // Destroy confirmation for non-quest, non-stackable items + destroyConfirmOpen_ = true; + destroyItemName_ = item.name; + destroyCount_ = static_cast(std::clamp( + std::max(1u, item.stackCount), 1u, 255u)); + if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + destroyBag_ = 0xFF; + destroySlot_ = static_cast(23 + backpackIndex); + } else if (kind == SlotKind::BACKPACK && isBagSlot) { + destroyBag_ = static_cast(19 + bagIndex); + destroySlot_ = static_cast(bagSlotIndex); + } else if (kind == SlotKind::EQUIPMENT) { + destroyBag_ = 0xFF; + destroySlot_ = static_cast(equipSlot); + } } }