diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 38655fd78..19d89b9fe 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -1620,6 +1620,32 @@ struct MoveItem_Struct /*0012*/ }; +// New for RoF2 - Size: 12 +struct InventorySlot_Struct +{ +/*000*/ int16 Type; // Worn and Normal inventory = 0, Bank = 1, Shared Bank = 2, Delete Item = -1 +/*002*/ int16 Unknown02; +/*004*/ int16 Slot; +/*006*/ int16 SubIndex; +/*008*/ int16 AugIndex; // Guessing - Seen 0xffff +/*010*/ int16 Unknown01; // Normally 0 - Seen 13262 when deleting an item, but didn't match item ID +/*012*/ +}; + +struct MultiMoveItemSub_Struct +{ +/*0000*/ InventorySlot_Struct from_slot; +/*0012*/ InventorySlot_Struct to_slot; +/*0024*/ uint32 number_in_stack; +/*0028*/ uint8 unknown[8]; +}; + +struct MultiMoveItem_Struct +{ +/*0000*/ uint32 count; +/*0004*/ MultiMoveItemSub_Struct moves[0]; +}; + // both MoveItem_Struct/DeleteItem_Struct server structures will be changing to a structure-based slot format..this will // be used for handling SoF/SoD/etc... time stamps sent using the MoveItem_Struct format. (nothing will be done with this // info at the moment..but, it is forwarded on to the server for handling/future use) diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 77e735259..a02fdc17d 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -10909,7 +10909,121 @@ void Client::Handle_OP_MoveItem(const EQApplicationPacket *app) void Client::Handle_OP_MoveMultipleItems(const EQApplicationPacket *app) { - Kick("Unimplemented move multiple items"); // TODO: lets not desync though + // This packet is only sent from the client if we ctrl click items in inventory + if (m_ClientVersionBit & EQ::versions::maskRoF2AndLater) { + if (!CharacterID()) { + LinkDead(); + return; + } + + if (app->size < sizeof(MultiMoveItem_Struct)) { + LinkDead(); + return; // Not enough data to be a valid packet + } + + const MultiMoveItem_Struct* multi_move = reinterpret_cast(app->pBuffer); + if (app->size != sizeof(MultiMoveItem_Struct) + sizeof(MultiMoveItemSub_Struct) * multi_move->count) { + LinkDead(); + return; // Packet size does not match expected size + } + + const int16 from_parent = multi_move->moves[0].from_slot.Slot; + const int16 to_parent = multi_move->moves[0].to_slot.Slot; + + // CTRL + left click drops an item into a bag without opening it. + // This can be a bag, in which case it tries to fill the target bag with the contents of the bag on the cursor + // CTRL + right click swaps the contents of two bags if a bag is on your cursor and you ctrl-right click on another bag. + + // We need to check if this is a swap or just an addition (left click or right click) + // Check if any component of this transaction is coming from anywhere other than the cursor + bool left_click = true; + for (int i = 0; i < multi_move->count; i++) { + if (multi_move->moves[i].from_slot.Slot != EQ::invslot::slotCursor) { + left_click = false; + } + } + + // This is a left click which is purely additive. This should always be cursor object or cursor bag contents into general\bank\whatever bag + if (left_click) { + for (int i = 0; i < multi_move->count; i++) { + MoveItem_Struct* mi = new MoveItem_Struct(); + mi->from_slot = multi_move->moves[i].from_slot.SubIndex == -1 ? multi_move->moves[i].from_slot.Slot : m_inv.CalcSlotId(multi_move->moves[i].from_slot.Slot, multi_move->moves[i].from_slot.SubIndex); + mi->to_slot = m_inv.CalcSlotId(multi_move->moves[i].to_slot.Slot, multi_move->moves[i].to_slot.SubIndex); + + if (multi_move->moves[i].to_slot.Type == EQ::invtype::typeBank) { // Target is bank inventory + mi->to_slot = m_inv.CalcSlotId(multi_move->moves[i].to_slot.Slot + EQ::invslot::BANK_BEGIN, multi_move->moves[i].to_slot.SubIndex); + } else if (multi_move->moves[i].to_slot.Type == EQ::invtype::typeSharedBank) { // Target is shared bank inventory + mi->to_slot = m_inv.CalcSlotId(multi_move->moves[i].to_slot.Slot + EQ::invslot::SHARED_BANK_BEGIN, multi_move->moves[i].to_slot.SubIndex); + } + + // This sends '1' as the stack count for unstackable items, which our titanium-era SwapItem blows up + if (m_inv.GetItem(mi->from_slot)->IsStackable()) { + mi->number_in_stack = multi_move->moves[i].number_in_stack; + } else { + mi->number_in_stack = 0; + } + + if (!SwapItem(mi) && IsValidSlot(mi->from_slot) && IsValidSlot(mi->to_slot)) { + bool error = false; + SwapItemResync(mi); + InterrogateInventory(this, false, true, false, error, false); + if (error) { + InterrogateInventory(this, true, false, true, error); + } + } + } + // This is the swap. + // Client behavior is just to move stacks without combining them + // Items get rearranged to fill the 'top' of the bag first + } else { + struct MoveInfo { + EQ::ItemInstance* item; + uint16 to_slot; + }; + + std::vector items; + items.reserve(multi_move->count); + + for (int i = 0; i < multi_move->count; i++) { + // These are always bags, so we don't need to worry about raw items in slotCursor + uint16 from_slot = m_inv.CalcSlotId(multi_move->moves[i].from_slot.Slot, multi_move->moves[i].from_slot.SubIndex); + if (multi_move->moves[i].from_slot.Type == EQ::invtype::typeBank) { // Target is bank inventory + from_slot = m_inv.CalcSlotId(multi_move->moves[i].from_slot.Slot + EQ::invslot::BANK_BEGIN, multi_move->moves[i].from_slot.SubIndex); + } else if (multi_move->moves[i].from_slot.Type == EQ::invtype::typeSharedBank) { // Target is shared bank inventory + from_slot = m_inv.CalcSlotId(multi_move->moves[i].from_slot.Slot + EQ::invslot::SHARED_BANK_BEGIN, multi_move->moves[i].from_slot.SubIndex); + } + + uint16 to_slot = m_inv.CalcSlotId(multi_move->moves[i].to_slot.Slot, multi_move->moves[i].to_slot.SubIndex); + if (multi_move->moves[i].to_slot.Type == EQ::invtype::typeBank) { // Target is bank inventory + to_slot = m_inv.CalcSlotId(multi_move->moves[i].to_slot.Slot + EQ::invslot::BANK_BEGIN, multi_move->moves[i].to_slot.SubIndex); + } else if (multi_move->moves[i].to_slot.Type == EQ::invtype::typeSharedBank) { // Target is shared bank inventory + to_slot = m_inv.CalcSlotId(multi_move->moves[i].to_slot.Slot + EQ::invslot::SHARED_BANK_BEGIN, multi_move->moves[i].to_slot.SubIndex); + } + + // I wasn't able to produce any error states on purpose. + MoveInfo move{ + .item = m_inv.PopItem(from_slot), // Don't delete the instance here + .to_slot = to_slot + }; + + if (move.item) { + items.push_back(move); + database.SaveInventory(CharacterID(), NULL, from_slot); // We have to manually save inventory here. + } else { + LinkDead(); + return; // Prevent inventory desync here. Forcing a resync would be better, but we don't have a MoveItem struct to work with. + } + } + + for (const MoveInfo& move : items) { + PutItemInInventory(move.to_slot, *move.item); // This saves inventory too + } + } + + } else { + LinkDead(); // This packet should not be sent by an older client + return; + } } void Client::Handle_OP_OpenContainer(const EQApplicationPacket *app)