Inventory: Fix order of callbacks when swapping items

This commit is contained in:
SmallJoker 2022-07-26 20:40:27 +02:00 committed by SmallJoker
parent d5d6e36ae0
commit 4245a7604b
6 changed files with 119 additions and 95 deletions

@ -3640,6 +3640,19 @@ Player Inventory lists
* Is not created automatically, use `InvRef:set_size` * Is not created automatically, use `InvRef:set_size`
* Is only used to enhance the empty hand's tool capabilities * Is only used to enhance the empty hand's tool capabilities
ItemStack transaction order
---------------------------
This list describes the situation for non-empty ItemStacks in both slots
that cannot be stacked at all, hence triggering an ItemStack swap operation.
Put/take callbacks on empty ItemStack are not executed.
1. The "allow take" and "allow put" callbacks are each run once for the source
and destination inventory.
2. The allowed ItemStacks are exchanged.
3. The "on take" callbacks are run for the source and destination inventories
4. The "on put" callbacks are run for the source and destination inventories
Colors Colors
====== ======

@ -29,12 +29,16 @@ minetest.register_node("chest:chest", {
return inv:is_empty("main") return inv:is_empty("main")
end, end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player) allow_metadata_inventory_put = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow put' event for " .. stack:to_string()) print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow put' (10) event for " .. stack:to_string())
return stack:get_count() return 10
end, end,
allow_metadata_inventory_take = function(pos, listname, index, stack, player) allow_metadata_inventory_take = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow take' event for " .. stack:to_string()) print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow take' (20) event for " .. stack:to_string())
return stack:get_count() return 20
end,
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow move' (30) event")
return 30
end, end,
on_metadata_inventory_put = function(pos, listname, index, stack, player) on_metadata_inventory_put = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " put " .. stack:to_string()) print_to_everything("Chest: ".. player:get_player_name() .. " put " .. stack:to_string())
@ -42,4 +46,7 @@ minetest.register_node("chest:chest", {
on_metadata_inventory_take = function(pos, listname, index, stack, player) on_metadata_inventory_take = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " took " .. stack:to_string()) print_to_everything("Chest: ".. player:get_player_name() .. " took " .. stack:to_string())
end, end,
on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
print_to_everything("Chest: ".. player:get_player_name() .. " moved " .. count)
end,
}) })

@ -758,54 +758,53 @@ void InventoryList::moveItemSomewhere(u32 i, InventoryList *dest, u32 count)
if (!leftover.empty()) { if (!leftover.empty()) {
// Add the remaining part back to the source item // Add the remaining part back to the source item
addItem(i, leftover); ItemStack &source = getItem(i);
source.add(leftover.count); // do NOT use addItem to allow oversized stacks!
} }
} }
u32 InventoryList::moveItem(u32 i, InventoryList *dest, u32 dest_i, ItemStack InventoryList::moveItem(u32 i, InventoryList *dest, u32 dest_i,
u32 count, bool swap_if_needed, bool *did_swap) u32 count, bool swap_if_needed, bool *did_swap)
{ {
ItemStack moved;
if (this == dest && i == dest_i) if (this == dest && i == dest_i)
return count; return moved;
// Take item from source list // Take item from source list
ItemStack item1;
if (count == 0) if (count == 0)
item1 = changeItem(i, ItemStack()); moved = changeItem(i, ItemStack());
else else
item1 = takeItem(i, count); moved = takeItem(i, count);
if (item1.empty())
return 0;
// Try to add the item to destination list // Try to add the item to destination list
count = item1.count; ItemStack leftover = dest->addItem(dest_i, moved);
item1 = dest->addItem(dest_i, item1);
// If something is returned, the item was not fully added // If something is returned, the item was not fully added
if (!item1.empty()) { if (!leftover.empty()) {
bool nothing_added = (item1.count == count); // Keep track of how many we actually moved
moved.remove(leftover.count);
// Add any leftover stack back to the source stack. // Add any leftover stack back to the source stack.
item1.add(getItem(i).count); // leftover + source count leftover.add(getItem(i).count); // leftover + source count
changeItem(i, item1); // do NOT use addItem to allow oversized stacks! changeItem(i, leftover); // do NOT use addItem to allow oversized stacks!
leftover.clear();
// Swap if no items could be moved // Swap if no items could be moved
if (nothing_added && swap_if_needed) { if (moved.empty() && swap_if_needed) {
// Tell that we swapped // Tell that we swapped
if (did_swap != NULL) { if (did_swap != NULL) {
*did_swap = true; *did_swap = true;
} }
// Take item from source list // Take item from source list
item1 = changeItem(i, ItemStack()); moved = changeItem(i, ItemStack());
// Adding was not possible, swap the items. // Adding was not possible, swap the items.
ItemStack item2 = dest->changeItem(dest_i, item1); ItemStack item2 = dest->changeItem(dest_i, moved);
// Put item from destination list to the source list // Put item from destination list to the source list
changeItem(i, item2); changeItem(i, item2);
item1.clear(); // no leftover
} }
} }
return (count - item1.count);
return moved;
} }
void InventoryList::checkResizeLock() void InventoryList::checkResizeLock()

@ -286,8 +286,8 @@ public:
// Move an item to a different list (or a different stack in the same list) // Move an item to a different list (or a different stack in the same list)
// count is the maximum number of items to move (0 for everything) // count is the maximum number of items to move (0 for everything)
// returns number of moved items // returns the moved stack
u32 moveItem(u32 i, InventoryList *dest, u32 dest_i, ItemStack moveItem(u32 i, InventoryList *dest, u32 dest_i,
u32 count = 0, bool swap_if_needed = true, bool *did_swap = NULL); u32 count = 0, bool swap_if_needed = true, bool *did_swap = NULL);
// like moveItem, but without a fixed destination index // like moveItem, but without a fixed destination index

@ -162,7 +162,20 @@ void IMoveAction::swapDirections()
std::swap(from_i, to_i); std::swap(from_i, to_i);
} }
void IMoveAction::onPutAndOnTake(const ItemStack &src_item, ServerActiveObject *player) const void IMoveAction::onTake(const ItemStack &src_item, ServerActiveObject *player) const
{
ServerScripting *sa = PLAYER_TO_SA(player);
if (from_inv.type == InventoryLocation::DETACHED)
sa->detached_inventory_OnTake(*this, src_item, player);
else if (from_inv.type == InventoryLocation::NODEMETA)
sa->nodemeta_inventory_OnTake(*this, src_item, player);
else if (from_inv.type == InventoryLocation::PLAYER)
sa->player_inventory_OnTake(*this, src_item, player);
else
assert(false);
}
void IMoveAction::onPut(const ItemStack &src_item, ServerActiveObject *player) const
{ {
ServerScripting *sa = PLAYER_TO_SA(player); ServerScripting *sa = PLAYER_TO_SA(player);
if (to_inv.type == InventoryLocation::DETACHED) if (to_inv.type == InventoryLocation::DETACHED)
@ -173,15 +186,6 @@ void IMoveAction::onPutAndOnTake(const ItemStack &src_item, ServerActiveObject *
sa->player_inventory_OnPut(*this, src_item, player); sa->player_inventory_OnPut(*this, src_item, player);
else else
assert(false); assert(false);
if (from_inv.type == InventoryLocation::DETACHED)
sa->detached_inventory_OnTake(*this, src_item, player);
else if (from_inv.type == InventoryLocation::NODEMETA)
sa->nodemeta_inventory_OnTake(*this, src_item, player);
else if (from_inv.type == InventoryLocation::PLAYER)
sa->player_inventory_OnTake(*this, src_item, player);
else
assert(false);
} }
void IMoveAction::onMove(int count, ServerActiveObject *player) const void IMoveAction::onMove(int count, ServerActiveObject *player) const
@ -244,6 +248,8 @@ int IMoveAction::allowMove(int try_take_count, ServerActiveObject *player) const
void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGameDef *gamedef) void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGameDef *gamedef)
{ {
/// Necessary for executing Lua callbacks which may manipulate the inventory,
/// hence invalidate pointers needed by IMoveAction::apply
auto get_borrow_checked_invlist = [mgr](const InventoryLocation &invloc, auto get_borrow_checked_invlist = [mgr](const InventoryLocation &invloc,
const std::string &listname) -> InventoryList::ResizeLocked const std::string &listname) -> InventoryList::ResizeLocked
{ {
@ -375,6 +381,8 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
bool allow_swap = !list_to->itemFits(to_i, src_item, &restitem) bool allow_swap = !list_to->itemFits(to_i, src_item, &restitem)
&& restitem.count == src_item.count && restitem.count == src_item.count
&& !caused_by_move_somewhere; && !caused_by_move_somewhere;
// move_count : How many items that were moved at the end
// count : Total items "in the queue" of being moved. Do not touch.
move_count = src_item.count - restitem.count; move_count = src_item.count - restitem.count;
// Shift-click: Cannot fill this stack, proceed with next slot // Shift-click: Cannot fill this stack, proceed with next slot
@ -383,10 +391,11 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
} }
if (allow_swap) { if (allow_swap) {
// Swap will affect the entire stack if it can performed. // Swap will affect the entire stack (= count) if it can performed.
src_item = list_from->getItem(from_i); src_item = list_from->getItem(from_i);
count = src_item.count; move_count = src_item.count;
} }
src_item.count = move_count; // Temporary movement stack
if (from_inv == to_inv) { if (from_inv == to_inv) {
// Move action within the same inventory // Move action within the same inventory
@ -409,16 +418,9 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
src_can_take_count = dst_can_put_count = 0; src_can_take_count = dst_can_put_count = 0;
} else { } else {
// Take from one inventory, put into another // Take from one inventory, put into another
int src_item_count = src_item.count;
if (caused_by_move_somewhere)
// When moving somewhere: temporarily use the actual movable stack
// size to ensure correct callback execution.
src_item.count = move_count;
dst_can_put_count = allowPut(src_item, player); dst_can_put_count = allowPut(src_item, player);
src_can_take_count = allowTake(src_item, player); src_can_take_count = allowTake(src_item, player);
if (caused_by_move_somewhere)
// Reset source item count
src_item.count = src_item_count;
bool swap_expected = allow_swap; bool swap_expected = allow_swap;
allow_swap = allow_swap allow_swap = allow_swap
&& (src_can_take_count == -1 || src_can_take_count >= src_item.count) && (src_can_take_count == -1 || src_can_take_count >= src_item.count)
@ -440,25 +442,20 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
src_can_take_count = dst_can_put_count = 0; src_can_take_count = dst_can_put_count = 0;
} }
int old_count = count; int old_move_count = move_count;
/* Modify count according to collected data */ // Apply limits given by allow_* callbacks
count = src_item.count; if (src_can_take_count != -1)
if (src_can_take_count != -1 && count > src_can_take_count) move_count = (u32)std::min<s32>(src_can_take_count, move_count);
count = src_can_take_count; if (dst_can_put_count != -1)
if (dst_can_put_count != -1 && count > dst_can_put_count) move_count = (u32)std::min<s32>(dst_can_put_count, move_count);
count = dst_can_put_count;
/* Limit according to source item count */ // allow_* callbacks should not modify the stack - but if they do - handle that.
if (count > list_from->getItem(from_i).count) if (move_count > list_from->getItem(from_i).count)
count = list_from->getItem(from_i).count; move_count = list_from->getItem(from_i).count;
/* If no items will be moved, don't go further */ /* If no items will be moved, don't go further */
if (count == 0) { if (move_count == 0) {
if (caused_by_move_somewhere)
// Set move count to zero, as no items have been moved
move_count = 0;
// Undo client prediction. See 'clientApply' // Undo client prediction. See 'clientApply'
if (from_inv.type == InventoryLocation::PLAYER) if (from_inv.type == InventoryLocation::PLAYER)
list_from->setModified(); list_from->setModified();
@ -467,7 +464,7 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
list_to->setModified(); list_to->setModified();
infostream<<"IMoveAction::apply(): move was completely disallowed:" infostream<<"IMoveAction::apply(): move was completely disallowed:"
<<" count="<<old_count <<" move_count="<<old_move_count
<<" from inv=\""<<from_inv.dump()<<"\"" <<" from inv=\""<<from_inv.dump()<<"\""
<<" list=\""<<from_list<<"\"" <<" list=\""<<from_list<<"\""
<<" i="<<from_i <<" i="<<from_i
@ -479,8 +476,7 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
return; return;
} }
src_item = list_from->getItem(from_i); // Backups stacks for infinite sources
src_item.count = count;
ItemStack from_stack_was = list_from->getItem(from_i); ItemStack from_stack_was = list_from->getItem(from_i);
ItemStack to_stack_was = list_to->getItem(to_i); ItemStack to_stack_was = list_to->getItem(to_i);
@ -491,13 +487,13 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
same as source), nothing happens same as source), nothing happens
*/ */
bool did_swap = false; bool did_swap = false;
move_count = list_from->moveItem(from_i, src_item = list_from->moveItem(from_i,
list_to.get(), to_i, count, allow_swap, &did_swap); list_to.get(), to_i, move_count, allow_swap, &did_swap);
if (caused_by_move_somewhere) move_count = src_item.count;
count = old_count;
assert(allow_swap == did_swap); assert(allow_swap == did_swap);
// If source is infinite, reset it's stack // If source is infinite, reset its stack
if (src_can_take_count == -1) { if (src_can_take_count == -1) {
// For the caused_by_move_somewhere == true case we didn't force-put the item, // For the caused_by_move_somewhere == true case we didn't force-put the item,
// which guarantees there is no leftover, and code below would duplicate the // which guarantees there is no leftover, and code below would duplicate the
@ -517,23 +513,23 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
} }
} }
if (move_count > 0 || did_swap) { if (move_count > 0 || did_swap) {
list_from->deleteItem(from_i); list_from->changeItem(from_i, from_stack_was);
list_from->addItem(from_i, from_stack_was);
} }
} }
// If destination is infinite, reset it's stack and take count from source // If destination is infinite, reset its stack and take count from source
if (dst_can_put_count == -1) { if (dst_can_put_count == -1) {
list_to->deleteItem(to_i); list_to->changeItem(to_i, to_stack_was);
list_to->addItem(to_i, to_stack_was); if (did_swap) {
list_from->deleteItem(from_i); // Undo swap result: set the expected stack + size
list_from->addItem(from_i, from_stack_was); list_from->changeItem(from_i, from_stack_was);
list_from->takeItem(from_i, count); list_from->takeItem(from_i, move_count);
}
} }
infostream << "IMoveAction::apply(): moved" infostream << "IMoveAction::apply(): moved"
<< " msom=" << move_somewhere << " msom=" << move_somewhere
<< " caused=" << caused_by_move_somewhere << " caused=" << caused_by_move_somewhere
<< " count=" << count << " move_count=" << move_count
<< " from inv=\"" << from_inv.dump() << "\"" << " from inv=\"" << from_inv.dump() << "\""
<< " list=\"" << from_list << "\"" << " list=\"" << from_list << "\""
<< " i=" << from_i << " i=" << from_i
@ -589,9 +585,9 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
// Source = destination => move // Source = destination => move
if (from_inv == to_inv) { if (from_inv == to_inv) {
onMove(count, player); onMove(move_count, player);
if (did_swap) { if (did_swap) {
// Item is now placed in source list // Already swapped. The other stack is now placed in "from" list
list_from = get_borrow_checked_invlist(from_inv, from_list); list_from = get_borrow_checked_invlist(from_inv, from_list);
if (list_from) { if (list_from) {
src_item = list_from->getItem(from_i); src_item = list_from->getItem(from_i);
@ -603,26 +599,34 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame
} }
mgr->setInventoryModified(from_inv); mgr->setInventoryModified(from_inv);
} else { } else {
int src_item_count = src_item.count; ItemStack swap_item;
if (caused_by_move_somewhere)
// When moving somewhere: temporarily use the actual movable stack
// size to ensure correct callback execution.
src_item.count = move_count;
onPutAndOnTake(src_item, player);
if (caused_by_move_somewhere)
// Reset source item count
src_item.count = src_item_count;
if (did_swap) { if (did_swap) {
// Item is now placed in source list // Already swapped. The other stack is now placed in "from" list
list_from = get_borrow_checked_invlist(from_inv, from_list); list_from = get_borrow_checked_invlist(from_inv, from_list);
if (list_from) { if (list_from) {
src_item = list_from->getItem(from_i); swap_item = list_from->getItem(from_i);
list_from.reset(); list_from.reset();
swapDirections();
onPutAndOnTake(src_item, player);
swapDirections();
} }
} }
// 1. Take the ItemStack (visually: freely detached)
onTake(src_item, player);
if (!swap_item.empty() && get_borrow_checked_invlist(to_inv, to_list)) {
swapDirections();
onTake(swap_item, player);
swapDirections();
}
// 2. Put the ItemStack
if (get_borrow_checked_invlist(to_inv, to_list))
onPut(src_item, player);
if (!swap_item.empty() && get_borrow_checked_invlist(to_inv, to_list)) {
swapDirections();
onPut(swap_item, player);
swapDirections();
}
mgr->setInventoryModified(to_inv); mgr->setInventoryModified(to_inv);
mgr->setInventoryModified(from_inv); mgr->setInventoryModified(from_inv);
} }

@ -191,7 +191,8 @@ struct IMoveAction : public InventoryAction, public MoveAction
void swapDirections(); void swapDirections();
void onPutAndOnTake(const ItemStack &src_item, ServerActiveObject *player) const; void onTake(const ItemStack &src_item, ServerActiveObject *player) const;
void onPut(const ItemStack &src_item, ServerActiveObject *player) const;
void onMove(int count, ServerActiveObject *player) const; void onMove(int count, ServerActiveObject *player) const;