Skip to content

Commit

Permalink
Readonly inventory (#655)
Browse files Browse the repository at this point in the history
# Objective
- ability to make inventories read only (player should be able to click
things in the inventory and still emit a click event, this can be useful
for creating inventory menus.
- closes  #427
- related #307 #355 
# Solution

- adds a public ``readonly: bool`` field to the ``Inventory`` component,
that will make any interactions with this item impossible (includes:
moving, shift moving, hotbar moving, dropping) if a player inventory is
readonly, then the player will also not be able to drop items (even when
not in the inventory), so the drop event will not be emitted (this could
be changed if requested)
- when implementing this i discovered a bug where a player is not able
to put a item from a open inventory in the offhand (by hitting F) that
will cause a desync. On the client the item will be in the offhand, but
if you try to interact with that it dissapears. (unrelated to this PR
and will not be fixed in this PR)

---------

Co-authored-by: Carson McManus <[email protected]>
  • Loading branch information
maxomatic458 and dyc3 authored Oct 11, 2024
1 parent 35b8e96 commit 9a0c82f
Show file tree
Hide file tree
Showing 2 changed files with 929 additions and 25 deletions.
160 changes: 150 additions & 10 deletions crates/valence_inventory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ pub struct Inventory {
/// Contains a set bit for each modified slot in `slots`.
#[doc(hidden)]
pub changed: u64,
/// 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)
pub readonly: bool,
}

impl Inventory {
Expand All @@ -86,6 +91,7 @@ impl Inventory {
kind,
slots: vec![ItemStack::EMPTY; kind.slot_count()].into(),
changed: 0,
readonly: false,
}
}

Expand Down Expand Up @@ -915,6 +921,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() {
Expand All @@ -938,6 +955,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);

Expand Down Expand Up @@ -966,6 +995,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() {
Expand Down Expand Up @@ -1002,7 +1042,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.
Expand All @@ -1026,22 +1067,60 @@ fn handle_click_slot(
continue;
}

cursor_item.set_if_neq(CursorItem(pkt.carried_item.clone()));
inv_state.client_updated_cursor_item = Some(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);
client_inv.set_slot(slot_id, slot.stack.clone());
inv_state.slots_changed |= 1 << slot_id;
}
}

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 {
// 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.

Expand All @@ -1062,11 +1141,14 @@ fn handle_click_slot(
continue;
}

cursor_item.set_if_neq(CursorItem(pkt.carried_item.clone()));
inv_state.client_updated_cursor_item = Some(pkt.carried_item.clone());
let mut new_cursor = pkt.carried_item.clone();

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 {
Expand All @@ -1078,6 +1160,19 @@ fn handle_click_slot(
);
}
}

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 {
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 {
Expand All @@ -1096,14 +1191,32 @@ fn handle_click_slot(

fn handle_player_actions(
mut packets: EventReader<PacketEvent>,
mut clients: Query<(&mut Inventory, &mut ClientInventoryState, &HeldItem)>,
mut clients: Query<(
&mut Inventory,
&mut ClientInventoryState,
&HeldItem,
&mut Client,
)>,
mut drop_item_stack_events: EventWriter<DropItemStackEvent>,
) {
for packet in packets.read() {
if let Some(pkt) = packet.decode::<PlayerActionC2s>() {
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),
});
continue;
}

let stack = inv.replace_slot(held.slot(), ItemStack::EMPTY);

if !stack.is_empty() {
Expand All @@ -1118,7 +1231,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() {
Expand All @@ -1142,7 +1268,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);
}
}
Expand Down
Loading

0 comments on commit 9a0c82f

Please sign in to comment.