From 977d493c7c6eea152b6cc4a37ce5e9d01b26530b Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:46:54 +0200 Subject: [PATCH 1/7] add ``readonly`` field to ``Inventory`` --- crates/valence_inventory/src/lib.rs | 158 ++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 9 deletions(-) diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 2fdf5e06e..650e609f9 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -71,6 +71,11 @@ pub struct Inventory { /// Contains a set bit for each modified slot in `slots`. #[doc(hidden)] pub changed: u64, + /// Makes a inventory read-only. This will prevent adding + /// or removing items. If this is a player inventory + /// This will also make it impossible to drop items while not + /// in the inventory (e.g. by pressing Q) + pub readonly: bool, } impl Inventory { @@ -85,6 +90,7 @@ impl Inventory { kind, slots: vec![ItemStack::EMPTY; kind.slot_count()].into(), changed: 0, + readonly: false, } } @@ -862,7 +868,7 @@ fn handle_click_slot( if pkt.slot_idx < 0 && pkt.mode == ClickMode::Click { // The client is dropping the cursor item by clicking outside the window. - + let stack = std::mem::take(&mut cursor_item.0); if !stack.is_empty() { @@ -907,6 +913,17 @@ fn handle_click_slot( if (0_i16..target_inventory.slot_count() as i16).contains(&pkt.slot_idx) { // The player is dropping an item from another inventory. + if target_inventory.readonly { + // resync target inventory + client.write_packet(&InventoryS2c { + window_id: inv_state.window_id, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(target_inventory.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + continue; + } + let stack = target_inventory.slot(pkt.slot_idx as u16); if !stack.is_empty() { @@ -930,6 +947,18 @@ fn handle_click_slot( } } else { // The player is dropping an item from their inventory. + + if client_inv.readonly { + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(client_inv.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + continue; + } + let slot_id = convert_to_player_slot_id(target_inventory.kind, pkt.slot_idx as u16); @@ -958,6 +987,17 @@ fn handle_click_slot( // The player has no inventory open and is dropping an item from their // inventory. + if client_inv.readonly { + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(client_inv.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + continue; + } + let stack = client_inv.slot(pkt.slot_idx as u16); if !stack.is_empty() { @@ -994,7 +1034,8 @@ fn handle_click_slot( } if let Some(mut open_inventory) = open_inventory { - // The player is interacting with an inventory that is open. + // The player is interacting with an inventory that is + // open or has an inventory open while interacting with their own inventory. let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else { // The inventory does not exist, ignore. @@ -1018,14 +1059,32 @@ fn handle_click_slot( continue; } - cursor_item.set_if_neq(CursorItem(pkt.carried_item.clone())); + let mut new_cursor = pkt.carried_item.clone(); for slot in pkt.slot_changes.iter() { + let transferred_between_inventories = + ((0_i16..target_inventory.slot_count() as i16).contains(&pkt.slot_idx) + && pkt.mode == ClickMode::Hotbar) + || pkt.mode == ClickMode::ShiftClick; + if (0_i16..target_inventory.slot_count() as i16).contains(&slot.idx) { - // The client is interacting with a slot in the target inventory. + if (client_inv.readonly && transferred_between_inventories) + || target_inventory.readonly + { + new_cursor = cursor_item.0.clone(); + continue; + } + target_inventory.set_slot(slot.idx as u16, slot.stack.clone()); open_inventory.client_changed |= 1 << slot.idx; } else { + if (target_inventory.readonly && transferred_between_inventories) + || client_inv.readonly + { + new_cursor = cursor_item.0.clone(); + continue; + } + // The client is interacting with a slot in their own inventory. let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16); @@ -1033,6 +1092,26 @@ fn handle_click_slot( inv_state.slots_changed |= 1 << slot_id; } } + + cursor_item.set_if_neq(CursorItem(new_cursor)); + + if target_inventory.readonly || client_inv.readonly { + // resync the target inventory + client.write_packet(&InventoryS2c { + window_id: inv_state.window_id, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(target_inventory.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(client_inv.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + } } else { // The client is interacting with their own inventory. @@ -1053,11 +1132,16 @@ fn handle_click_slot( continue; } - cursor_item.set_if_neq(CursorItem(pkt.carried_item.clone())); + let mut new_cursor = pkt.carried_item.clone(); + inv_state.client_updated_cursor_item = true; for slot in pkt.slot_changes.iter() { if (0_i16..client_inv.slot_count() as i16).contains(&slot.idx) { + if client_inv.readonly { + new_cursor = cursor_item.0.clone(); + continue; + } client_inv.set_slot(slot.idx as u16, slot.stack.clone()); inv_state.slots_changed |= 1 << slot.idx; } else { @@ -1069,6 +1153,18 @@ fn handle_click_slot( ); } } + + cursor_item.set_if_neq(CursorItem(new_cursor)); + + if client_inv.readonly { + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(client_inv.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + } } click_slot_events.send(ClickSlotEvent { @@ -1087,14 +1183,31 @@ fn handle_click_slot( fn handle_player_actions( mut packets: EventReader, - mut clients: Query<(&mut Inventory, &mut ClientInventoryState, &HeldItem)>, + mut clients: Query<( + &mut Inventory, + &mut ClientInventoryState, + &HeldItem, + &mut Client, + )>, mut drop_item_stack_events: EventWriter, ) { for packet in packets.read() { if let Some(pkt) = packet.decode::() { match pkt.action { PlayerAction::DropAllItems => { - if let Ok((mut inv, mut inv_state, &held)) = clients.get_mut(packet.client) { + if let Ok((mut inv, mut inv_state, &held, mut client)) = + clients.get_mut(packet.client) + { + if inv.readonly { + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(inv.slot_slice()), + carried_item: Cow::Borrowed(&ItemStack::EMPTY), + }); + } + let stack = inv.replace_slot(held.slot(), ItemStack::EMPTY); if !stack.is_empty() { @@ -1109,7 +1222,20 @@ fn handle_player_actions( } } PlayerAction::DropItem => { - if let Ok((mut inv, mut inv_state, held)) = clients.get_mut(packet.client) { + if let Ok((mut inv, mut inv_state, held, mut client)) = + clients.get_mut(packet.client) + { + if inv.readonly { + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(inv.slot_slice()), + carried_item: Cow::Borrowed(&ItemStack::EMPTY), + }); + continue; + } + let mut stack = inv.replace_slot(held.slot(), ItemStack::EMPTY); if !stack.is_empty() { @@ -1133,7 +1259,21 @@ fn handle_player_actions( } } PlayerAction::SwapItemWithOffhand => { - if let Ok((mut inv, _, held)) = clients.get_mut(packet.client) { + if let Ok((mut inv, inv_state, held, mut client)) = + clients.get_mut(packet.client) + { + // this check here might not actually be necessary + if inv.readonly { + // resync the client inventory + client.write_packet(&InventoryS2c { + window_id: 0, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(inv.slot_slice()), + carried_item: Cow::Borrowed(&ItemStack::EMPTY), + }); + continue; + } + inv.swap_slot(held.slot(), PlayerInventory::SLOT_OFFHAND); } } From 33b4af0c4cdaa1e5ab6e7336a77396238d7b898a Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:58:00 +0200 Subject: [PATCH 2/7] format --- crates/valence_inventory/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 650e609f9..9007d7b52 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -73,7 +73,7 @@ pub struct Inventory { pub changed: u64, /// Makes a inventory read-only. This will prevent adding /// or removing items. If this is a player inventory - /// This will also make it impossible to drop items while not + /// This will also make it impossible to drop items while not /// in the inventory (e.g. by pressing Q) pub readonly: bool, } @@ -868,7 +868,7 @@ fn handle_click_slot( if pkt.slot_idx < 0 && pkt.mode == ClickMode::Click { // The client is dropping the cursor item by clicking outside the window. - + let stack = std::mem::take(&mut cursor_item.0); if !stack.is_empty() { From 8afd6a2375495a622abfc8d2730090b4c108becc Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:17:28 +0200 Subject: [PATCH 3/7] fix player still being able to drop stack --- crates/valence_inventory/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 9007d7b52..686b2f49b 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -1206,6 +1206,7 @@ fn handle_player_actions( slots: Cow::Borrowed(inv.slot_slice()), carried_item: Cow::Borrowed(&ItemStack::EMPTY), }); + continue; } let stack = inv.replace_slot(held.slot(), ItemStack::EMPTY); From e6d9c5bdf2ce07fd4b553577a9e9e96627930817 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:10:47 +0200 Subject: [PATCH 4/7] add tests (some not working) --- src/tests/inventory.rs | 652 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 652 insertions(+) diff --git a/src/tests/inventory.rs b/src/tests/inventory.rs index d1d6d900f..575485956 100644 --- a/src/tests/inventory.rs +++ b/src/tests/inventory.rs @@ -330,6 +330,79 @@ fn test_should_modify_open_inventory_click_slot() { assert_eq!(cursor_item.0, ItemStack::new(ItemKind::Diamond, 2, None)); } +#[test] +fn test_prevent_modify_open_inventory_click_slot_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // The open inventory is readonly, the client can not interact with it. + let inventory_ent = set_up_open_inventory(&mut app, client); + + let mut inventory = app + .world_mut() + .get_mut::(inventory_ent) + .expect("could not find inventory for client"); + + inventory.readonly = true; + inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None)); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + // Make the client click the slot and pick up the item. + let inv_state = app.world_mut().get::(client).unwrap(); + let state_id = inv_state.state_id(); + let window_id = inv_state.window_id(); + + helper.send(&ClickSlotC2s { + window_id, + state_id: VarInt(state_id.0), + slot_idx: 20, + button: 0, + // If the inventory is readonly, this should actually not be possible, + // as you cant even select an item (so its on your cursor), + // this is also why 2 resyncs are sent, see below. + mode: ClickMode::Click, + slot_changes: vec![SlotChange { + idx: 20, + stack: ItemStack::EMPTY, + }] + .into(), + carried_item: ItemStack::new(ItemKind::Diamond, 2, None), + }); + + app.update(); + + let sent_packets = helper.collect_received(); + + // because the inventory is readonly, we need to resync the client's inventory. + // 2 resync packets are sent, see above. + sent_packets.assert_count::(2); + sent_packets.assert_count::(0); + + // Make assertions + let inventory = app + .world_mut() + .get::(inventory_ent) + .expect("could not find inventory"); + // Inventory is read-only, the item is not being moved + assert_eq!( + inventory.slot(20), + &ItemStack::new(ItemKind::Diamond, 2, None) + ); + let cursor_item = app + .world_mut() + .get::(client) + .expect("could not find client"); + // Inventory is read-only, items can not be picked up with the cursor + assert_eq!(cursor_item.0, ItemStack::EMPTY); +} + #[test] fn test_should_modify_open_inventory_server_side() { let ScenarioSingleClient { @@ -372,6 +445,262 @@ fn test_should_modify_open_inventory_server_side() { ); } +// NOT WORKING +// #[test] +// fn test_prevent_hotbar_item_click_container_readonly_inventory() { +// let ScenarioSingleClient { +// mut app, +// client, +// mut helper, +// .. +// } = ScenarioSingleClient::new(); + +// // Process a tick to get past the "on join" logic. +// app.update(); +// helper.clear_received(); + +// // player inventory is not read-only +// let mut player_inventory = app +// .world_mut() +// .get_mut::(client) +// .expect("could not find inventory for client"); + +// // 36 is the first hotbar slot +// player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, +// None)); + +// let open_inv_ent = set_up_open_inventory(&mut app, client); + +// let mut open_inventory = app +// .world_mut() +// .get_mut::(open_inv_ent) +// .expect("could not find inventory for client"); + +// // Open inventory is read-only +// // open_inventory.readonly = true; +// open_inventory.set_slot(0, ItemStack::new(ItemKind::IronIngot, 10, +// None)); + +// let inv_state = +// app.world_mut().get::(client).unwrap(); +// let state_id = inv_state.state_id(); +// let window_id = inv_state.window_id(); + +// // The player hovers over the iron ingots in the open inventory, and +// tries // to move them to their own (via pressing 1), which should swap +// the iron // for the diamonds. However the opened inventory is read-only, +// so nothing // should happen. +// helper.send(&ClickSlotC2s { +// window_id, +// state_id: VarInt(state_id.0), +// slot_idx: 0, +// button: 0, // hotbar slot starting at 0 +// mode: ClickMode::Hotbar, +// slot_changes: vec![ +// // First SlotChange is the item is the slot in the player's +// hotbar. // target slot. +// SlotChange { +// idx: 0, +// stack: ItemStack::new(ItemKind::Diamond, 1, None), +// }, +// SlotChange { +// // 54 is the players hotbar slot 1, when the 9x3 inventory is +// opnened. idx: 54, +// stack: ItemStack::new(ItemKind::IronIngot, 10, None), +// }, +// // The second one is the slot in the open inventory, after the +// ClickSlot action // source slot. +// ] +// .into(), +// carried_item: ItemStack::EMPTY, +// }); + +// app.update(); + +// let sent_packets = helper.collect_received(); + +// // 1 resync for each inventory +// sent_packets.assert_count::(2); + +// // Make assertions +// let player_inventory = app +// .world_mut() +// .get::(client) +// .expect("could not find client"); + +// // Opened inventory is read-only, the items are not swapped. +// assert_eq!(player_inventory.slot(36), &ItemStack::new(ItemKind::Diamond, +// 1, None)); + +// let open_inventory = app +// .world_mut() +// .get::(open_inv_ent) +// .expect("could not find inventory"); + +// // Opened inventory is read-only, the items are not swapped. +// assert_eq!(open_inventory.slot(0), &ItemStack::new(ItemKind::IronIngot, +// 10, None)); } + +// NOT WORKING +// #[test] +// fn test_still_allow_hotbar_item_click_in_own_inventory_if_container_readonly_inventory() { +// let ScenarioSingleClient { +// mut app, +// client, +// mut helper, +// .. +// } = ScenarioSingleClient::new(); + +// // Process a tick to get past the "on join" logic. +// app.update(); +// helper.clear_received(); + +// // player inventory is not read-only +// let mut player_inventory = app +// .world_mut() +// .get_mut::(client) +// .expect("could not find inventory for client"); + +// // 36 is the first hotbar slot +// player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 10, +// None)); + +// let open_inv_ent = set_up_open_inventory(&mut app, client); + +// let mut open_inventory = app +// .world_mut() +// .get_mut::(open_inv_ent) +// .expect("could not find inventory for client"); + +// // Open inventory is read-only +// open_inventory.readonly = true; + +// let inv_state = +// app.world_mut().get::(client).unwrap(); +// let state_id = inv_state.state_id(); +// let window_id = inv_state.window_id(); + +// // The player's inventory is not readonly, so the player should still be +// // able to move items from the hotbar to other parts of the inventory +// even // if the other inventory is still open. +// helper.send(&ClickSlotC2s { +// window_id, +// state_id: VarInt(state_id.0), +// slot_idx: 27, +// button: 0, // hotbar slot starting at 0 +// mode: ClickMode::Hotbar, +// slot_changes: vec![ +// SlotChange { +// idx: 27, +// stack: ItemStack::new(ItemKind::Diamond, 10, None), +// }, +// SlotChange { +// idx: 54, +// stack: ItemStack::EMPTY, +// }, +// ] +// .into(), +// carried_item: ItemStack::EMPTY, +// }); + +// app.update(); +// // Make assertions +// let sent_packets = helper.collect_received(); +// sent_packets.assert_count::(2); + +// let player_inventory = app +// .world_mut() +// .get::(client) +// .expect("could not find client"); + +// // Items swapped successfully, as player item is not read-only +// assert_eq!(player_inventory.slot(36), &ItemStack::EMPTY); +// assert_eq!(player_inventory.slot(9), &ItemStack::new(ItemKind::Diamond, +// 1, None)); } + +// NOT WORKING +// #[test] +// fn test_prevent_shift_item_click_container_readonly_inventory() { +// let ScenarioSingleClient { +// mut app, +// client, +// mut helper, +// .. +// } = ScenarioSingleClient::new(); + +// // Process a tick to get past the "on join" logic. +// app.update(); +// helper.clear_received(); + +// // player inventory is not read-only +// let mut player_inventory = app +// .world_mut() +// .get_mut::(client) +// .expect("could not find inventory for client"); + +// player_inventory.set_slot(9, ItemStack::new(ItemKind::Diamond, 64, +// None)); + +// let open_inv_ent = set_up_open_inventory(&mut app, client); + +// let mut open_inventory = app +// .world_mut() +// .get_mut::(open_inv_ent) +// .expect("could not find inventory for client"); + +// // Open inventory is read-only +// open_inventory.readonly = true; + +// let inv_state = +// app.world_mut().get::(client).unwrap(); +// let state_id = inv_state.state_id(); +// let window_id = inv_state.window_id(); + +// // The player tries to Shift-click transfer the stack of diamonds into +// the open container helper.send(&ClickSlotC2s { +// window_id, +// state_id: VarInt(state_id.0), +// slot_idx: 54, +// button: 0, // hotbar slot starting at 0 +// mode: ClickMode::ShiftClick, +// slot_changes: vec![ +// // target +// SlotChange { +// idx: 0, +// stack: ItemStack::new(ItemKind::Diamond, 64, None), +// }, +// // source +// SlotChange { +// idx: 27, +// stack: ItemStack::EMPTY, +// }, +// ] +// .into(), +// carried_item: ItemStack::EMPTY, +// }); + +// app.update(); + +// // Make assertions +// let sent_packets = helper.collect_received(); +// sent_packets.assert_count::(2); + +// let player_inventory = app +// .world_mut() +// .get::(client) +// .expect("could not find client"); + +// assert_eq!(player_inventory.slot(9), &ItemStack::new(ItemKind::Diamond, +// 64, None)); + +// let open_inventory = app +// .world_mut() +// .get::(open_inv_ent) +// .expect("could not find inventory"); + +// assert_eq!(open_inventory.slot(0), &ItemStack::EMPTY); +// } + #[test] fn test_should_sync_entire_open_inventory() { let ScenarioSingleClient { @@ -640,6 +969,63 @@ mod dropping_items { sent_packets.assert_count::(0); } + #[test] + fn prevent_drop_item_player_action_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let mut inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + inventory.readonly = true; + inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 3, None)); + + helper.send(&PlayerActionC2s { + action: PlayerAction::DropItem, + position: BlockPos::new(0, 0, 0), + direction: Direction::Down, + sequence: VarInt(0), + }); + + app.update(); + + // Make assertions + let inventory = app + .world_mut() + .get::(client) + .expect("could not find client"); + + assert_eq!( + inventory.slot(36), + // Inventory is read-only, item is not being dropped + &ItemStack::new(ItemKind::IronIngot, 3, None) + ); + + let events = app + .world_mut() + .get_resource::>() + .expect("expected drop item stack events"); + + let events = events.iter_current_update_events().collect::>(); + + // when the inventory is read-only we do not emit a drop event + assert_eq!(events.len(), 0); + + let sent_packets = helper.collect_received(); + + // we do need to update the player inventory so we dont desync + sent_packets.assert_count::(1); + } + #[test] fn should_drop_item_stack_player_action() { let ScenarioSingleClient { @@ -693,6 +1079,60 @@ mod dropping_items { ); } + #[test] + fn prevent_drop_item_stack_player_action_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let mut inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + inventory.readonly = true; + inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 32, None)); + + helper.send(&PlayerActionC2s { + action: PlayerAction::DropAllItems, + position: BlockPos::new(0, 0, 0), + direction: Direction::Down, + sequence: VarInt(0), + }); + + app.update(); + + // Make assertions + let held = app + .world_mut() + .get::(client) + .expect("could not find client"); + assert_eq!(held.slot(), 36); + let inventory = app + .world_mut() + .get::(client) + .expect("could not find inventory"); + // Inventory is read-only, item is not being dropped + assert_eq!( + inventory.slot(36), + &ItemStack::new(ItemKind::IronIngot, 32, None) + ); + let events = app + .world_mut() + .get_resource::>() + .expect("expected drop item stack events"); + let events = events.iter_current_update_events().collect::>(); + + // when the inventory is read-only we do not emit a drop event + assert_eq!(events.len(), 0); + } + #[test] fn should_drop_item_stack_set_creative_mode_slot() { let ScenarioSingleClient { @@ -854,6 +1294,73 @@ mod dropping_items { ); } + #[test] + fn prevent_drop_item_click_container_with_dropkey_single_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let inv_state = app + .world_mut() + .get_mut::(client) + .expect("could not find client"); + + let state_id = inv_state.state_id().0; + + let mut inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + + inventory.readonly = true; + inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None)); + + helper.send(&ClickSlotC2s { + window_id: 0, + slot_idx: 40, + button: 0, + mode: ClickMode::DropKey, + state_id: VarInt(state_id), + slot_changes: vec![SlotChange { + idx: 40, + stack: ItemStack::new(ItemKind::IronIngot, 31, None), + }] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + + // Make assertions + let inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + + assert_eq!( + inventory.slot(40), + // Inventory is read-only, item is not being dropped + &ItemStack::new(ItemKind::IronIngot, 32, None) + ); + + let events = app + .world_mut() + .get_resource::>() + .expect("expected drop item stack events"); + + let events = events.iter_current_update_events().collect::>(); + + // when the inventory is read-only we do not emit a drop event + assert_eq!(events.len(), 0); + } + #[test] fn should_drop_item_stack_click_container_with_dropkey() { let ScenarioSingleClient { @@ -914,6 +1421,73 @@ mod dropping_items { ); } + #[test] + fn prevent_drop_item_stack_click_container_with_dropkey_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let inv_state = app + .world_mut() + .get_mut::(client) + .expect("could not find client"); + + let state_id = inv_state.state_id().0; + + let mut inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + + inventory.readonly = true; + inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None)); + + helper.send(&ClickSlotC2s { + window_id: 0, + slot_idx: 40, + button: 1, // pressing control + mode: ClickMode::DropKey, + state_id: VarInt(state_id), + slot_changes: vec![SlotChange { + idx: 40, + stack: ItemStack::EMPTY, + }] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + + // Make assertions + let inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + + assert_eq!( + inventory.slot(40), + // Inventory is read-only, item is not being dropped + &ItemStack::new(ItemKind::IronIngot, 32, None) + ); + + let events = app + .world_mut() + .get_resource::>() + .expect("expected drop item stack events"); + + let events = events.iter_current_update_events().collect::>(); + + // when the inventory is read-only we do not emit a drop event + assert_eq!(events.len(), 0); + } + /// The item should be dropped successfully, if the player has an inventory /// open and the slot id points to his inventory. #[test] @@ -1000,6 +1574,84 @@ mod dropping_items { &ItemStack::new(ItemKind::IronIngot, 31, None) ); } + + #[test] + fn prevent_drop_item_player_open_inventory_with_dropkey_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + + let mut inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory"); + + inventory.readonly = true; + inventory.set_slot( + convert_to_player_slot_id(InventoryKind::Generic9x3, 50), + ItemStack::new(ItemKind::IronIngot, 32, None), + ); + + let _inventory_ent = set_up_open_inventory(&mut app, client); + + app.update(); + + helper.clear_received(); + + let inv_state = app + .world_mut() + .get_mut::(client) + .expect("could not find client"); + + let state_id = inv_state.state_id().0; + let window_id = inv_state.window_id(); + + helper.send(&ClickSlotC2s { + window_id, + state_id: VarInt(state_id), + slot_idx: 50, // not pressing control + button: 0, + mode: ClickMode::DropKey, + slot_changes: vec![SlotChange { + idx: 50, + stack: ItemStack::new(ItemKind::IronIngot, 31, None), + }] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + + // Make assertions + let events = app + .world() + .get_resource::>() + .expect("expected drop item stack events"); + + let player_inventory = app + .world() + .get::(client) + .expect("could not find inventory"); + + let events = events.iter_current_update_events().collect::>(); + // when the inventory is read-only we do not emit a drop event + assert_eq!(events.len(), 0); + + // Also make sure that the player inventory was not updated (as it is + // read-only). + let expected_player_slot_id = convert_to_player_slot_id(InventoryKind::Generic9x3, 50); + assert_eq!( + player_inventory.slot(expected_player_slot_id), + // Inventory is read-only, item is not being dropped + &ItemStack::new(ItemKind::IronIngot, 32, None) + ); + } } /// The item stack should be dropped successfully, if the player has an From 454cfd7a03f50d23eb6e8d59bbc246586055dc6c Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:11:17 +0200 Subject: [PATCH 5/7] Update crates/valence_inventory/src/lib.rs Co-authored-by: Carson McManus --- crates/valence_inventory/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 686b2f49b..fd986043b 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -71,7 +71,7 @@ pub struct Inventory { /// Contains a set bit for each modified slot in `slots`. #[doc(hidden)] pub changed: u64, - /// Makes a inventory read-only. This will prevent adding + /// Makes an inventory read-only for clients. This will prevent adding /// or removing items. If this is a player inventory /// This will also make it impossible to drop items while not /// in the inventory (e.g. by pressing Q) From 12220a4788056c1c7d66b102b03c08822bdd9cd3 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:21:42 +0200 Subject: [PATCH 6/7] fix tests --- src/tests/inventory.rs | 622 ++++++++++++++++++++++++----------------- 1 file changed, 367 insertions(+), 255 deletions(-) diff --git a/src/tests/inventory.rs b/src/tests/inventory.rs index 575485956..8c83261ec 100644 --- a/src/tests/inventory.rs +++ b/src/tests/inventory.rs @@ -445,261 +445,373 @@ fn test_should_modify_open_inventory_server_side() { ); } -// NOT WORKING -// #[test] -// fn test_prevent_hotbar_item_click_container_readonly_inventory() { -// let ScenarioSingleClient { -// mut app, -// client, -// mut helper, -// .. -// } = ScenarioSingleClient::new(); - -// // Process a tick to get past the "on join" logic. -// app.update(); -// helper.clear_received(); - -// // player inventory is not read-only -// let mut player_inventory = app -// .world_mut() -// .get_mut::(client) -// .expect("could not find inventory for client"); - -// // 36 is the first hotbar slot -// player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, -// None)); - -// let open_inv_ent = set_up_open_inventory(&mut app, client); - -// let mut open_inventory = app -// .world_mut() -// .get_mut::(open_inv_ent) -// .expect("could not find inventory for client"); - -// // Open inventory is read-only -// // open_inventory.readonly = true; -// open_inventory.set_slot(0, ItemStack::new(ItemKind::IronIngot, 10, -// None)); - -// let inv_state = -// app.world_mut().get::(client).unwrap(); -// let state_id = inv_state.state_id(); -// let window_id = inv_state.window_id(); - -// // The player hovers over the iron ingots in the open inventory, and -// tries // to move them to their own (via pressing 1), which should swap -// the iron // for the diamonds. However the opened inventory is read-only, -// so nothing // should happen. -// helper.send(&ClickSlotC2s { -// window_id, -// state_id: VarInt(state_id.0), -// slot_idx: 0, -// button: 0, // hotbar slot starting at 0 -// mode: ClickMode::Hotbar, -// slot_changes: vec![ -// // First SlotChange is the item is the slot in the player's -// hotbar. // target slot. -// SlotChange { -// idx: 0, -// stack: ItemStack::new(ItemKind::Diamond, 1, None), -// }, -// SlotChange { -// // 54 is the players hotbar slot 1, when the 9x3 inventory is -// opnened. idx: 54, -// stack: ItemStack::new(ItemKind::IronIngot, 10, None), -// }, -// // The second one is the slot in the open inventory, after the -// ClickSlot action // source slot. -// ] -// .into(), -// carried_item: ItemStack::EMPTY, -// }); - -// app.update(); - -// let sent_packets = helper.collect_received(); - -// // 1 resync for each inventory -// sent_packets.assert_count::(2); - -// // Make assertions -// let player_inventory = app -// .world_mut() -// .get::(client) -// .expect("could not find client"); - -// // Opened inventory is read-only, the items are not swapped. -// assert_eq!(player_inventory.slot(36), &ItemStack::new(ItemKind::Diamond, -// 1, None)); - -// let open_inventory = app -// .world_mut() -// .get::(open_inv_ent) -// .expect("could not find inventory"); - -// // Opened inventory is read-only, the items are not swapped. -// assert_eq!(open_inventory.slot(0), &ItemStack::new(ItemKind::IronIngot, -// 10, None)); } - -// NOT WORKING -// #[test] -// fn test_still_allow_hotbar_item_click_in_own_inventory_if_container_readonly_inventory() { -// let ScenarioSingleClient { -// mut app, -// client, -// mut helper, -// .. -// } = ScenarioSingleClient::new(); - -// // Process a tick to get past the "on join" logic. -// app.update(); -// helper.clear_received(); - -// // player inventory is not read-only -// let mut player_inventory = app -// .world_mut() -// .get_mut::(client) -// .expect("could not find inventory for client"); - -// // 36 is the first hotbar slot -// player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 10, -// None)); - -// let open_inv_ent = set_up_open_inventory(&mut app, client); - -// let mut open_inventory = app -// .world_mut() -// .get_mut::(open_inv_ent) -// .expect("could not find inventory for client"); - -// // Open inventory is read-only -// open_inventory.readonly = true; - -// let inv_state = -// app.world_mut().get::(client).unwrap(); -// let state_id = inv_state.state_id(); -// let window_id = inv_state.window_id(); - -// // The player's inventory is not readonly, so the player should still be -// // able to move items from the hotbar to other parts of the inventory -// even // if the other inventory is still open. -// helper.send(&ClickSlotC2s { -// window_id, -// state_id: VarInt(state_id.0), -// slot_idx: 27, -// button: 0, // hotbar slot starting at 0 -// mode: ClickMode::Hotbar, -// slot_changes: vec![ -// SlotChange { -// idx: 27, -// stack: ItemStack::new(ItemKind::Diamond, 10, None), -// }, -// SlotChange { -// idx: 54, -// stack: ItemStack::EMPTY, -// }, -// ] -// .into(), -// carried_item: ItemStack::EMPTY, -// }); - -// app.update(); -// // Make assertions -// let sent_packets = helper.collect_received(); -// sent_packets.assert_count::(2); - -// let player_inventory = app -// .world_mut() -// .get::(client) -// .expect("could not find client"); - -// // Items swapped successfully, as player item is not read-only -// assert_eq!(player_inventory.slot(36), &ItemStack::EMPTY); -// assert_eq!(player_inventory.slot(9), &ItemStack::new(ItemKind::Diamond, -// 1, None)); } - -// NOT WORKING -// #[test] -// fn test_prevent_shift_item_click_container_readonly_inventory() { -// let ScenarioSingleClient { -// mut app, -// client, -// mut helper, -// .. -// } = ScenarioSingleClient::new(); - -// // Process a tick to get past the "on join" logic. -// app.update(); -// helper.clear_received(); - -// // player inventory is not read-only -// let mut player_inventory = app -// .world_mut() -// .get_mut::(client) -// .expect("could not find inventory for client"); - -// player_inventory.set_slot(9, ItemStack::new(ItemKind::Diamond, 64, -// None)); - -// let open_inv_ent = set_up_open_inventory(&mut app, client); - -// let mut open_inventory = app -// .world_mut() -// .get_mut::(open_inv_ent) -// .expect("could not find inventory for client"); - -// // Open inventory is read-only -// open_inventory.readonly = true; - -// let inv_state = -// app.world_mut().get::(client).unwrap(); -// let state_id = inv_state.state_id(); -// let window_id = inv_state.window_id(); - -// // The player tries to Shift-click transfer the stack of diamonds into -// the open container helper.send(&ClickSlotC2s { -// window_id, -// state_id: VarInt(state_id.0), -// slot_idx: 54, -// button: 0, // hotbar slot starting at 0 -// mode: ClickMode::ShiftClick, -// slot_changes: vec![ -// // target -// SlotChange { -// idx: 0, -// stack: ItemStack::new(ItemKind::Diamond, 64, None), -// }, -// // source -// SlotChange { -// idx: 27, -// stack: ItemStack::EMPTY, -// }, -// ] -// .into(), -// carried_item: ItemStack::EMPTY, -// }); - -// app.update(); - -// // Make assertions -// let sent_packets = helper.collect_received(); -// sent_packets.assert_count::(2); - -// let player_inventory = app -// .world_mut() -// .get::(client) -// .expect("could not find client"); - -// assert_eq!(player_inventory.slot(9), &ItemStack::new(ItemKind::Diamond, -// 64, None)); - -// let open_inventory = app -// .world_mut() -// .get::(open_inv_ent) -// .expect("could not find inventory"); - -// assert_eq!(open_inventory.slot(0), &ItemStack::EMPTY); -// } +#[test] +fn test_hotbar_item_swap_container() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory for client"); + + // 36 is the first hotbar slot + player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, None)); + + let open_inv_ent = set_up_open_inventory(&mut app, client); + + let mut open_inventory = app + .world_mut() + .get_mut::(open_inv_ent) + .expect("could not find inventory for client"); + + open_inventory.set_slot(0, ItemStack::new(ItemKind::IronIngot, 10, None)); + + // This update makes sure we have the items in the inventory by the time the + // client wants to update these + app.update(); + helper.clear_received(); + let inv_state = app.world_mut().get::(client).unwrap(); + let state_id = inv_state.state_id(); + let window_id = inv_state.window_id(); + + // The player hovers over the iron ingots in the open inventory, and tries + // to move them to their own (via pressing 1), which should swap the iron + // for the diamonds. + helper.send(&ClickSlotC2s { + window_id, + state_id: VarInt(state_id.0), + slot_idx: 0, + button: 0, // hotbar slot starting at 0 + mode: ClickMode::Hotbar, + slot_changes: vec![ + // First SlotChange is the item is the slot in the player's hotbar. + // target slot. + SlotChange { + idx: 0, + stack: ItemStack::new(ItemKind::Diamond, 1, None), + }, + SlotChange { + // 54 is the players hotbar slot 1, when the 9x3 inventory is opnened. + idx: 54, + stack: ItemStack::new(ItemKind::IronIngot, 10, None), + }, + // The second one is the slot in the open inventory, after the ClickSlot action + // source slot. + ] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + + let sent_packets = helper.collect_received(); + + // No resyncs because the client was in sync and sent us the updates + sent_packets.assert_count::(0); + + // Make assertions + let player_inventory = app + .world_mut() + .get::(client) + .expect("could not find client"); + + // Swapped items successfully + assert_eq!( + player_inventory.slot(36), + &ItemStack::new(ItemKind::IronIngot, 10, None) + ); + + let open_inventory = app + .world_mut() + .get::(open_inv_ent) + .expect("could not find inventory"); + + assert_eq!( + open_inventory.slot(0), + &ItemStack::new(ItemKind::Diamond, 1, None) + ); +} + +#[test] +fn test_prevent_hotbar_item_click_container_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + // player inventory is not read-only + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory for client"); + + // 36 is the first hotbar slot + player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, None)); + + let open_inv_ent = set_up_open_inventory(&mut app, client); + + let mut open_inventory = app + .world_mut() + .get_mut::(open_inv_ent) + .expect("could not find inventory for client"); + + // Open inventory is read-only + open_inventory.readonly = true; + open_inventory.set_slot(0, ItemStack::new(ItemKind::IronIngot, 10, None)); + + // This update makes sure we have the items in the inventory by the time the + // client wants to update these + app.update(); + helper.clear_received(); + + let inv_state = app.world_mut().get::(client).unwrap(); + let state_id = inv_state.state_id(); + let window_id = inv_state.window_id(); + + // The player hovers over the iron ingots in the open inventory, and tries + // to move them to their own (via pressing 1), which should swap the iron + // for the diamonds. However the opened inventory is read-only, so nothing + // should happen. + helper.send(&ClickSlotC2s { + window_id, + state_id: VarInt(state_id.0), + slot_idx: 0, + button: 0, // hotbar slot starting at 0 + mode: ClickMode::Hotbar, + slot_changes: vec![ + // First SlotChange is the item is the slot in the player's hotbar. + // target slot. + SlotChange { + idx: 0, + stack: ItemStack::new(ItemKind::Diamond, 1, None), + }, + // The second one is the slot in the open inventory, after the ClickSlot action + // source slot. + SlotChange { + // 54 is the players hotbar slot 1, when the 9x3 inventory is opnened. + idx: 54, + stack: ItemStack::new(ItemKind::IronIngot, 10, None), + }, + ] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + + let sent_packets = helper.collect_received(); + + // 1 resync for each inventory + sent_packets.assert_count::(2); + + // Make assertions + let player_inventory = app + .world_mut() + .get::(client) + .expect("could not find client"); + + // Opened inventory is read-only, the items are not swapped. + assert_eq!( + player_inventory.slot(36), + &ItemStack::new(ItemKind::Diamond, 1, None) + ); + + let open_inventory = app + .world_mut() + .get::(open_inv_ent) + .expect("could not find inventory"); + + // Opened inventory is read-only, the items are not swapped. + assert_eq!( + open_inventory.slot(0), + &ItemStack::new(ItemKind::IronIngot, 10, None) + ); +} + +#[test] +fn test_still_allow_hotbar_item_click_in_own_inventory_if_container_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + // player inventory is not read-only + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory for client"); + + // 36 is the first hotbar slot + player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 10, None)); + + let open_inv_ent = set_up_open_inventory(&mut app, client); + + let mut open_inventory = app + .world_mut() + .get_mut::(open_inv_ent) + .expect("could not find inventory for client"); + + // Open inventory is read-only + open_inventory.readonly = true; + + // This update makes sure we have the items in the inventory by the time the + // client wants to update these + app.update(); + helper.clear_received(); + + let inv_state = app.world_mut().get::(client).unwrap(); + let state_id = inv_state.state_id(); + let window_id = inv_state.window_id(); + + // The player's inventory is not readonly, so the player should still be + // able to move items from the hotbar to other parts of the inventory even + // if the other inventory is still open. + helper.send(&ClickSlotC2s { + window_id, + state_id: VarInt(state_id.0), + slot_idx: 27, + button: 0, // hotbar slot starting at 0 + mode: ClickMode::Hotbar, + slot_changes: vec![ + SlotChange { + idx: 27, + stack: ItemStack::new(ItemKind::Diamond, 10, None), + }, + SlotChange { + idx: 54, + stack: ItemStack::EMPTY, + }, + ] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + // Make assertions + let sent_packets = helper.collect_received(); + sent_packets.assert_count::(2); + + let player_inventory = app + .world_mut() + .get::(client) + .expect("could not find client"); + + // Items swapped successfully, as player item is not read-only + assert_eq!(player_inventory.slot(36), &ItemStack::EMPTY); + assert_eq!( + player_inventory.slot(9), + &ItemStack::new(ItemKind::Diamond, 10, None) + ); +} + +#[test] +fn test_prevent_shift_item_click_container_readonly_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + // player inventory is not read-only + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not find inventory for client"); + + player_inventory.set_slot(9, ItemStack::new(ItemKind::Diamond, 64, None)); + + let open_inv_ent = set_up_open_inventory(&mut app, client); + + let mut open_inventory = app + .world_mut() + .get_mut::(open_inv_ent) + .expect("could not find inventory for client"); + + // Open inventory is read-only + open_inventory.readonly = true; + + // This update makes sure we have the items in the inventory by the time the + // client wants to update these + app.update(); + helper.clear_received(); + + let inv_state = app.world_mut().get::(client).unwrap(); + let state_id = inv_state.state_id(); + let window_id = inv_state.window_id(); + + // The player tries to Shift-click transfer the stack of diamonds into + // the open container + helper.send(&ClickSlotC2s { + window_id, + state_id: VarInt(state_id.0), + slot_idx: 27, + button: 0, // hotbar slot starting at 0 + mode: ClickMode::ShiftClick, + slot_changes: vec![ + // target + SlotChange { + idx: 0, + stack: ItemStack::new(ItemKind::Diamond, 64, None), + }, + // source + SlotChange { + idx: 27, + stack: ItemStack::EMPTY, + }, + ] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + // 1 resync per inventory + sent_packets.assert_count::(2); + + let player_inventory = app + .world_mut() + .get::(client) + .expect("could not find client"); + + assert_eq!( + player_inventory.slot(9), + &ItemStack::new(ItemKind::Diamond, 64, None) + ); + + let open_inventory = app + .world_mut() + .get::(open_inv_ent) + .expect("could not find inventory"); + + assert_eq!(open_inventory.slot(0), &ItemStack::EMPTY); +} #[test] fn test_should_sync_entire_open_inventory() { From 6679f35970cb5cd7a203b761bd6a7a05866b4307 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:53:35 +0200 Subject: [PATCH 7/7] fix ci --- crates/valence_inventory/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index c5c8657de..a5a8f80ee 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -1066,7 +1066,7 @@ fn handle_click_slot( continue; } - + let mut new_cursor = pkt.carried_item.clone(); for slot in pkt.slot_changes.iter() { @@ -1101,7 +1101,7 @@ fn handle_click_slot( } } - cursor_item.set_if_neq(CursorItem(new_cursor)); + cursor_item.set_if_neq(CursorItem(new_cursor.clone())); inv_state.client_updated_cursor_item = Some(new_cursor); if target_inventory.readonly || client_inv.readonly { @@ -1161,9 +1161,9 @@ fn handle_click_slot( } } - cursor_item.set_if_neq(CursorItem(new_cursor)); + cursor_item.set_if_neq(CursorItem(new_cursor.clone())); inv_state.client_updated_cursor_item = Some(new_cursor); - + if client_inv.readonly { // resync the client inventory client.write_packet(&InventoryS2c {