mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
Rough click slot packet validation (#293)
## Description This adds some validation for incoming inventory packets that makes it so that you can't just spawn items by sending malicious packets. It adds type 1 and type 2 validations as outlined in #292. This also adds some new helpers, `InventoryWindow` and `InventoryWindowMut`. fixes #292 <details> <summary>Playground</summary> ```rust use valence::client::{default_event_handler, despawn_disconnected_clients}; use valence::prelude::event::PlayerInteractBlock; use valence::prelude::*; #[allow(unused_imports)] use crate::extras::*; const SPAWN_Y: i32 = 64; const CHEST_POS: [i32; 3] = [0, SPAWN_Y + 1, 3]; pub fn build_app(app: &mut App) { app.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline)) .add_startup_system(setup) .add_system(default_event_handler.in_schedule(EventLoopSchedule)) .add_system(init_clients) .add_system(despawn_disconnected_clients) .add_systems((toggle_gamemode_on_sneak, open_chest).in_schedule(EventLoopSchedule)); } fn setup(mut commands: Commands, server: Res<Server>) { let mut instance = server.new_instance(DimensionId::default()); for z in -5..5 { for x in -5..5 { instance.insert_chunk([x, z], Chunk::default()); } } for z in -25..25 { for x in -25..25 { instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } instance.set_block(CHEST_POS, BlockState::CHEST); commands.spawn(instance); let mut inventory = Inventory::new(InventoryKind::Generic9x3); inventory.set_slot(0, ItemStack::new(ItemKind::Apple, 100, None)); inventory.set_slot(1, ItemStack::new(ItemKind::Diamond, 40, None)); inventory.set_slot(2, ItemStack::new(ItemKind::Diamond, 30, None)); commands.spawn(inventory); } fn init_clients( mut clients: Query<(&mut Position, &mut Location, &mut Inventory), Added<Client>>, instances: Query<Entity, With<Instance>>, ) { for (mut pos, mut loc, mut inv) in &mut clients { pos.0 = [0.5, SPAWN_Y as f64 + 1.0, 0.5].into(); loc.0 = instances.single(); inv.set_slot(24, ItemStack::new(ItemKind::Apple, 100, None)); inv.set_slot(25, ItemStack::new(ItemKind::Apple, 10, None)); } } // Add new systems here! fn open_chest( mut commands: Commands, inventories: Query<Entity, (With<Inventory>, Without<Client>)>, mut events: EventReader<PlayerInteractBlock>, ) { let Ok(inventory) = inventories.get_single() else { return; }; for event in events.iter() { if event.position != CHEST_POS.into() { continue; } let open_inventory = OpenInventory::new(inventory); commands.entity(event.client).insert(open_inventory); } } ``` </details> ## Test Plan Steps: 1. `cargo test` --------- Co-authored-by: Ryan Johnson <ryanj00a@gmail.com>
This commit is contained in:
parent
8d93ddee24
commit
a57800959f
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
|
@ -8,7 +9,7 @@ use bevy_ecs::schedule::ScheduleLabel;
|
||||||
use bevy_ecs::system::{SystemParam, SystemState};
|
use bevy_ecs::system::{SystemParam, SystemState};
|
||||||
use glam::{DVec3, Vec3};
|
use glam::{DVec3, Vec3};
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
use tracing::warn;
|
use tracing::{debug, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use valence_protocol::block_pos::BlockPos;
|
use valence_protocol::block_pos::BlockPos;
|
||||||
use valence_protocol::ident::Ident;
|
use valence_protocol::ident::Ident;
|
||||||
|
@ -27,15 +28,19 @@ use valence_protocol::packet::c2s::play::update_structure_block::{
|
||||||
use valence_protocol::packet::c2s::play::{
|
use valence_protocol::packet::c2s::play::{
|
||||||
AdvancementTabC2s, ClientStatusC2s, ResourcePackStatusC2s, UpdatePlayerAbilitiesC2s,
|
AdvancementTabC2s, ClientStatusC2s, ResourcePackStatusC2s, UpdatePlayerAbilitiesC2s,
|
||||||
};
|
};
|
||||||
|
use valence_protocol::packet::s2c::play::InventoryS2c;
|
||||||
use valence_protocol::packet::C2sPlayPacket;
|
use valence_protocol::packet::C2sPlayPacket;
|
||||||
use valence_protocol::types::{Difficulty, Direction, Hand};
|
use valence_protocol::types::{Difficulty, Direction, Hand};
|
||||||
|
use valence_protocol::var_int::VarInt;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
CursorItem, KeepaliveState, PlayerActionSequence, PlayerInventoryState, TeleportState,
|
CursorItem, KeepaliveState, PlayerActionSequence, PlayerInventoryState, TeleportState,
|
||||||
};
|
};
|
||||||
use crate::client::Client;
|
use crate::client::Client;
|
||||||
use crate::component::{Look, OnGround, Ping, Position};
|
use crate::component::{Look, OnGround, Ping, Position};
|
||||||
use crate::inventory::Inventory;
|
use crate::inventory::{Inventory, InventorySettings};
|
||||||
|
use crate::packet::WritePacket;
|
||||||
|
use crate::prelude::OpenInventory;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct QueryBlockNbt {
|
pub struct QueryBlockNbt {
|
||||||
|
@ -671,6 +676,7 @@ pub(crate) struct EventLoopQuery {
|
||||||
keepalive_state: &'static mut KeepaliveState,
|
keepalive_state: &'static mut KeepaliveState,
|
||||||
cursor_item: &'static mut CursorItem,
|
cursor_item: &'static mut CursorItem,
|
||||||
inventory: &'static mut Inventory,
|
inventory: &'static mut Inventory,
|
||||||
|
open_inventory: Option<&'static mut OpenInventory>,
|
||||||
position: &'static mut Position,
|
position: &'static mut Position,
|
||||||
look: &'static mut Look,
|
look: &'static mut Look,
|
||||||
on_ground: &'static mut OnGround,
|
on_ground: &'static mut OnGround,
|
||||||
|
@ -682,10 +688,17 @@ pub(crate) struct EventLoopQuery {
|
||||||
/// An exclusive system for running the event loop schedule.
|
/// An exclusive system for running the event loop schedule.
|
||||||
fn run_event_loop(
|
fn run_event_loop(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
state: &mut SystemState<(Query<EventLoopQuery>, ClientEvents, Commands)>,
|
state: &mut SystemState<(
|
||||||
|
Query<EventLoopQuery>,
|
||||||
|
ClientEvents,
|
||||||
|
Commands,
|
||||||
|
Query<&Inventory, Without<Client>>,
|
||||||
|
Res<InventorySettings>,
|
||||||
|
)>,
|
||||||
mut clients_to_check: Local<Vec<Entity>>,
|
mut clients_to_check: Local<Vec<Entity>>,
|
||||||
) {
|
) {
|
||||||
let (mut clients, mut events, mut commands) = state.get_mut(world);
|
let (mut clients, mut events, mut commands, mut inventories, inventory_settings) =
|
||||||
|
state.get_mut(world);
|
||||||
|
|
||||||
update_all_event_buffers(&mut events);
|
update_all_event_buffers(&mut events);
|
||||||
|
|
||||||
|
@ -703,7 +716,7 @@ fn run_event_loop(
|
||||||
|
|
||||||
q.client.dec.queue_bytes(bytes);
|
q.client.dec.queue_bytes(bytes);
|
||||||
|
|
||||||
match handle_one_packet(&mut q, &mut events) {
|
match handle_one_packet(&mut q, &mut events, &mut inventories, &inventory_settings) {
|
||||||
Ok(had_packet) => {
|
Ok(had_packet) => {
|
||||||
if had_packet {
|
if had_packet {
|
||||||
// We decoded one packet, but there might be more.
|
// We decoded one packet, but there might be more.
|
||||||
|
@ -723,7 +736,8 @@ fn run_event_loop(
|
||||||
while !clients_to_check.is_empty() {
|
while !clients_to_check.is_empty() {
|
||||||
world.run_schedule(EventLoopSchedule);
|
world.run_schedule(EventLoopSchedule);
|
||||||
|
|
||||||
let (mut clients, mut events, mut commands) = state.get_mut(world);
|
let (mut clients, mut events, mut commands, mut inventories, inventory_settings) =
|
||||||
|
state.get_mut(world);
|
||||||
|
|
||||||
clients_to_check.retain(|&entity| {
|
clients_to_check.retain(|&entity| {
|
||||||
let Ok(mut q) = clients.get_mut(entity) else {
|
let Ok(mut q) = clients.get_mut(entity) else {
|
||||||
|
@ -731,7 +745,7 @@ fn run_event_loop(
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
match handle_one_packet(&mut q, &mut events) {
|
match handle_one_packet(&mut q, &mut events, &mut inventories, &inventory_settings) {
|
||||||
Ok(had_packet) => had_packet,
|
Ok(had_packet) => had_packet,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("failed to dispatch events for client {:?}: {e:?}", q.entity);
|
warn!("failed to dispatch events for client {:?}: {e:?}", q.entity);
|
||||||
|
@ -748,6 +762,8 @@ fn run_event_loop(
|
||||||
fn handle_one_packet(
|
fn handle_one_packet(
|
||||||
q: &mut EventLoopQueryItem,
|
q: &mut EventLoopQueryItem,
|
||||||
events: &mut ClientEvents,
|
events: &mut ClientEvents,
|
||||||
|
inventories: &mut Query<&Inventory, Without<Client>>,
|
||||||
|
inventory_settings: &Res<InventorySettings>,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
let Some(pkt) = q.client.dec.try_next_packet::<C2sPlayPacket>()? else {
|
let Some(pkt) = q.client.dec.try_next_packet::<C2sPlayPacket>()? else {
|
||||||
// No packets to decode.
|
// No packets to decode.
|
||||||
|
@ -847,7 +863,57 @@ fn handle_one_packet(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
C2sPlayPacket::ClickSlotC2s(p) => {
|
C2sPlayPacket::ClickSlotC2s(p) => {
|
||||||
if p.slot_idx < 0 {
|
let open_inv = q
|
||||||
|
.open_inventory
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|open| inventories.get_mut(open.entity).ok());
|
||||||
|
if let Err(msg) =
|
||||||
|
crate::inventory::validate_click_slot_impossible(&p, &q.inventory, open_inv)
|
||||||
|
{
|
||||||
|
debug!(
|
||||||
|
"client {:#?} invalid click slot packet: \"{}\" {:#?}",
|
||||||
|
q.entity, msg, p
|
||||||
|
);
|
||||||
|
let inventory = open_inv.unwrap_or(&q.inventory);
|
||||||
|
q.client.write_packet(&InventoryS2c {
|
||||||
|
window_id: if open_inv.is_some() {
|
||||||
|
q.player_inventory_state.window_id
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
state_id: VarInt(q.player_inventory_state.state_id.0),
|
||||||
|
slots: Cow::Borrowed(inventory.slot_slice()),
|
||||||
|
carried_item: Cow::Borrowed(&q.cursor_item.0),
|
||||||
|
});
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
if inventory_settings.enable_item_dupe_check {
|
||||||
|
if let Err(msg) = crate::inventory::validate_click_slot_item_duplication(
|
||||||
|
&p,
|
||||||
|
&q.inventory,
|
||||||
|
open_inv,
|
||||||
|
&q.cursor_item,
|
||||||
|
) {
|
||||||
|
debug!(
|
||||||
|
"client {:#?} click slot packet tried to incorrectly modify items: \"{}\" \
|
||||||
|
{:#?}",
|
||||||
|
q.entity, msg, p
|
||||||
|
);
|
||||||
|
let inventory = open_inv.unwrap_or(&q.inventory);
|
||||||
|
q.client.write_packet(&InventoryS2c {
|
||||||
|
window_id: if open_inv.is_some() {
|
||||||
|
q.player_inventory_state.window_id
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
state_id: VarInt(q.player_inventory_state.state_id.0),
|
||||||
|
slots: Cow::Borrowed(inventory.slot_slice()),
|
||||||
|
carried_item: Cow::Borrowed(&q.cursor_item.0),
|
||||||
|
});
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.slot_idx < 0 && p.mode == ClickMode::Click {
|
||||||
if let Some(stack) = q.cursor_item.0.take() {
|
if let Some(stack) = q.cursor_item.0.take() {
|
||||||
events.2.drop_item_stack.send(DropItemStack {
|
events.2.drop_item_stack.send(DropItemStack {
|
||||||
client: entity,
|
client: entity,
|
||||||
|
|
|
@ -49,6 +49,14 @@ use crate::component::GameMode;
|
||||||
use crate::packet::WritePacket;
|
use crate::packet::WritePacket;
|
||||||
use crate::prelude::FlushPacketsSet;
|
use crate::prelude::FlushPacketsSet;
|
||||||
|
|
||||||
|
mod validate;
|
||||||
|
|
||||||
|
pub(crate) use validate::*;
|
||||||
|
|
||||||
|
/// The number of slots in the "main" part of the player inventory. 3 rows of 9,
|
||||||
|
/// plus the hotbar.
|
||||||
|
pub const PLAYER_INVENTORY_MAIN_SLOTS_COUNT: u16 = 36;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Component)]
|
#[derive(Debug, Clone, Component)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
title: Text,
|
title: Text,
|
||||||
|
@ -237,7 +245,7 @@ impl Inventory {
|
||||||
std::mem::replace(&mut self.title, title.into())
|
std::mem::replace(&mut self.title, title.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn slot_slice(&self) -> &[Option<ItemStack>] {
|
pub(crate) fn slot_slice(&self) -> &[Option<ItemStack>] {
|
||||||
self.slots.as_ref()
|
self.slots.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,6 +311,141 @@ impl OpenInventory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A helper to represent the inventory window that the player is currently
|
||||||
|
/// viewing. Handles dispatching reads to the correct inventory.
|
||||||
|
///
|
||||||
|
/// This is a read-only version of [`InventoryWindowMut`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
/// player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// let target_inventory = Inventory::new(InventoryKind::Generic9x3);
|
||||||
|
/// let window = InventoryWindow::new(&player_inventory, Some(&target_inventory));
|
||||||
|
/// assert_eq!(
|
||||||
|
/// window.slot(54),
|
||||||
|
/// Some(&ItemStack::new(ItemKind::Diamond, 1, None))
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub struct InventoryWindow<'a> {
|
||||||
|
player_inventory: &'a Inventory,
|
||||||
|
open_inventory: Option<&'a Inventory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> InventoryWindow<'a> {
|
||||||
|
pub fn new(player_inventory: &'a Inventory, open_inventory: Option<&'a Inventory>) -> Self {
|
||||||
|
Self {
|
||||||
|
player_inventory,
|
||||||
|
open_inventory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn slot(&self, idx: u16) -> Option<&ItemStack> {
|
||||||
|
if let Some(open_inv) = self.open_inventory.as_ref() {
|
||||||
|
if idx < open_inv.slot_count() {
|
||||||
|
return open_inv.slot(idx);
|
||||||
|
} else {
|
||||||
|
return self
|
||||||
|
.player_inventory
|
||||||
|
.slot(convert_to_player_slot_id(open_inv.kind(), idx));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return self.player_inventory.slot(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn slot_count(&self) -> u16 {
|
||||||
|
match self.open_inventory.as_ref() {
|
||||||
|
Some(inv) => inv.slot_count() + PLAYER_INVENTORY_MAIN_SLOTS_COUNT,
|
||||||
|
None => self.player_inventory.slot_count(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper to represent the inventory window that the player is currently
|
||||||
|
/// viewing. Handles dispatching reads/writes to the correct inventory.
|
||||||
|
///
|
||||||
|
/// This is a writable version of [`InventoryWindow`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
/// let mut target_inventory = Inventory::new(InventoryKind::Generic9x3);
|
||||||
|
/// let mut window = InventoryWindowMut::new(&mut player_inventory, Some(&mut target_inventory));
|
||||||
|
/// window.set_slot(54, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// assert_eq!(
|
||||||
|
/// player_inventory.slot(36),
|
||||||
|
/// Some(&ItemStack::new(ItemKind::Diamond, 1, None))
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub struct InventoryWindowMut<'a> {
|
||||||
|
player_inventory: &'a mut Inventory,
|
||||||
|
open_inventory: Option<&'a mut Inventory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> InventoryWindowMut<'a> {
|
||||||
|
pub fn new(
|
||||||
|
player_inventory: &'a mut Inventory,
|
||||||
|
open_inventory: Option<&'a mut Inventory>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
player_inventory,
|
||||||
|
open_inventory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn slot(&self, idx: u16) -> Option<&ItemStack> {
|
||||||
|
if let Some(open_inv) = self.open_inventory.as_ref() {
|
||||||
|
if idx < open_inv.slot_count() {
|
||||||
|
return open_inv.slot(idx);
|
||||||
|
} else {
|
||||||
|
return self
|
||||||
|
.player_inventory
|
||||||
|
.slot(convert_to_player_slot_id(open_inv.kind(), idx));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return self.player_inventory.slot(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
#[must_use]
|
||||||
|
pub fn replace_slot(
|
||||||
|
&mut self,
|
||||||
|
idx: u16,
|
||||||
|
item: impl Into<Option<ItemStack>>,
|
||||||
|
) -> Option<ItemStack> {
|
||||||
|
assert!(idx < self.slot_count(), "slot index of {idx} out of bounds");
|
||||||
|
|
||||||
|
if let Some(open_inv) = self.open_inventory.as_mut() {
|
||||||
|
if idx < open_inv.slot_count() {
|
||||||
|
open_inv.replace_slot(idx, item)
|
||||||
|
} else {
|
||||||
|
self.player_inventory
|
||||||
|
.replace_slot(convert_to_player_slot_id(open_inv.kind(), idx), item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.player_inventory.replace_slot(idx, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
#[inline]
|
||||||
|
pub fn set_slot(&mut self, idx: u16, item: impl Into<Option<ItemStack>>) {
|
||||||
|
let _ = self.replace_slot(idx, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slot_count(&self) -> u16 {
|
||||||
|
match self.open_inventory.as_ref() {
|
||||||
|
Some(inv) => inv.slot_count() + PLAYER_INVENTORY_MAIN_SLOTS_COUNT,
|
||||||
|
None => self.player_inventory.slot_count(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct InventoryPlugin;
|
pub(crate) struct InventoryPlugin;
|
||||||
|
|
||||||
impl Plugin for InventoryPlugin {
|
impl Plugin for InventoryPlugin {
|
||||||
|
@ -574,7 +717,7 @@ fn handle_click_container(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor_item.0 = event.carried_item.clone();
|
cursor_item.set_if_neq(CursorItem(event.carried_item.clone()));
|
||||||
|
|
||||||
for slot in event.slot_changes.clone() {
|
for slot in event.slot_changes.clone() {
|
||||||
if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
|
if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
|
||||||
|
@ -608,8 +751,6 @@ fn handle_click_container(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: do more validation on the click
|
|
||||||
|
|
||||||
cursor_item.set_if_neq(CursorItem(event.carried_item.clone()));
|
cursor_item.set_if_neq(CursorItem(event.carried_item.clone()));
|
||||||
inv_state.client_updated_cursor_item = true;
|
inv_state.client_updated_cursor_item = true;
|
||||||
|
|
||||||
|
@ -692,7 +833,7 @@ fn convert_to_player_slot_id(target_kind: InventoryKind, slot_id: u16) -> u16 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_hotbar_slot_id(slot_id: u16) -> u16 {
|
fn convert_hotbar_slot_id(slot_id: u16) -> u16 {
|
||||||
slot_id + 36
|
slot_id + PLAYER_INVENTORY_MAIN_SLOTS_COUNT
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
@ -823,10 +964,25 @@ impl From<WindowType> for InventoryKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Resource)]
|
||||||
|
pub struct InventorySettings {
|
||||||
|
pub enable_item_dupe_check: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InventorySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enable_item_dupe_check: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use bevy_app::App;
|
use bevy_app::App;
|
||||||
use valence_protocol::item::ItemKind;
|
use valence_protocol::item::ItemKind;
|
||||||
|
use valence_protocol::packet::c2s::play::click_slot::{ClickMode, Slot};
|
||||||
|
use valence_protocol::packet::c2s::play::ClickSlotC2s;
|
||||||
use valence_protocol::packet::S2cPlayPacket;
|
use valence_protocol::packet::S2cPlayPacket;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -1098,6 +1254,11 @@ mod test {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
|
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
|
||||||
let inventory_ent = set_up_open_inventory(&mut app, client_ent);
|
let inventory_ent = set_up_open_inventory(&mut app, client_ent);
|
||||||
|
let mut inventory = app
|
||||||
|
.world
|
||||||
|
.get_mut::<Inventory>(inventory_ent)
|
||||||
|
.expect("could not find inventory for client");
|
||||||
|
inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
@ -1342,7 +1503,7 @@ mod test {
|
||||||
|
|
||||||
mod dropping_items {
|
mod dropping_items {
|
||||||
use valence_protocol::block_pos::BlockPos;
|
use valence_protocol::block_pos::BlockPos;
|
||||||
use valence_protocol::packet::c2s::play::click_slot::ClickMode;
|
use valence_protocol::packet::c2s::play::click_slot::{ClickMode, Slot};
|
||||||
use valence_protocol::packet::c2s::play::player_action::Action;
|
use valence_protocol::packet::c2s::play::player_action::Action;
|
||||||
use valence_protocol::types::Direction;
|
use valence_protocol::types::Direction;
|
||||||
|
|
||||||
|
@ -1567,7 +1728,10 @@ mod test {
|
||||||
button: 0,
|
button: 0,
|
||||||
mode: ClickMode::DropKey,
|
mode: ClickMode::DropKey,
|
||||||
state_id: VarInt(state_id),
|
state_id: VarInt(state_id),
|
||||||
slots: vec![],
|
slots: vec![Slot {
|
||||||
|
idx: 40,
|
||||||
|
item: Some(ItemStack::new(ItemKind::IronIngot, 31, None)),
|
||||||
|
}],
|
||||||
carried_item: None,
|
carried_item: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1615,7 +1779,10 @@ mod test {
|
||||||
button: 1, // pressing control
|
button: 1, // pressing control
|
||||||
mode: ClickMode::DropKey,
|
mode: ClickMode::DropKey,
|
||||||
state_id: VarInt(state_id),
|
state_id: VarInt(state_id),
|
||||||
slots: vec![],
|
slots: vec![Slot {
|
||||||
|
idx: 40,
|
||||||
|
item: None,
|
||||||
|
}],
|
||||||
carried_item: None,
|
carried_item: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1638,4 +1805,69 @@ mod test {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dragging_items() -> anyhow::Result<()> {
|
||||||
|
let mut app = App::new();
|
||||||
|
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
|
||||||
|
app.world.get_mut::<CursorItem>(client_ent).unwrap().0 =
|
||||||
|
Some(ItemStack::new(ItemKind::Diamond, 64, None));
|
||||||
|
|
||||||
|
// Process a tick to get past the "on join" logic.
|
||||||
|
app.update();
|
||||||
|
client_helper.clear_sent();
|
||||||
|
|
||||||
|
let inv_state = app.world.get::<PlayerInventoryState>(client_ent).unwrap();
|
||||||
|
let window_id = inv_state.window_id;
|
||||||
|
let state_id = inv_state.state_id.0;
|
||||||
|
|
||||||
|
let drag_packet = ClickSlotC2s {
|
||||||
|
window_id,
|
||||||
|
state_id: VarInt(state_id),
|
||||||
|
slot_idx: -999,
|
||||||
|
button: 2,
|
||||||
|
mode: ClickMode::Drag,
|
||||||
|
slots: vec![
|
||||||
|
Slot {
|
||||||
|
idx: 9,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 10,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 11,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::Diamond, 1, None)),
|
||||||
|
};
|
||||||
|
client_helper.send(&drag_packet);
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
let sent_packets = client_helper.collect_sent()?;
|
||||||
|
assert_eq!(sent_packets.len(), 0);
|
||||||
|
|
||||||
|
let cursor_item = app
|
||||||
|
.world
|
||||||
|
.get::<CursorItem>(client_ent)
|
||||||
|
.expect("could not find client");
|
||||||
|
assert_eq!(
|
||||||
|
cursor_item.0,
|
||||||
|
Some(ItemStack::new(ItemKind::Diamond, 1, None))
|
||||||
|
);
|
||||||
|
let inventory = app
|
||||||
|
.world
|
||||||
|
.get::<Inventory>(client_ent)
|
||||||
|
.expect("could not find inventory");
|
||||||
|
for i in 9..12 {
|
||||||
|
assert_eq!(
|
||||||
|
inventory.slot(i),
|
||||||
|
Some(&ItemStack::new(ItemKind::Diamond, 21, None))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
821
crates/valence/src/inventory/validate.rs
Normal file
821
crates/valence/src/inventory/validate.rs
Normal file
|
@ -0,0 +1,821 @@
|
||||||
|
use anyhow::{bail, ensure};
|
||||||
|
use valence_protocol::packet::c2s::play::click_slot::ClickMode;
|
||||||
|
use valence_protocol::packet::c2s::play::ClickSlotC2s;
|
||||||
|
|
||||||
|
use super::{Inventory, InventoryWindow, PLAYER_INVENTORY_MAIN_SLOTS_COUNT};
|
||||||
|
use crate::prelude::CursorItem;
|
||||||
|
|
||||||
|
/// Validates a click slot packet enforcing that all fields are valid.
|
||||||
|
pub(crate) fn validate_click_slot_impossible(
|
||||||
|
packet: &ClickSlotC2s,
|
||||||
|
player_inventory: &Inventory,
|
||||||
|
open_inventory: Option<&Inventory>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
ensure!(
|
||||||
|
(packet.window_id == 0) == open_inventory.is_none(),
|
||||||
|
"window id and open inventory mismatch: window_id: {} open_inventory: {}",
|
||||||
|
packet.window_id,
|
||||||
|
open_inventory.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
|
let max_slot = match open_inventory {
|
||||||
|
Some(inv) => inv.slot_count() + PLAYER_INVENTORY_MAIN_SLOTS_COUNT,
|
||||||
|
None => player_inventory.slot_count(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// check all slot ids and item counts are valid
|
||||||
|
ensure!(
|
||||||
|
packet.slots.iter().all(|s| {
|
||||||
|
if !(0..=max_slot).contains(&(s.idx as u16)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(slot) = s.item.as_ref() {
|
||||||
|
let max_stack_size = slot
|
||||||
|
.item
|
||||||
|
.max_stack()
|
||||||
|
.max(slot.count())
|
||||||
|
.min(valence_protocol::item::STACK_MAX);
|
||||||
|
if !(1..=max_stack_size).contains(&slot.count()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}),
|
||||||
|
"invalid slot ids or item counts"
|
||||||
|
);
|
||||||
|
|
||||||
|
// check carried item count is valid
|
||||||
|
if let Some(carried_item) = &packet.carried_item {
|
||||||
|
let max_stack_size = carried_item
|
||||||
|
.item
|
||||||
|
.max_stack()
|
||||||
|
.max(carried_item.count())
|
||||||
|
.min(valence_protocol::item::STACK_MAX);
|
||||||
|
ensure!(
|
||||||
|
(1..=max_stack_size).contains(&carried_item.count()),
|
||||||
|
"invalid carried item count"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match packet.mode {
|
||||||
|
ClickMode::Click => {
|
||||||
|
ensure!((0..=1).contains(&packet.button), "invalid button");
|
||||||
|
ensure!(
|
||||||
|
(0..=max_slot).contains(&(packet.slot_idx as u16)) || packet.slot_idx == -999,
|
||||||
|
"invalid slot index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ClickMode::ShiftClick => {
|
||||||
|
ensure!((0..=1).contains(&packet.button), "invalid button");
|
||||||
|
ensure!(
|
||||||
|
packet.carried_item.is_none(),
|
||||||
|
"carried item must be empty for a hotbar swap"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
(0..=max_slot).contains(&(packet.slot_idx as u16)),
|
||||||
|
"invalid slot index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ClickMode::Hotbar => {
|
||||||
|
ensure!(matches!(packet.button, 0..=8 | 40), "invalid button");
|
||||||
|
ensure!(
|
||||||
|
packet.carried_item.is_none(),
|
||||||
|
"carried item must be empty for a hotbar swap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ClickMode::CreativeMiddleClick => {
|
||||||
|
ensure!(packet.button == 2, "invalid button");
|
||||||
|
ensure!(
|
||||||
|
(0..=max_slot).contains(&(packet.slot_idx as u16)),
|
||||||
|
"invalid slot index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ClickMode::DropKey => {
|
||||||
|
ensure!((0..=1).contains(&packet.button), "invalid button");
|
||||||
|
ensure!(
|
||||||
|
packet.carried_item.is_none(),
|
||||||
|
"carried item must be empty for an item drop"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
(0..=max_slot).contains(&(packet.slot_idx as u16)),
|
||||||
|
"invalid slot index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ClickMode::Drag => {
|
||||||
|
ensure!(
|
||||||
|
matches!(packet.button, 0..=2 | 4..=6 | 8..=10),
|
||||||
|
"invalid button"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
(0..=max_slot).contains(&(packet.slot_idx as u16)) || packet.slot_idx == -999,
|
||||||
|
"invalid slot index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ClickMode::DoubleClick => ensure!(packet.button == 0, "invalid button"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a click slot packet, enforcing that items can't be duplicated, eg.
|
||||||
|
/// conservation of mass.
|
||||||
|
///
|
||||||
|
/// Relies on assertions made by [`validate_click_slot_impossible`].
|
||||||
|
pub(crate) fn validate_click_slot_item_duplication(
|
||||||
|
packet: &ClickSlotC2s,
|
||||||
|
player_inventory: &Inventory,
|
||||||
|
open_inventory: Option<&Inventory>,
|
||||||
|
cursor_item: &CursorItem,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let window = InventoryWindow {
|
||||||
|
player_inventory,
|
||||||
|
open_inventory,
|
||||||
|
};
|
||||||
|
|
||||||
|
match packet.mode {
|
||||||
|
ClickMode::Click => {
|
||||||
|
if packet.slot_idx == -999 {
|
||||||
|
// Clicked outside the window, so the client is dropping an item
|
||||||
|
ensure!(packet.slots.is_empty(), "slot modifications must be empty");
|
||||||
|
|
||||||
|
// Clicked outside the window
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
let expected_delta = match packet.button {
|
||||||
|
1 => -1,
|
||||||
|
0 => -cursor_item
|
||||||
|
.0
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.count() as i32)
|
||||||
|
.unwrap_or(0),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
ensure!(
|
||||||
|
count_deltas == expected_delta,
|
||||||
|
"invalid item delta: expected {}, got {}",
|
||||||
|
expected_delta,
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ensure!(
|
||||||
|
packet.slots.len() == 1,
|
||||||
|
"click must modify one slot, got {}",
|
||||||
|
packet.slots.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let old_slot = window.slot(packet.slots[0].idx as u16);
|
||||||
|
// TODO: make sure NBT is the same
|
||||||
|
// Sometimes, the client will add nbt data to an item if it's missing, like
|
||||||
|
// "Damage" to a sword
|
||||||
|
let should_swap = packet.button == 0
|
||||||
|
&& match (old_slot, cursor_item.0.as_ref()) {
|
||||||
|
(Some(old_slot), Some(cursor_item)) => old_slot.item != cursor_item.item,
|
||||||
|
(Some(_), None) => true,
|
||||||
|
(None, Some(cursor_item)) => {
|
||||||
|
cursor_item.count() <= cursor_item.item.max_stack()
|
||||||
|
}
|
||||||
|
(None, None) => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_swap {
|
||||||
|
// assert that a swap occurs
|
||||||
|
ensure!(
|
||||||
|
old_slot == packet.carried_item.as_ref()
|
||||||
|
&& cursor_item.0 == packet.slots[0].item,
|
||||||
|
"swapped items must match"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// assert that a merge occurs
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
ensure!(
|
||||||
|
count_deltas == 0,
|
||||||
|
"invalid item delta for stack merge: {}",
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClickMode::ShiftClick => {
|
||||||
|
ensure!(
|
||||||
|
(2..=3).contains(&packet.slots.len()),
|
||||||
|
"shift click must modify 2 or 3 slots, got {}",
|
||||||
|
packet.slots.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
ensure!(
|
||||||
|
count_deltas == 0,
|
||||||
|
"invalid item delta: expected 0, got {}",
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some(item_kind) = packet
|
||||||
|
.slots
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.item.as_ref())
|
||||||
|
.next()
|
||||||
|
.map(|s| s.item) else {
|
||||||
|
bail!("shift click must move an item");
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(old_slot_kind) = window.slot(packet.slot_idx as u16).map(|s| s.item) else {
|
||||||
|
bail!("shift click must move an item");
|
||||||
|
};
|
||||||
|
ensure!(
|
||||||
|
old_slot_kind == item_kind,
|
||||||
|
"shift click must move the same item kind as modified slots"
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert all moved items are the same kind
|
||||||
|
ensure!(
|
||||||
|
packet
|
||||||
|
.slots
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.item.as_ref())
|
||||||
|
.all(|s| s.item == item_kind),
|
||||||
|
"shift click must move the same item kind"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickMode::Hotbar => {
|
||||||
|
ensure!(
|
||||||
|
packet.slots.len() == 2,
|
||||||
|
"hotbar swap must modify two slots, got {}",
|
||||||
|
packet.slots.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
ensure!(
|
||||||
|
count_deltas == 0,
|
||||||
|
"invalid item delta: expected 0, got {}",
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert that a swap occurs
|
||||||
|
let old_slots = [
|
||||||
|
window.slot(packet.slots[0].idx as u16),
|
||||||
|
window.slot(packet.slots[1].idx as u16),
|
||||||
|
];
|
||||||
|
ensure!(
|
||||||
|
old_slots
|
||||||
|
.iter()
|
||||||
|
.any(|s| s == &packet.slots[0].item.as_ref())
|
||||||
|
&& old_slots
|
||||||
|
.iter()
|
||||||
|
.any(|s| s == &packet.slots[1].item.as_ref()),
|
||||||
|
"swapped items must match"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ClickMode::CreativeMiddleClick => {}
|
||||||
|
ClickMode::DropKey => {
|
||||||
|
ensure!(
|
||||||
|
packet.slots.len() == 1,
|
||||||
|
"drop key must modify exactly one slot"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
packet.slot_idx == packet.slots.first().map(|s| s.idx).unwrap_or(-2),
|
||||||
|
"slot index does not match modified slot"
|
||||||
|
);
|
||||||
|
|
||||||
|
let old_slot = window.slot(packet.slot_idx as u16);
|
||||||
|
let new_slot = packet.slots[0].item.as_ref();
|
||||||
|
let is_transmuting = match (old_slot, new_slot) {
|
||||||
|
// TODO: make sure NBT is the same
|
||||||
|
// Sometimes, the client will add nbt data to an item if it's missing, like "Damage"
|
||||||
|
// to a sword
|
||||||
|
(Some(old_slot), Some(new_slot)) => old_slot.item != new_slot.item,
|
||||||
|
(_, None) => false,
|
||||||
|
(None, Some(_)) => true,
|
||||||
|
};
|
||||||
|
ensure!(!is_transmuting, "transmuting items is not allowed");
|
||||||
|
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
|
||||||
|
let expected_delta = match packet.button {
|
||||||
|
0 => -1,
|
||||||
|
1 => -old_slot.map(|s| s.count() as i32).unwrap_or(0),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
ensure!(
|
||||||
|
count_deltas == expected_delta,
|
||||||
|
"invalid item delta: expected {}, got {}",
|
||||||
|
expected_delta,
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ClickMode::Drag => {
|
||||||
|
if matches!(packet.button, 2 | 6 | 10) {
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
ensure!(
|
||||||
|
count_deltas == 0,
|
||||||
|
"invalid item delta: expected 0, got {}",
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ensure!(packet.slots.is_empty() && packet.carried_item == cursor_item.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClickMode::DoubleClick => {
|
||||||
|
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
|
||||||
|
ensure!(
|
||||||
|
count_deltas == 0,
|
||||||
|
"invalid item delta: expected 0, got {}",
|
||||||
|
count_deltas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the total difference in item counts if the changes in this packet
|
||||||
|
/// were to be applied.
|
||||||
|
///
|
||||||
|
/// Returns a positive number if items were added to the window, and a negative
|
||||||
|
/// number if items were removed from the window.
|
||||||
|
fn calculate_net_item_delta(
|
||||||
|
packet: &ClickSlotC2s,
|
||||||
|
window: &InventoryWindow,
|
||||||
|
cursor_item: &CursorItem,
|
||||||
|
) -> i32 {
|
||||||
|
let mut net_item_delta: i32 = 0;
|
||||||
|
|
||||||
|
for slot in &packet.slots {
|
||||||
|
let old_slot = window.slot(slot.idx as u16);
|
||||||
|
let new_slot = slot.item.as_ref();
|
||||||
|
|
||||||
|
net_item_delta += match (old_slot, new_slot) {
|
||||||
|
(Some(old), Some(new)) => new.count() as i32 - old.count() as i32,
|
||||||
|
(Some(old), None) => -(old.count() as i32),
|
||||||
|
(None, Some(new)) => new.count() as i32,
|
||||||
|
(None, None) => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
net_item_delta += match (cursor_item.0.as_ref(), packet.carried_item.as_ref()) {
|
||||||
|
(Some(old), Some(new)) => new.count() as i32 - old.count() as i32,
|
||||||
|
(Some(old), None) => -(old.count() as i32),
|
||||||
|
(None, Some(new)) => new.count() as i32,
|
||||||
|
(None, None) => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
net_item_delta
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use valence_protocol::item::{ItemKind, ItemStack};
|
||||||
|
use valence_protocol::packet::c2s::play::click_slot::Slot;
|
||||||
|
use valence_protocol::var_int::VarInt;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::prelude::InventoryKind;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn net_item_delta_1() {
|
||||||
|
let drag_packet = ClickSlotC2s {
|
||||||
|
window_id: 2,
|
||||||
|
state_id: VarInt(14),
|
||||||
|
slot_idx: -999,
|
||||||
|
button: 2,
|
||||||
|
mode: ClickMode::Drag,
|
||||||
|
slots: vec![
|
||||||
|
Slot {
|
||||||
|
idx: 4,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 3,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 5,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::Diamond, 1, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let inventory = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
let window = InventoryWindow::new(&player_inventory, Some(&inventory));
|
||||||
|
let cursor_item = CursorItem(Some(ItemStack::new(ItemKind::Diamond, 64, None)));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
calculate_net_item_delta(&drag_packet, &window, &cursor_item),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn net_item_delta_2() {
|
||||||
|
let drag_packet = ClickSlotC2s {
|
||||||
|
window_id: 2,
|
||||||
|
state_id: VarInt(14),
|
||||||
|
slot_idx: -999,
|
||||||
|
button: 2,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
slots: vec![
|
||||||
|
Slot {
|
||||||
|
idx: 2,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 3,
|
||||||
|
item: Some(ItemStack::new(ItemKind::IronIngot, 2, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 4,
|
||||||
|
item: Some(ItemStack::new(ItemKind::GoldIngot, 2, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 5,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Emerald, 2, None)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::OakWood, 2, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let inventory = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
let window = InventoryWindow::new(&player_inventory, Some(&inventory));
|
||||||
|
let cursor_item = CursorItem::default();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
calculate_net_item_delta(&drag_packet, &window, &cursor_item),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_filled_slot_with_empty_cursor_success() {
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let mut inventory = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
inventory.set_slot(0, ItemStack::new(ItemKind::Diamond, 20, None));
|
||||||
|
let cursor_item = CursorItem::default();
|
||||||
|
let packet = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot { idx: 0, item: None }],
|
||||||
|
carried_item: inventory.slot(0).cloned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet, &player_inventory, Some(&inventory))
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect("packet should not fail item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_slot_with_filled_cursor_success() {
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let inventory1 = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
let mut inventory2 = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
inventory2.set_slot(0, ItemStack::new(ItemKind::Diamond, 10, None));
|
||||||
|
let cursor_item = CursorItem(Some(ItemStack::new(ItemKind::Diamond, 20, None)));
|
||||||
|
let packet1 = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 20, None)),
|
||||||
|
}],
|
||||||
|
carried_item: None,
|
||||||
|
};
|
||||||
|
let packet2 = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 30, None)),
|
||||||
|
}],
|
||||||
|
carried_item: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet1, &player_inventory, Some(&inventory1))
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet1,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory1),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect("packet should not fail item duplication check");
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet2, &player_inventory, Some(&inventory2))
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet2,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory2),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect("packet should not fail item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_filled_slot_with_filled_cursor_stack_overflow_success() {
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let mut inventory = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
inventory.set_slot(0, ItemStack::new(ItemKind::Diamond, 20, None));
|
||||||
|
let cursor_item = CursorItem(Some(ItemStack::new(ItemKind::Diamond, 64, None)));
|
||||||
|
let packet = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 64, None)),
|
||||||
|
}],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::Diamond, 20, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet, &player_inventory, Some(&inventory))
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect("packet should not fail item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_filled_slot_with_filled_cursor_different_item_success() {
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let mut inventory = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
inventory.set_slot(0, ItemStack::new(ItemKind::IronIngot, 2, None));
|
||||||
|
let cursor_item = CursorItem(Some(ItemStack::new(ItemKind::Diamond, 2, None)));
|
||||||
|
let packet = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
|
||||||
|
}],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::IronIngot, 2, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet, &player_inventory, Some(&inventory))
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect("packet should not fail item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_slot_with_filled_cursor_failure() {
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let inventory1 = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
let mut inventory2 = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
inventory2.set_slot(0, ItemStack::new(ItemKind::Diamond, 10, None));
|
||||||
|
let cursor_item = CursorItem(Some(ItemStack::new(ItemKind::Diamond, 20, None)));
|
||||||
|
let packet1 = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 22, None)),
|
||||||
|
}],
|
||||||
|
carried_item: None,
|
||||||
|
};
|
||||||
|
let packet2 = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 32, None)),
|
||||||
|
}],
|
||||||
|
carried_item: None,
|
||||||
|
};
|
||||||
|
let packet3 = ClickSlotC2s {
|
||||||
|
window_id: 1,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 0,
|
||||||
|
slots: vec![
|
||||||
|
Slot {
|
||||||
|
idx: 0,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 22, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 1,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 22, None)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
carried_item: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet1, &player_inventory, Some(&inventory1))
|
||||||
|
.expect("packet 1 should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet1,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory1),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect_err("packet 1 should fail item duplication check");
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet2, &player_inventory, Some(&inventory2))
|
||||||
|
.expect("packet 2 should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet2,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory2),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect_err("packet 2 should fail item duplication check");
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet3, &player_inventory, Some(&inventory1))
|
||||||
|
.expect("packet 3 should be valid");
|
||||||
|
validate_click_slot_item_duplication(
|
||||||
|
&packet3,
|
||||||
|
&player_inventory,
|
||||||
|
Some(&inventory1),
|
||||||
|
&cursor_item,
|
||||||
|
)
|
||||||
|
.expect_err("packet 3 should fail item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disallow_item_transmutation() {
|
||||||
|
// no alchemy allowed - make sure that lead can't be turned into gold
|
||||||
|
|
||||||
|
let mut player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
player_inventory.set_slot(9, ItemStack::new(ItemKind::Lead, 2, None));
|
||||||
|
let cursor_item = CursorItem::default();
|
||||||
|
|
||||||
|
let packets = vec![
|
||||||
|
ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::ShiftClick,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 9,
|
||||||
|
slots: vec![
|
||||||
|
Slot { idx: 9, item: None },
|
||||||
|
Slot {
|
||||||
|
idx: 36,
|
||||||
|
item: Some(ItemStack::new(ItemKind::GoldIngot, 2, None)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
carried_item: None,
|
||||||
|
},
|
||||||
|
ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Hotbar,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 9,
|
||||||
|
slots: vec![
|
||||||
|
Slot { idx: 9, item: None },
|
||||||
|
Slot {
|
||||||
|
idx: 36,
|
||||||
|
item: Some(ItemStack::new(ItemKind::GoldIngot, 2, None)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
carried_item: None,
|
||||||
|
},
|
||||||
|
ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 9,
|
||||||
|
slots: vec![Slot { idx: 9, item: None }],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::GoldIngot, 2, None)),
|
||||||
|
},
|
||||||
|
ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::DropKey,
|
||||||
|
state_id: VarInt(0),
|
||||||
|
slot_idx: 9,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 9,
|
||||||
|
item: Some(ItemStack::new(ItemKind::GoldIngot, 1, None)),
|
||||||
|
}],
|
||||||
|
carried_item: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, packet) in packets.iter().enumerate() {
|
||||||
|
validate_click_slot_impossible(packet, &player_inventory, None)
|
||||||
|
.unwrap_or_else(|e| panic!("packet {i} should be valid: {e}"));
|
||||||
|
validate_click_slot_item_duplication(packet, &player_inventory, None, &cursor_item)
|
||||||
|
.expect_err(&format!(
|
||||||
|
"packet {i} passed item duplication check when it should have failed"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_shift_click_overflow_to_new_stack() {
|
||||||
|
let mut player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
player_inventory.set_slot(9, ItemStack::new(ItemKind::Diamond, 64, None));
|
||||||
|
player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 32, None));
|
||||||
|
let cursor_item = CursorItem::default();
|
||||||
|
|
||||||
|
let packet = ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
state_id: VarInt(2),
|
||||||
|
slot_idx: 9,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::ShiftClick,
|
||||||
|
slots: vec![
|
||||||
|
Slot {
|
||||||
|
idx: 37,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 32, None)),
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
idx: 36,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Diamond, 64, None)),
|
||||||
|
},
|
||||||
|
Slot { idx: 9, item: None },
|
||||||
|
],
|
||||||
|
carried_item: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet, &player_inventory, None)
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(&packet, &player_inventory, None, &cursor_item)
|
||||||
|
.expect("packet should pass item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_pickup_overfull_stack_click() {
|
||||||
|
let mut player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
player_inventory.set_slot(9, ItemStack::new(ItemKind::Apple, 100, None));
|
||||||
|
let cursor_item = CursorItem::default();
|
||||||
|
|
||||||
|
let packet = ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
state_id: VarInt(2),
|
||||||
|
slot_idx: 9,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
slots: vec![Slot { idx: 9, item: None }],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::Apple, 100, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet, &player_inventory, None)
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(&packet, &player_inventory, None, &cursor_item)
|
||||||
|
.expect("packet should pass item duplication check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_place_overfull_stack_click() {
|
||||||
|
let player_inventory = Inventory::new(InventoryKind::Player);
|
||||||
|
let cursor_item = CursorItem(Some(ItemStack::new(ItemKind::Apple, 100, None)));
|
||||||
|
|
||||||
|
let packet = ClickSlotC2s {
|
||||||
|
window_id: 0,
|
||||||
|
state_id: VarInt(2),
|
||||||
|
slot_idx: 9,
|
||||||
|
button: 0,
|
||||||
|
mode: ClickMode::Click,
|
||||||
|
slots: vec![Slot {
|
||||||
|
idx: 9,
|
||||||
|
item: Some(ItemStack::new(ItemKind::Apple, 64, None)),
|
||||||
|
}],
|
||||||
|
carried_item: Some(ItemStack::new(ItemKind::Apple, 36, None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_click_slot_impossible(&packet, &player_inventory, None)
|
||||||
|
.expect("packet should be valid");
|
||||||
|
validate_click_slot_item_duplication(&packet, &player_inventory, None, &cursor_item)
|
||||||
|
.expect("packet should pass item duplication check");
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,7 +59,9 @@ pub mod prelude {
|
||||||
pub use entity::{EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw};
|
pub use entity::{EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw};
|
||||||
pub use glam::DVec3;
|
pub use glam::DVec3;
|
||||||
pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance};
|
pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance};
|
||||||
pub use inventory::{Inventory, InventoryKind, OpenInventory};
|
pub use inventory::{
|
||||||
|
Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory,
|
||||||
|
};
|
||||||
pub use player_list::{PlayerList, PlayerListEntry};
|
pub use player_list::{PlayerList, PlayerListEntry};
|
||||||
pub use protocol::block::{BlockState, PropName, PropValue};
|
pub use protocol::block::{BlockState, PropName, PropValue};
|
||||||
pub use protocol::ident::Ident;
|
pub use protocol::ident::Ident;
|
||||||
|
|
|
@ -25,7 +25,7 @@ use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
|
||||||
use crate::dimension::{validate_dimensions, Dimension, DimensionId};
|
use crate::dimension::{validate_dimensions, Dimension, DimensionId};
|
||||||
use crate::entity::EntityPlugin;
|
use crate::entity::EntityPlugin;
|
||||||
use crate::instance::{Instance, InstancePlugin};
|
use crate::instance::{Instance, InstancePlugin};
|
||||||
use crate::inventory::InventoryPlugin;
|
use crate::inventory::{InventoryPlugin, InventorySettings};
|
||||||
use crate::player_list::PlayerListPlugin;
|
use crate::player_list::PlayerListPlugin;
|
||||||
use crate::prelude::event::ClientEventPlugin;
|
use crate::prelude::event::ClientEventPlugin;
|
||||||
use crate::prelude::ComponentPlugin;
|
use crate::prelude::ComponentPlugin;
|
||||||
|
@ -303,6 +303,7 @@ pub fn build_plugin(
|
||||||
|
|
||||||
// Insert resources.
|
// Insert resources.
|
||||||
app.insert_resource(server);
|
app.insert_resource(server);
|
||||||
|
app.insert_resource(InventorySettings::default());
|
||||||
|
|
||||||
// Make the app loop forever at the configured TPS.
|
// Make the app loop forever at the configured TPS.
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,8 +16,8 @@ pub struct ItemStack {
|
||||||
pub nbt: Option<Compound>,
|
pub nbt: Option<Compound>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const STACK_MIN: u8 = 1;
|
pub const STACK_MIN: u8 = 1;
|
||||||
const STACK_MAX: u8 = 127;
|
pub const STACK_MAX: u8 = 127;
|
||||||
|
|
||||||
impl ItemStack {
|
impl ItemStack {
|
||||||
pub fn new(item: ItemKind, count: u8, nbt: Option<Compound>) -> Self {
|
pub fn new(item: ItemKind, count: u8, nbt: Option<Compound>) -> Self {
|
||||||
|
|
|
@ -7,6 +7,8 @@ pub struct ClickSlotC2s {
|
||||||
pub window_id: u8,
|
pub window_id: u8,
|
||||||
pub state_id: VarInt,
|
pub state_id: VarInt,
|
||||||
pub slot_idx: i16,
|
pub slot_idx: i16,
|
||||||
|
/// The button used to click the slot. An enum can't easily be used for this
|
||||||
|
/// because the meaning of this value depends on the mode.
|
||||||
pub button: i8,
|
pub button: i8,
|
||||||
pub mode: ClickMode,
|
pub mode: ClickMode,
|
||||||
pub slots: Vec<Slot>,
|
pub slots: Vec<Slot>,
|
||||||
|
|
Loading…
Reference in a new issue