From 7161491005f4308eff7cce646df910c3c9cc5993 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 27 Dec 2024 00:27:24 +0100 Subject: [PATCH 1/6] add some simple test to our nbt crate --- pumpkin-core/src/text/click.rs | 2 + pumpkin-entity/src/entity_type.rs | 2 +- pumpkin-nbt/src/deserializer.rs | 12 +++--- pumpkin-nbt/src/lib.rs | 62 +++++++++++++++++++++++++++++++ pumpkin-protocol/Cargo.toml | 2 +- pumpkin/Cargo.toml | 2 +- 6 files changed, 73 insertions(+), 9 deletions(-) diff --git a/pumpkin-core/src/text/click.rs b/pumpkin-core/src/text/click.rs index 0e917e15c..caa78f61d 100644 --- a/pumpkin-core/src/text/click.rs +++ b/pumpkin-core/src/text/click.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; pub enum ClickEvent<'a> { /// Opens a URL OpenUrl(Cow<'a, str>), + /// Opens a File + OpenFile(Cow<'a, str>), /// Works in signs, but only on the root text component RunCommand(Cow<'a, str>), /// Replaces the contents of the chat box with the text, not necessarily a diff --git a/pumpkin-entity/src/entity_type.rs b/pumpkin-entity/src/entity_type.rs index 282ac1297..e065722fd 100644 --- a/pumpkin-entity/src/entity_type.rs +++ b/pumpkin-entity/src/entity_type.rs @@ -1,4 +1,4 @@ -// TODO +// TODO make this dynamic #[derive(Clone)] #[repr(i32)] pub enum EntityType { diff --git a/pumpkin-nbt/src/deserializer.rs b/pumpkin-nbt/src/deserializer.rs index ae017b821..80e3b96f4 100644 --- a/pumpkin-nbt/src/deserializer.rs +++ b/pumpkin-nbt/src/deserializer.rs @@ -23,7 +23,7 @@ impl<'de, T: Buf> Deserializer<'de, T> { } } -/// Deserializes struct using Serde Deserializer from unnamed (network) NBT +/// Deserializes struct using Serde Deserializer from normal NBT pub fn from_bytes<'a, T>(s: &'a mut impl Buf) -> Result where T: Deserialize<'a>, @@ -32,20 +32,20 @@ where T::deserialize(&mut deserializer) } -pub fn from_cursor<'a, T>(cursor: &'a mut Cursor<&[u8]>) -> Result +/// Deserializes struct using Serde Deserializer from normal NBT +pub fn from_bytes_unnamed<'a, T>(s: &'a mut impl Buf) -> Result where T: Deserialize<'a>, { - let mut deserializer = Deserializer::new(cursor, true); + let mut deserializer = Deserializer::new(s, false); T::deserialize(&mut deserializer) } -/// Deserializes struct using Serde Deserializer from normal NBT -pub fn from_bytes_unnamed<'a, T>(s: &'a mut impl Buf) -> Result +pub fn from_cursor<'a, T>(cursor: &'a mut Cursor<&[u8]>) -> Result where T: Deserialize<'a>, { - let mut deserializer = Deserializer::new(s, false); + let mut deserializer = Deserializer::new(cursor, true); T::deserialize(&mut deserializer) } diff --git a/pumpkin-nbt/src/lib.rs b/pumpkin-nbt/src/lib.rs index b2be0d657..acee723fe 100644 --- a/pumpkin-nbt/src/lib.rs +++ b/pumpkin-nbt/src/lib.rs @@ -200,3 +200,65 @@ macro_rules! impl_array { impl_array!(IntArray, "int"); impl_array!(LongArray, "long"); impl_array!(BytesArray, "byte"); + +#[cfg(test)] +mod test { + use serde::{Deserialize, Serialize}; + + use crate::BytesArray; + use crate::IntArray; + use crate::LongArray; + use crate::{ + deserializer::from_bytes_unnamed, + serializer::to_bytes_unnamed, + }; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + byte: i8, + short: i16, + int: i32, + long: i64, + float: f32, + string: String, + } + + #[test] + fn test_simple_ser_de_unamed() { + let test = Test { + byte: 123, + short: 1342, + int: 4313, + long: 34, + float: 1.00, + string: "Hello test".to_string(), + }; + let mut bytes = to_bytes_unnamed(&test).unwrap(); + let recreated_struct: Test = from_bytes_unnamed(&mut bytes).unwrap(); + + assert_eq!(test, recreated_struct); + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct TestArray { + #[serde(with = "BytesArray")] + byte_array: Vec, + #[serde(with = "IntArray")] + int_array: Vec, + #[serde(with = "LongArray")] + long_array: Vec, + } + + #[test] + fn test_simple_ser_de_array() { + let test = TestArray { + byte_array: vec![0, 3, 2], + int_array: vec![13, 1321, 2], + long_array: vec![1, 0, 200301, 1], + }; + let mut bytes = to_bytes_unnamed(&test).unwrap(); + let recreated_struct: TestArray = from_bytes_unnamed(&mut bytes).unwrap(); + + assert_eq!(test, recreated_struct); + } +} diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index c167df953..f04d77311 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -30,4 +30,4 @@ aes = "0.8.4" cfb8 = "0.8.1" # decryption -libdeflater = "1.22.0" \ No newline at end of file +libdeflater = "1.23.0" \ No newline at end of file diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index a3f4a14ea..87b030201 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -46,7 +46,7 @@ rsa = "0.9.7" rsa-der = "0.3.0" # authentication -reqwest = { version = "0.12.9", default-features = false, features = [ +reqwest = { version = "0.12.10", default-features = false, features = [ "http2", "json", "macos-system-configuration", From af54cb428969a2861f239bdd1f60969758d15f5b Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 27 Dec 2024 13:15:45 +0100 Subject: [PATCH 2/6] fix: clippy fmt --- pumpkin-nbt/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pumpkin-nbt/src/lib.rs b/pumpkin-nbt/src/lib.rs index acee723fe..5ed48da9b 100644 --- a/pumpkin-nbt/src/lib.rs +++ b/pumpkin-nbt/src/lib.rs @@ -208,10 +208,7 @@ mod test { use crate::BytesArray; use crate::IntArray; use crate::LongArray; - use crate::{ - deserializer::from_bytes_unnamed, - serializer::to_bytes_unnamed, - }; + use crate::{deserializer::from_bytes_unnamed, serializer::to_bytes_unnamed}; #[derive(Serialize, Deserialize, PartialEq, Debug)] struct Test { From 37372986b029985e839dedcc615932187dd9d3d8 Mon Sep 17 00:00:00 2001 From: LieOnLion Date: Fri, 27 Dec 2024 12:18:43 +0000 Subject: [PATCH 3/6] Fixed Pumpkin website links (#423) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e4df0ef3..f71139300 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ -[Pumpkin](https://snowiiii.github.io/Pumpkin/) is a Minecraft server built entirely in Rust, offering a fast, efficient, +[Pumpkin](https://snowiiii.github.io/Pumpkin-Website/) is a Minecraft server built entirely in Rust, offering a fast, efficient, and customizable experience. It prioritizes performance and player enjoyment while adhering to the core mechanics of the game. ![image](https://github.com/user-attachments/assets/7e2e865e-b150-4675-a2d5-b52f9900378e) @@ -87,7 +87,7 @@ Check out our [Github Project](https://github.com/users/Snowiiii/projects/12/vie ## How to run -See our [Quick Start](https://snowiiii.github.io/Pumpkin/about/quick-start.html) Guide to get Pumpkin running +See our [Quick Start](https://snowiiii.github.io/Pumpkin-Website/about/quick-start.html) Guide to get Pumpkin running ## Contributions @@ -95,7 +95,7 @@ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) ## Docs -The Documentation of Pumpkin can be found at https://snowiiii.github.io/Pumpkin/ +The Documentation of Pumpkin can be found at https://snowiiii.github.io/Pumpkin-Website/ ## Communication From 98d140664639cffc1b48167adb2dff3d24c12624 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 27 Dec 2024 16:04:40 +0100 Subject: [PATCH 4/6] Implement session.lock Lock session.lock so other processes know that we currently read and write to the level. This is vanilla behavior. Test: Tested to start the a new server while on was locking the file, and got `Failed to lock level: AlreadyLocked("session.lock")` as expected --- pumpkin-world/Cargo.toml | 2 ++ pumpkin-world/src/level.rs | 9 +++++++++ pumpkin-world/src/lib.rs | 1 + pumpkin-world/src/lock/anvil.rs | 32 ++++++++++++++++++++++++++++++++ pumpkin-world/src/lock/mod.rs | 18 ++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 pumpkin-world/src/lock/anvil.rs create mode 100644 pumpkin-world/src/lock/mod.rs diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index 495ee3ff8..1c7eacd87 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -25,6 +25,8 @@ dashmap = "6.1.0" flate2 = "1.0" lz4 = "1.28.0" +file-guard = "0.2.0" + enum_dispatch = "0.3.13" fastnbt = { git = "https://github.com/owengage/fastnbt.git" } diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 930363ec6..ede2d6168 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -14,6 +14,7 @@ use crate::{ anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError, }, generation::{get_world_gen, Seed, WorldGenerator}, + lock::{anvil::AnvilLevelLocker, LevelLocker}, world_info::{anvil::AnvilLevelInfo, LevelData, WorldInfoReader, WorldInfoWriter}, }; @@ -35,6 +36,9 @@ pub struct Level { chunk_watchers: Arc, usize>>, chunk_reader: Arc, world_gen: Arc, + // Gets unlocked when dropped + // TODO: Make this a trait + _locker: Arc, } #[derive(Clone)] @@ -55,6 +59,10 @@ impl Level { region_folder, }; + // if we fail to lock, lets crash ???. maybe not the best soultion when we have a large server with many worlds and one is locked. + // So TODO + let locker = AnvilLevelLocker::look(&level_folder).expect("Failed to lock level"); + // TODO: Load info correctly based on world format type let level_info = AnvilLevelInfo .read_world_info(&level_folder) @@ -71,6 +79,7 @@ impl Level { loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), level_info, + _locker: Arc::new(locker), } } diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 8e26b1060..ec23b1516 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -16,6 +16,7 @@ pub mod dimension; mod generation; pub mod item; pub mod level; +mod lock; pub mod world_info; pub const WORLD_HEIGHT: usize = 384; pub const WORLD_LOWEST_Y: i16 = -64; diff --git a/pumpkin-world/src/lock/anvil.rs b/pumpkin-world/src/lock/anvil.rs new file mode 100644 index 000000000..7864cf711 --- /dev/null +++ b/pumpkin-world/src/lock/anvil.rs @@ -0,0 +1,32 @@ +use file_guard::{FileGuard, Lock}; + +use super::{LevelLocker, LockError}; + +use std::{fs::File, io::Write, sync::Arc}; + +pub struct AnvilLevelLocker { + _lock: Option>>, +} + +const SESSION_LOCK_FILE_NAME: &str = "session.lock"; + +const SNOWMAN: &[u8] = "☃".as_bytes(); + +impl LevelLocker for AnvilLevelLocker { + fn look(folder: &crate::level::LevelFolder) -> Result { + let file_path = folder.root_folder.join(SESSION_LOCK_FILE_NAME); + let mut file = File::options() + .create(true) + .truncate(false) + .write(true) + .open(file_path) + .unwrap(); + // im not joking, mojang writes a snowman into the lock file + file.write_all(SNOWMAN) + .map_err(|_| LockError::FailedWrite)?; + let file_arc = Arc::new(file); + let lock = file_guard::try_lock(file_arc, Lock::Exclusive, 0, 1) + .map_err(|_| LockError::AlreadyLocked(SESSION_LOCK_FILE_NAME.to_string()))?; + Ok(Self { _lock: Some(lock) }) + } +} diff --git a/pumpkin-world/src/lock/mod.rs b/pumpkin-world/src/lock/mod.rs new file mode 100644 index 000000000..bf4b262f2 --- /dev/null +++ b/pumpkin-world/src/lock/mod.rs @@ -0,0 +1,18 @@ +use thiserror::Error; + +use crate::level::LevelFolder; + +pub mod anvil; + +// Gets unlocked when dropped +pub trait LevelLocker: Send + Sync { + fn look(folder: &LevelFolder) -> Result; +} + +#[derive(Error, Debug)] +pub enum LockError { + #[error("Oh no, Level is already locked by {0}")] + AlreadyLocked(String), + #[error("Failed to write into lock file")] + FailedWrite, +} From c521ef8bb0be0c6c8cb1887c087a16c6730ca2fd Mon Sep 17 00:00:00 2001 From: Kris <37947442+OfficialKris@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:24:23 -0500 Subject: [PATCH 5/6] Correct Interactive Block Behavior (#388) * Added chest and furnace * Corrected interactive block functionality * Fixed merge * Added standard functions and player remove on destroy * Added unique block standard functions * Fix clippy * Fix deadlock in standard function --- pumpkin-core/src/math/position.rs | 2 +- pumpkin-inventory/src/lib.rs | 7 ++ pumpkin-inventory/src/open_container.rs | 22 +++++ pumpkin/src/block/block_manager.rs | 18 +++- pumpkin/src/block/blocks/chest.rs | 65 +++++++----- pumpkin/src/block/blocks/crafting_table.rs | 75 +++++++++----- pumpkin/src/block/blocks/furnace.rs | 55 ++++++----- pumpkin/src/block/blocks/jukebox.rs | 18 +++- pumpkin/src/block/blocks/mod.rs | 110 +++++++++++++++++++++ pumpkin/src/block/pumpkin_block.rs | 40 +++++++- pumpkin/src/net/container.rs | 1 + pumpkin/src/net/packet/play.rs | 16 +-- pumpkin/src/server/mod.rs | 57 +++++++++++ 13 files changed, 400 insertions(+), 86 deletions(-) diff --git a/pumpkin-core/src/math/position.rs b/pumpkin-core/src/math/position.rs index 839890646..a3b82cbeb 100644 --- a/pumpkin-core/src/math/position.rs +++ b/pumpkin-core/src/math/position.rs @@ -5,7 +5,7 @@ use crate::math::vector2::Vector2; use num_traits::Euclid; use serde::{Deserialize, Serialize}; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] /// Aka Block Position pub struct WorldPosition(pub Vector3); diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index bf56dca63..f1273b054 100644 --- a/pumpkin-inventory/src/lib.rs +++ b/pumpkin-inventory/src/lib.rs @@ -94,6 +94,13 @@ pub trait Container: Sync + Send { fn all_slots_ref(&self) -> Vec>; + fn clear_all_slots(&mut self) { + let all_slots = self.all_slots(); + for stack in all_slots { + *stack = None; + } + } + fn all_combinable_slots(&self) -> Vec> { self.all_slots_ref() } diff --git a/pumpkin-inventory/src/open_container.rs b/pumpkin-inventory/src/open_container.rs index ec68578fb..3025ed92f 100644 --- a/pumpkin-inventory/src/open_container.rs +++ b/pumpkin-inventory/src/open_container.rs @@ -5,8 +5,10 @@ use pumpkin_world::block::block_registry::Block; use pumpkin_world::item::ItemStack; use std::sync::Arc; use tokio::sync::Mutex; + pub struct OpenContainer { // TODO: unique id should be here + // TODO: should this be uuid? players: Vec, container: Arc>>, location: Option, @@ -54,6 +56,22 @@ impl OpenContainer { } } + pub fn is_location(&self, try_position: WorldPosition) -> bool { + if let Some(location) = self.location { + location == try_position + } else { + false + } + } + + pub async fn clear_all_slots(&self) { + self.container.lock().await.clear_all_slots(); + } + + pub fn clear_all_players(&mut self) { + self.players = vec![]; + } + pub fn all_player_ids(&self) -> Vec { self.players.clone() } @@ -62,6 +80,10 @@ impl OpenContainer { self.location } + pub async fn set_location(&mut self, location: Option) { + self.location = location; + } + pub fn get_block(&self) -> Option { self.block.clone() } diff --git a/pumpkin/src/block/block_manager.rs b/pumpkin/src/block/block_manager.rs index 92395bb81..0fb26ede3 100644 --- a/pumpkin/src/block/block_manager.rs +++ b/pumpkin/src/block/block_manager.rs @@ -2,6 +2,7 @@ use crate::block::pumpkin_block::{BlockMetadata, PumpkinBlock}; use crate::entity::player::Player; use crate::server::Server; use pumpkin_core::math::position::WorldPosition; +use pumpkin_inventory::OpenContainer; use pumpkin_world::block::block_registry::Block; use pumpkin_world::item::item_registry::Item; use std::collections::HashMap; @@ -33,7 +34,7 @@ impl BlockManager { ) { let pumpkin_block = self.get_pumpkin_block(block); if let Some(pumpkin_block) = pumpkin_block { - pumpkin_block.on_use(player, location, server).await; + pumpkin_block.on_use(block, player, location, server).await; } } @@ -48,7 +49,7 @@ impl BlockManager { let pumpkin_block = self.get_pumpkin_block(block); if let Some(pumpkin_block) = pumpkin_block { return pumpkin_block - .on_use_with_item(player, location, item, server) + .on_use_with_item(block, player, location, item, server) .await; } BlockActionResult::Continue @@ -63,7 +64,9 @@ impl BlockManager { ) { let pumpkin_block = self.get_pumpkin_block(block); if let Some(pumpkin_block) = pumpkin_block { - pumpkin_block.on_placed(player, location, server).await; + pumpkin_block + .on_placed(block, player, location, server) + .await; } } @@ -76,7 +79,9 @@ impl BlockManager { ) { let pumpkin_block = self.get_pumpkin_block(block); if let Some(pumpkin_block) = pumpkin_block { - pumpkin_block.on_broken(player, location, server).await; + pumpkin_block + .on_broken(block, player, location, server) + .await; } } @@ -86,10 +91,13 @@ impl BlockManager { player: &Player, location: WorldPosition, server: &Server, + container: &mut OpenContainer, ) { let pumpkin_block = self.get_pumpkin_block(block); if let Some(pumpkin_block) = pumpkin_block { - pumpkin_block.on_close(player, location, server).await; + pumpkin_block + .on_close(block, player, location, server, container) + .await; } } diff --git a/pumpkin/src/block/blocks/chest.rs b/pumpkin/src/block/blocks/chest.rs index 1600bb096..313d9a5bf 100644 --- a/pumpkin/src/block/blocks/chest.rs +++ b/pumpkin/src/block/blocks/chest.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use pumpkin_core::math::position::WorldPosition; use pumpkin_inventory::{Chest, OpenContainer, WindowType}; use pumpkin_macros::{pumpkin_block, sound}; -use pumpkin_world::{block::block_registry::get_block, item::item_registry::Item}; +use pumpkin_world::{block::block_registry::Block, item::item_registry::Item}; use crate::{ block::{block_manager::BlockActionResult, pumpkin_block::PumpkinBlock}, @@ -15,8 +15,15 @@ pub struct ChestBlock; #[async_trait] impl PumpkinBlock for ChestBlock { - async fn on_use<'a>(&self, player: &Player, _location: WorldPosition, server: &Server) { - self.open_chest_block(player, _location, server).await; + async fn on_use<'a>( + &self, + block: &Block, + player: &Player, + _location: WorldPosition, + server: &Server, + ) { + self.open_chest_block(block, player, _location, server) + .await; player .world() .play_block_sound(sound!("block.chest.open"), _location) @@ -25,46 +32,60 @@ impl PumpkinBlock for ChestBlock { async fn on_use_with_item<'a>( &self, + block: &Block, player: &Player, _location: WorldPosition, _item: &Item, server: &Server, ) -> BlockActionResult { - self.open_chest_block(player, _location, server).await; + self.open_chest_block(block, player, _location, server) + .await; BlockActionResult::Consume } - async fn on_close<'a>(&self, player: &Player, _location: WorldPosition, _server: &Server) { + async fn on_broken<'a>( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + ) { + super::standard_on_broken_with_container(block, player, location, server).await; + } + + async fn on_close<'a>( + &self, + _block: &Block, + player: &Player, + _location: WorldPosition, + _server: &Server, + _container: &OpenContainer, + ) { player .world() .play_block_sound(sound!("block.chest.close"), _location) .await; + // TODO: send entity updates close } } impl ChestBlock { pub async fn open_chest_block( &self, + block: &Block, player: &Player, location: WorldPosition, server: &Server, ) { - let entity_id = player.entity_id(); - // TODO: This should be a unique identifier for the each block container - player.open_container.store(Some(2)); - { - let mut open_containers = server.open_containers.write().await; - if let Some(chest) = open_containers.get_mut(&2) { - chest.add_player(entity_id); - } else { - let open_container = OpenContainer::new_empty_container::( - entity_id, - Some(location), - get_block("minecraft:chest").cloned(), - ); - open_containers.insert(2, open_container); - } - } - player.open_container(server, WindowType::Generic9x3).await; + // TODO: shouldn't Chest and window type be constrained together to avoid errors? + super::standard_open_container::( + block, + player, + location, + server, + WindowType::Generic9x3, + ) + .await; + // TODO: send entity updates open } } diff --git a/pumpkin/src/block/blocks/crafting_table.rs b/pumpkin/src/block/blocks/crafting_table.rs index 962d56626..01ad41795 100644 --- a/pumpkin/src/block/blocks/crafting_table.rs +++ b/pumpkin/src/block/blocks/crafting_table.rs @@ -6,54 +6,83 @@ use async_trait::async_trait; use pumpkin_core::math::position::WorldPosition; use pumpkin_inventory::{CraftingTable, OpenContainer, WindowType}; use pumpkin_macros::pumpkin_block; -use pumpkin_world::{block::block_registry::get_block, item::item_registry::Item}; +use pumpkin_world::{block::block_registry::Block, item::item_registry::Item}; #[pumpkin_block("minecraft:crafting_table")] pub struct CraftingTableBlock; #[async_trait] impl PumpkinBlock for CraftingTableBlock { - async fn on_use<'a>(&self, player: &Player, _location: WorldPosition, server: &Server) { - self.open_crafting_screen(player, _location, server).await; + async fn on_use<'a>( + &self, + block: &Block, + player: &Player, + _location: WorldPosition, + server: &Server, + ) { + self.open_crafting_screen(block, player, _location, server) + .await; } async fn on_use_with_item<'a>( &self, + block: &Block, player: &Player, _location: WorldPosition, _item: &Item, server: &Server, ) -> BlockActionResult { - self.open_crafting_screen(player, _location, server).await; + self.open_crafting_screen(block, player, _location, server) + .await; BlockActionResult::Consume } -} -impl CraftingTableBlock { - pub async fn open_crafting_screen( + async fn on_broken<'a>( &self, + block: &Block, player: &Player, location: WorldPosition, server: &Server, ) { - //TODO: Adjust /craft command to real crafting table + super::standard_on_broken_with_container(block, player, location, server).await; + } + + async fn on_close<'a>( + &self, + _block: &Block, + player: &Player, + _location: WorldPosition, + _server: &Server, + container: &OpenContainer, + ) { let entity_id = player.entity_id(); - player.open_container.store(Some(1)); - { - let mut open_containers = server.open_containers.write().await; - if let Some(ender_chest) = open_containers.get_mut(&1) { - ender_chest.add_player(entity_id); - } else { - let open_container = OpenContainer::new_empty_container::( - entity_id, - Some(location), - get_block("minecraft:crafting_table").cloned(), - ); - open_containers.insert(1, open_container); + for player_id in container.all_player_ids() { + if entity_id == player_id { + container.clear_all_slots().await; } } - player - .open_container(server, WindowType::CraftingTable) - .await; + + // TODO: items should be re-added to player inventory or dropped dependending on if they are in movement. + // TODO: unique containers should be implemented as a separate stack internally (optimizes large player servers for example) + // TODO: ephemeral containers (crafting tables) might need to be separate data structure than stored (ender chest) + } +} + +impl CraftingTableBlock { + pub async fn open_crafting_screen( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + ) { + super::standard_open_container_unique::( + block, + player, + location, + server, + WindowType::CraftingTable, + ) + .await; } } diff --git a/pumpkin/src/block/blocks/furnace.rs b/pumpkin/src/block/blocks/furnace.rs index 8fc479492..1ecdf9950 100644 --- a/pumpkin/src/block/blocks/furnace.rs +++ b/pumpkin/src/block/blocks/furnace.rs @@ -3,9 +3,9 @@ use crate::entity::player::Player; use async_trait::async_trait; use pumpkin_core::math::position::WorldPosition; use pumpkin_inventory::Furnace; -use pumpkin_inventory::{OpenContainer, WindowType}; +use pumpkin_inventory::WindowType; use pumpkin_macros::pumpkin_block; -use pumpkin_world::block::block_registry::get_block; +use pumpkin_world::block::block_registry::Block; use pumpkin_world::item::item_registry::Item; use crate::{block::pumpkin_block::PumpkinBlock, server::Server}; @@ -15,45 +15,56 @@ pub struct FurnaceBlock; #[async_trait] impl PumpkinBlock for FurnaceBlock { - async fn on_use<'a>(&self, player: &Player, _location: WorldPosition, server: &Server) { - self.open_furnace_screen(player, _location, server).await; + async fn on_use<'a>( + &self, + block: &Block, + player: &Player, + _location: WorldPosition, + server: &Server, + ) { + self.open_furnace_screen(block, player, _location, server) + .await; } async fn on_use_with_item<'a>( &self, + block: &Block, player: &Player, _location: WorldPosition, _item: &Item, server: &Server, ) -> BlockActionResult { - self.open_furnace_screen(player, _location, server).await; + self.open_furnace_screen(block, player, _location, server) + .await; BlockActionResult::Consume } + + async fn on_broken<'a>( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + ) { + super::standard_on_broken_with_container(block, player, location, server).await; + } } impl FurnaceBlock { pub async fn open_furnace_screen( &self, + block: &Block, player: &Player, location: WorldPosition, server: &Server, ) { - //TODO: Adjust /craft command to real crafting table - let entity_id = player.entity_id(); - player.open_container.store(Some(3)); - { - let mut open_containers = server.open_containers.write().await; - if let Some(ender_chest) = open_containers.get_mut(&3) { - ender_chest.add_player(entity_id); - } else { - let open_container = OpenContainer::new_empty_container::( - entity_id, - Some(location), - get_block("minecraft:furnace").cloned(), - ); - open_containers.insert(3, open_container); - } - } - player.open_container(server, WindowType::Furnace).await; + super::standard_open_container::( + block, + player, + location, + server, + WindowType::Furnace, + ) + .await; } } diff --git a/pumpkin/src/block/blocks/jukebox.rs b/pumpkin/src/block/blocks/jukebox.rs index 3c52395ba..4836c4be7 100644 --- a/pumpkin/src/block/blocks/jukebox.rs +++ b/pumpkin/src/block/blocks/jukebox.rs @@ -6,6 +6,7 @@ use async_trait::async_trait; use pumpkin_core::math::position::WorldPosition; use pumpkin_macros::pumpkin_block; use pumpkin_registry::SYNCED_REGISTRIES; +use pumpkin_world::block::block_registry::Block; use pumpkin_world::item::item_registry::Item; #[pumpkin_block("minecraft:jukebox")] @@ -13,7 +14,13 @@ pub struct JukeboxBlock; #[async_trait] impl PumpkinBlock for JukeboxBlock { - async fn on_use<'a>(&self, player: &Player, location: WorldPosition, _server: &Server) { + async fn on_use<'a>( + &self, + _block: &Block, + player: &Player, + location: WorldPosition, + _server: &Server, + ) { // For now just stop the music at this position let world = &player.living_entity.entity.world; @@ -22,6 +29,7 @@ impl PumpkinBlock for JukeboxBlock { async fn on_use_with_item<'a>( &self, + _block: &Block, player: &Player, location: WorldPosition, item: &Item, @@ -49,7 +57,13 @@ impl PumpkinBlock for JukeboxBlock { BlockActionResult::Consume } - async fn on_broken<'a>(&self, player: &Player, location: WorldPosition, _server: &Server) { + async fn on_broken<'a>( + &self, + _block: &Block, + player: &Player, + location: WorldPosition, + _server: &Server, + ) { // For now just stop the music at this position let world = &player.living_entity.entity.world; diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index 4bdfa819b..b0b128028 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -1,4 +1,114 @@ +use pumpkin_core::math::position::WorldPosition; +use pumpkin_inventory::{Container, OpenContainer, WindowType}; +use pumpkin_world::block::block_registry::Block; + +use crate::{entity::player::Player, server::Server}; + pub(crate) mod chest; pub(crate) mod crafting_table; pub(crate) mod furnace; pub(crate) mod jukebox; + +/// The standard destroy with container removes the player forcibly from the container, +/// drops items to the floor, and back to the player's inventory if the item stack is in movement. +pub async fn standard_on_broken_with_container( + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, +) { + // TODO: drop all items and back to players inventory if in motion + if let Some(all_container_ids) = server.get_all_container_ids(location, block.clone()).await { + let mut open_containers = server.open_containers.write().await; + for individual_id in all_container_ids { + if let Some(container) = open_containers.get_mut(&u64::from(individual_id)) { + container.clear_all_slots().await; + player.open_container.store(None); + close_all_in_container(player, container).await; + container.clear_all_players(); + } + } + } +} + +/// The standard open container creates a new container if a container of the same block +/// type does not exist at the selected block location. If a container of the same type exists, the player +/// is added to the currently connected players to that container. +pub async fn standard_open_container( + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + window_type: WindowType, +) { + let entity_id = player.entity_id(); + // If container exists, add player to container, otherwise create new container + if let Some(container_id) = server.get_container_id(location, block.clone()).await { + let mut open_containers = server.open_containers.write().await; + log::debug!("Using previous standard container ID: {}", container_id); + if let Some(container) = open_containers.get_mut(&u64::from(container_id)) { + container.add_player(entity_id); + player.open_container.store(Some(container_id.into())); + } + } else { + let mut open_containers = server.open_containers.write().await; + let new_id = server.new_container_id(); + log::debug!("Creating new standard container ID: {}", new_id); + let open_container = + OpenContainer::new_empty_container::(entity_id, Some(location), Some(block.clone())); + open_containers.insert(new_id.into(), open_container); + player.open_container.store(Some(new_id.into())); + } + player.open_container(server, window_type).await; +} + +pub async fn standard_open_container_unique( + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + window_type: WindowType, +) { + let entity_id = player.entity_id(); + let mut open_containers = server.open_containers.write().await; + let mut id_to_use = -1; + + // TODO: we can do better than brute force + for (id, container) in open_containers.iter() { + if let Some(a_block) = container.get_block() { + if a_block.id == block.id && container.all_player_ids().is_empty() { + id_to_use = *id as i64; + } + } + } + + if id_to_use == -1 { + let new_id = server.new_container_id(); + log::debug!("Creating new unqiue container ID: {}", new_id); + let open_container = + OpenContainer::new_empty_container::(entity_id, Some(location), Some(block.clone())); + + open_containers.insert(new_id.into(), open_container); + + player.open_container.store(Some(new_id.into())); + } else { + log::debug!("Using previous unqiue container ID: {}", id_to_use); + if let Some(unique_container) = open_containers.get_mut(&(id_to_use as u64)) { + unique_container.set_location(Some(location)).await; + unique_container.add_player(entity_id); + player + .open_container + .store(Some(id_to_use.try_into().unwrap())); + } + } + drop(open_containers); + player.open_container(server, window_type).await; +} + +pub async fn close_all_in_container(player: &Player, container: &OpenContainer) { + for id in container.all_player_ids() { + if let Some(remote_player) = player.world().get_player_by_entityid(id).await { + remote_player.close_container().await; + } + } +} diff --git a/pumpkin/src/block/pumpkin_block.rs b/pumpkin/src/block/pumpkin_block.rs index 1849ea5a5..c0a65c316 100644 --- a/pumpkin/src/block/pumpkin_block.rs +++ b/pumpkin/src/block/pumpkin_block.rs @@ -3,6 +3,8 @@ use crate::entity::player::Player; use crate::server::Server; use async_trait::async_trait; use pumpkin_core::math::position::WorldPosition; +use pumpkin_inventory::OpenContainer; +use pumpkin_world::block::block_registry::Block; use pumpkin_world::item::item_registry::Item; pub trait BlockMetadata { @@ -15,9 +17,17 @@ pub trait BlockMetadata { #[async_trait] pub trait PumpkinBlock: Send + Sync { - async fn on_use<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} + async fn on_use<'a>( + &self, + _block: &Block, + _player: &Player, + _location: WorldPosition, + _server: &Server, + ) { + } async fn on_use_with_item<'a>( &self, + _block: &Block, _player: &Player, _location: WorldPosition, _item: &Item, @@ -26,9 +36,31 @@ pub trait PumpkinBlock: Send + Sync { BlockActionResult::Continue } - async fn on_placed<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} + async fn on_placed<'a>( + &self, + _block: &Block, + _player: &Player, + _location: WorldPosition, + _server: &Server, + ) { + } - async fn on_broken<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} + async fn on_broken<'a>( + &self, + _block: &Block, + _player: &Player, + _location: WorldPosition, + _server: &Server, + ) { + } - async fn on_close<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} + async fn on_close<'a>( + &self, + _block: &Block, + _player: &Player, + _location: WorldPosition, + _server: &Server, + _container: &OpenContainer, + ) { + } } diff --git a/pumpkin/src/net/container.rs b/pumpkin/src/net/container.rs index 9e06a46de..c157b5e25 100644 --- a/pumpkin/src/net/container.rs +++ b/pumpkin/src/net/container.rs @@ -81,6 +81,7 @@ impl Player { } /// The official Minecraft client is weird, and will always just close *any* window that is opened when this gets sent + // TODO: is this just bc ids are not synced? pub async fn close_container(&self) { let mut inventory = self.inventory().lock().await; inventory.total_opened_containers += 1; diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index ec01ee04e..baf904659 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -18,7 +18,7 @@ use pumpkin_core::{ text::TextComponent, GameMode, }; -use pumpkin_inventory::{InventoryError, WindowType}; +use pumpkin_inventory::InventoryError; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse; use pumpkin_protocol::{ @@ -838,11 +838,13 @@ impl Player { // TODO: // This function will in the future be used to keep track of if the client is in a valid state. // But this is not possible yet - pub async fn handle_close_container(&self, server: &Server, packet: SCloseContainer) { - let Some(_window_type) = WindowType::from_i32(packet.window_id.0) else { - self.kick(TextComponent::text("Invalid window ID")).await; - return; - }; + pub async fn handle_close_container(&self, server: &Server, _packet: SCloseContainer) { + // TODO: This should check if player sent this packet before + // let Some(_window_type) = WindowType::from_i32(packet.window_id.0) else { + // log::info!("Closed ID: {}", packet.window_id.0); + // self.kick(TextComponent::text("Invalid window ID")).await; + // return; + // }; // window_id 0 represents both 9x1 Generic AND inventory here let mut inventory = self.inventory().lock().await; @@ -856,7 +858,7 @@ impl Player { if let Some(block) = container.get_block() { server .block_manager - .on_close(&block, self, pos, server) //block, self, location, server) + .on_close(&block, self, pos, server, container) //block, self, location, server) .await; } } diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 2ae5804f5..96090ae8d 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -1,6 +1,7 @@ use connection_cache::{CachedBranding, CachedStatus}; use key_store::KeyStore; use pumpkin_config::BASIC_CONFIG; +use pumpkin_core::math::position::WorldPosition; use pumpkin_core::math::vector2::Vector2; use pumpkin_core::GameMode; use pumpkin_entity::EntityId; @@ -9,9 +10,11 @@ use pumpkin_inventory::{Container, OpenContainer}; use pumpkin_protocol::client::login::CEncryptionRequest; use pumpkin_protocol::{client::config::CPluginMessage, ClientPacket}; use pumpkin_registry::{DimensionType, Registry}; +use pumpkin_world::block::block_registry::Block; use pumpkin_world::dimension::Dimension; use rand::prelude::SliceRandom; use std::collections::HashMap; +use std::sync::atomic::AtomicU32; use std::{ sync::{ atomic::{AtomicI32, Ordering}, @@ -57,10 +60,13 @@ pub struct Server { /// Caches game registries for efficient access. pub cached_registry: Vec, /// Tracks open containers used for item interactions. + // TODO: should have per player open_containers pub open_containers: RwLock>, pub drag_handler: DragHandler, /// Assigns unique IDs to entities. entity_id: AtomicI32, + /// Assigns unique IDs to containers. + container_id: AtomicU32, /// Manages authentication with a authentication server, if enabled. pub auth_client: Option, /// The server's custom bossbars @@ -102,6 +108,7 @@ impl Server { drag_handler: DragHandler::new(), // 0 is invalid entity_id: 2.into(), + container_id: 0.into(), worlds: vec![Arc::new(world)], dimensions: vec![ DimensionType::Overworld, @@ -192,6 +199,51 @@ impl Server { .cloned() } + /// Returns the first id with a matching location and block type. If this is used with unique + /// blocks, the output will return a random result. + pub async fn get_container_id(&self, location: WorldPosition, block: Block) -> Option { + let open_containers = self.open_containers.read().await; + // TODO: do better than brute force + for (id, container) in open_containers.iter() { + if container.is_location(location) { + if let Some(container_block) = container.get_block() { + if container_block.id == block.id { + log::debug!("Found container id: {}", id); + return Some(*id as u32); + } + } + } + } + + drop(open_containers); + + None + } + + pub async fn get_all_container_ids( + &self, + location: WorldPosition, + block: Block, + ) -> Option> { + let open_containers = self.open_containers.read().await; + let mut matching_container_ids: Vec = vec![]; + // TODO: do better than brute force + for (id, container) in open_containers.iter() { + if container.is_location(location) { + if let Some(container_block) = container.get_block() { + if container_block.id == block.id { + log::debug!("Found matching container id: {}", id); + matching_container_ids.push(*id as u32); + } + } + } + } + + drop(open_containers); + + Some(matching_container_ids) + } + /// Broadcasts a packet to all players in all worlds. /// /// This function sends the specified packet to every connected player in every world managed by the server. @@ -303,6 +355,11 @@ impl Server { self.entity_id.fetch_add(1, Ordering::SeqCst) } + /// Generates a new container id + pub fn new_container_id(&self) -> u32 { + self.container_id.fetch_add(1, Ordering::SeqCst) + } + pub fn get_branding(&self) -> CPluginMessage<'_> { self.server_branding.get_branding() } From 6a5add8de54de6c285cd61e6a81f6cfa26fc14d1 Mon Sep 17 00:00:00 2001 From: Kyle Davis Date: Fri, 27 Dec 2024 11:22:19 -0500 Subject: [PATCH 6/6] Added native operator permission management (#348) * Starting work on Operator permission system This commit adds the ops.json configuration found in vanilla servers, and updates the basic configuration to handle the default permission level. Server now uses ops file and defaults players to op level 0 The /op command has been added but needs players to rejoin for now. * Clippy Fix + need to Rejoin after /op removed I found the source of the DeadLock. Updated set permission function. Fix cargo formatting issues and clippy issues. * Remove temp warn message * Move OperatorConfig to server As Snowiii pointed out, OperatorConfig is runtime data. Revert most changes in pumpkin-config. op_permission_level must say in the basic configuration for parity with Minecraft. * cargo fmt + cargo clippy * Sync permission change with client * Fixs issues @Commandcracker found in review - Move PermissionLvl to core and removed OpLevel - Move ops.json to /data/ops.json - Fix permissions issue with /op command - Shorten /op command description * refactor into `data` folder * create data dir when needed * add to readme * fix: conflicts --------- Co-authored-by: Alexander Medvedev --- README.md | 1 + docker-compose.yml | 4 +- pumpkin-config/Cargo.toml | 1 + pumpkin-config/src/lib.rs | 6 +- pumpkin-config/src/op.rs | 27 ++++++ pumpkin-core/src/lib.rs | 2 + pumpkin-core/src/permission.rs | 56 +++++++++++++ pumpkin/src/command/commands/cmd_fill.rs | 3 +- pumpkin/src/command/commands/cmd_gamemode.rs | 2 +- pumpkin/src/command/commands/cmd_give.rs | 2 +- pumpkin/src/command/commands/cmd_kick.rs | 1 + pumpkin/src/command/commands/cmd_op.rs | 80 ++++++++++++++++++ pumpkin/src/command/commands/cmd_say.rs | 14 ++-- pumpkin/src/command/commands/cmd_seed.rs | 2 +- pumpkin/src/command/commands/cmd_setblock.rs | 2 +- pumpkin/src/command/commands/cmd_stop.rs | 2 +- pumpkin/src/command/commands/cmd_teleport.rs | 2 +- pumpkin/src/command/commands/cmd_time.rs | 10 +-- pumpkin/src/command/commands/cmd_transfer.rs | 2 +- pumpkin/src/command/commands/mod.rs | 1 + pumpkin/src/command/mod.rs | 9 +- pumpkin/src/data/mod.rs | 88 ++++++++++++++++++++ pumpkin/src/data/op_data.rs | 26 ++++++ pumpkin/src/entity/player.rs | 62 +++++++------- pumpkin/src/main.rs | 1 + 25 files changed, 346 insertions(+), 60 deletions(-) create mode 100644 pumpkin-config/src/op.rs create mode 100644 pumpkin-core/src/permission.rs create mode 100644 pumpkin/src/command/commands/cmd_op.rs create mode 100644 pumpkin/src/data/mod.rs create mode 100644 pumpkin/src/data/op_data.rs diff --git a/README.md b/README.md index f71139300..e62595150 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [x] Particles - [x] Chat - [x] Commands + - [x] OP Permission - Proxy - [x] Bungeecord - [x] Velocity diff --git a/docker-compose.yml b/docker-compose.yml index a8c019a08..418bdac0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ services: pumpkin: build: . ports: - - 25565:25565 + - "25565:25565" volumes: - ./data:/pumpkin + stdin_open: true + tty: true diff --git a/pumpkin-config/Cargo.toml b/pumpkin-config/Cargo.toml index d3ce22e43..c9bf9ba85 100644 --- a/pumpkin-config/Cargo.toml +++ b/pumpkin-config/Cargo.toml @@ -7,5 +7,6 @@ edition.workspace = true pumpkin-core = { path = "../pumpkin-core" } serde.workspace = true log.workspace = true +uuid.workspace = true toml = "0.8" diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index 5e82ae334..94feda0b8 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -1,6 +1,6 @@ use log::warn; use logging::LoggingConfig; -use pumpkin_core::{Difficulty, GameMode}; +use pumpkin_core::{Difficulty, GameMode, PermissionLvl}; use query::QueryConfig; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -29,6 +29,7 @@ pub use server_links::ServerLinksConfig; mod commands; pub mod compression; mod lan_broadcast; +pub mod op; mod pvp; mod rcon; mod server_links; @@ -79,6 +80,8 @@ pub struct BasicConfiguration { pub simulation_distance: NonZeroU8, /// The default game difficulty. pub default_difficulty: Difficulty, + /// The op level assign by the /op command + pub op_permission_level: PermissionLvl, /// Whether the Nether dimension is enabled. pub allow_nether: bool, /// Whether the server is in hardcore mode. @@ -109,6 +112,7 @@ impl Default for BasicConfiguration { view_distance: NonZeroU8::new(10).unwrap(), simulation_distance: NonZeroU8::new(10).unwrap(), default_difficulty: Difficulty::Normal, + op_permission_level: PermissionLvl::Four, allow_nether: true, hardcore: false, online_mode: true, diff --git a/pumpkin-config/src/op.rs b/pumpkin-config/src/op.rs new file mode 100644 index 000000000..edbd71bca --- /dev/null +++ b/pumpkin-config/src/op.rs @@ -0,0 +1,27 @@ +use pumpkin_core::permission::PermissionLvl; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct Op { + pub uuid: Uuid, + pub name: String, + pub level: PermissionLvl, + pub bypasses_player_limit: bool, +} + +impl Op { + pub fn new( + uuid: Uuid, + name: String, + level: PermissionLvl, + bypasses_player_limit: bool, + ) -> Self { + Self { + uuid, + name, + level, + bypasses_player_limit, + } + } +} diff --git a/pumpkin-core/src/lib.rs b/pumpkin-core/src/lib.rs index 8ffd25b29..8116211b1 100644 --- a/pumpkin-core/src/lib.rs +++ b/pumpkin-core/src/lib.rs @@ -1,9 +1,11 @@ pub mod gamemode; pub mod math; +pub mod permission; pub mod random; pub mod text; pub use gamemode::GameMode; +pub use permission::PermissionLvl; use serde::{Deserialize, Serialize}; diff --git a/pumpkin-core/src/permission.rs b/pumpkin-core/src/permission.rs new file mode 100644 index 000000000..7229cbcb0 --- /dev/null +++ b/pumpkin-core/src/permission.rs @@ -0,0 +1,56 @@ +use num_derive::{FromPrimitive, ToPrimitive}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Represents the player's permission level +/// +/// Permission levels determine the player's access to commands and server operations. +/// Each numeric level corresponds to a specific role: +/// - `Zero`: `normal`: Player can use basic commands. +/// - `One`: `moderator`: Player can bypass spawn protection. +/// - `Two`: `gamemaster`: Player or executor can use more commands and player can use command blocks. +/// - `Three`: `admin`: Player or executor can use commands related to multiplayer management. +/// - `Four`: `owner`: Player or executor can use all of the commands, including commands related to server management. +#[derive(FromPrimitive, ToPrimitive, Clone, Copy, Default, PartialEq, Eq)] +#[repr(i8)] +pub enum PermissionLvl { + #[default] + Zero = 0, + One = 1, + Two = 2, + Three = 3, + Four = 4, +} + +impl PartialOrd for PermissionLvl { + fn partial_cmp(&self, other: &Self) -> Option { + (*self as u8).partial_cmp(&(*other as u8)) + } +} + +impl Serialize for PermissionLvl { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_u8(*self as u8) + } +} + +impl<'de> Deserialize<'de> for PermissionLvl { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + match value { + 0 => Ok(PermissionLvl::Zero), + 2 => Ok(PermissionLvl::Two), + 3 => Ok(PermissionLvl::Three), + 4 => Ok(PermissionLvl::Four), + _ => Err(serde::de::Error::custom(format!( + "Invalid value for OpLevel: {}", + value + ))), + } + } +} diff --git a/pumpkin/src/command/commands/cmd_fill.rs b/pumpkin/src/command/commands/cmd_fill.rs index 140e87299..4f5e3d06a 100644 --- a/pumpkin/src/command/commands/cmd_fill.rs +++ b/pumpkin/src/command/commands/cmd_fill.rs @@ -4,10 +4,11 @@ use crate::command::args::{ConsumedArgs, FindArg}; use crate::command::tree::CommandTree; use crate::command::tree_builder::{argument, literal, require}; use crate::command::{CommandError, CommandExecutor, CommandSender}; -use crate::entity::player::PermissionLvl; + use async_trait::async_trait; use pumpkin_core::math::position::WorldPosition; use pumpkin_core::math::vector3::Vector3; +use pumpkin_core::permission::PermissionLvl; use pumpkin_core::text::TextComponent; const NAMES: [&str; 1] = ["fill"]; diff --git a/pumpkin/src/command/commands/cmd_gamemode.rs b/pumpkin/src/command/commands/cmd_gamemode.rs index 2b6278628..1b586130c 100644 --- a/pumpkin/src/command/commands/cmd_gamemode.rs +++ b/pumpkin/src/command/commands/cmd_gamemode.rs @@ -3,8 +3,8 @@ use async_trait::async_trait; use crate::command::args::arg_gamemode::GamemodeArgumentConsumer; use crate::command::args::GetCloned; -use crate::entity::player::PermissionLvl; use crate::TextComponent; +use pumpkin_core::permission::PermissionLvl; use crate::command::args::arg_players::PlayersArgumentConsumer; diff --git a/pumpkin/src/command/commands/cmd_give.rs b/pumpkin/src/command/commands/cmd_give.rs index cdc3334b0..d3f91bafa 100644 --- a/pumpkin/src/command/commands/cmd_give.rs +++ b/pumpkin/src/command/commands/cmd_give.rs @@ -9,7 +9,7 @@ use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName}; use crate::command::tree::CommandTree; use crate::command::tree_builder::{argument, argument_default_name, require}; use crate::command::{CommandError, CommandExecutor, CommandSender}; -use crate::entity::player::PermissionLvl; +use pumpkin_core::permission::PermissionLvl; const NAMES: [&str; 1] = ["give"]; diff --git a/pumpkin/src/command/commands/cmd_kick.rs b/pumpkin/src/command/commands/cmd_kick.rs index 8d77bf3df..51832258b 100644 --- a/pumpkin/src/command/commands/cmd_kick.rs +++ b/pumpkin/src/command/commands/cmd_kick.rs @@ -49,6 +49,7 @@ impl CommandExecutor for KickExecutor { } } +// TODO: Permission pub fn init_command_tree() -> CommandTree { CommandTree::new(NAMES, DESCRIPTION) .with_child(argument(ARG_TARGET, PlayersArgumentConsumer).execute(KickExecutor)) diff --git a/pumpkin/src/command/commands/cmd_op.rs b/pumpkin/src/command/commands/cmd_op.rs new file mode 100644 index 000000000..614ffcc7d --- /dev/null +++ b/pumpkin/src/command/commands/cmd_op.rs @@ -0,0 +1,80 @@ +use crate::{ + command::{ + args::{arg_players::PlayersArgumentConsumer, Arg, ConsumedArgs}, + tree::CommandTree, + tree_builder::{argument, require}, + CommandError, CommandExecutor, CommandSender, + }, + data::{op_data::OPERATOR_CONFIG, SaveJSONConfiguration}, +}; +use async_trait::async_trait; +use pumpkin_config::{op::Op, BASIC_CONFIG}; +use pumpkin_core::permission::PermissionLvl; +use pumpkin_core::text::TextComponent; +use CommandError::InvalidConsumption; + +const NAMES: [&str; 1] = ["op"]; +const DESCRIPTION: &str = "Grants operator status to a player."; +const ARG_TARGET: &str = "player"; + +struct OpExecutor; + +#[async_trait] +impl CommandExecutor for OpExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let mut config = OPERATOR_CONFIG.write().await; + + let Some(Arg::Players(targets)) = args.get(&ARG_TARGET) else { + return Err(InvalidConsumption(Some(ARG_TARGET.into()))); + }; + + // log each player to the console. + for player in targets { + let new_level = if BASIC_CONFIG.op_permission_level > sender.permission_lvl() { + sender.permission_lvl() + } else { + BASIC_CONFIG.op_permission_level + }; + + let op_entry = Op::new( + player.gameprofile.id, + player.gameprofile.name.clone(), + new_level, + false, + ); + if let Some(op) = config + .ops + .iter_mut() + .find(|o| o.uuid == player.gameprofile.id) + { + op.level = new_level; + } else { + config.ops.push(op_entry); + } + config.save(); + + player + .set_permission_lvl(new_level, &server.command_dispatcher) + .await; + + let player_name = player.gameprofile.name.clone(); + let message = format!("Made {player_name} a server operator."); + let msg = TextComponent::text(&message); + sender.send_message(msg).await; + } + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).with_child( + require(|sender| sender.has_permission_lvl(PermissionLvl::Three)) + .with_child(argument(ARG_TARGET, PlayersArgumentConsumer).execute(OpExecutor)), + ) +} diff --git a/pumpkin/src/command/commands/cmd_say.rs b/pumpkin/src/command/commands/cmd_say.rs index fad4b64b6..ffeb9c884 100644 --- a/pumpkin/src/command/commands/cmd_say.rs +++ b/pumpkin/src/command/commands/cmd_say.rs @@ -2,15 +2,13 @@ use async_trait::async_trait; use pumpkin_core::text::TextComponent; use pumpkin_protocol::client::play::CSystemChatMessage; -use crate::{ - command::{ - args::{arg_message::MsgArgConsumer, Arg, ConsumedArgs}, - tree::CommandTree, - tree_builder::{argument, require}, - CommandError, CommandExecutor, CommandSender, - }, - entity::player::PermissionLvl, +use crate::command::{ + args::{arg_message::MsgArgConsumer, Arg, ConsumedArgs}, + tree::CommandTree, + tree_builder::{argument, require}, + CommandError, CommandExecutor, CommandSender, }; +use pumpkin_core::permission::PermissionLvl; use CommandError::InvalidConsumption; const NAMES: [&str; 1] = ["say"]; diff --git a/pumpkin/src/command/commands/cmd_seed.rs b/pumpkin/src/command/commands/cmd_seed.rs index c4ffc3752..50fe6ecd8 100644 --- a/pumpkin/src/command/commands/cmd_seed.rs +++ b/pumpkin/src/command/commands/cmd_seed.rs @@ -2,8 +2,8 @@ use crate::command::tree_builder::require; use crate::command::{ args::ConsumedArgs, tree::CommandTree, CommandError, CommandExecutor, CommandSender, }; -use crate::entity::player::PermissionLvl; use async_trait::async_trait; +use pumpkin_core::permission::PermissionLvl; use pumpkin_core::text::click::ClickEvent; use pumpkin_core::text::hover::HoverEvent; use pumpkin_core::text::{color::NamedColor, TextComponent}; diff --git a/pumpkin/src/command/commands/cmd_setblock.rs b/pumpkin/src/command/commands/cmd_setblock.rs index 8b7cf9739..82e813924 100644 --- a/pumpkin/src/command/commands/cmd_setblock.rs +++ b/pumpkin/src/command/commands/cmd_setblock.rs @@ -8,7 +8,7 @@ use crate::command::args::{ConsumedArgs, FindArg}; use crate::command::tree::CommandTree; use crate::command::tree_builder::{argument, literal, require}; use crate::command::{CommandError, CommandExecutor, CommandSender}; -use crate::entity::player::PermissionLvl; +use pumpkin_core::permission::PermissionLvl; const NAMES: [&str; 1] = ["setblock"]; diff --git a/pumpkin/src/command/commands/cmd_stop.rs b/pumpkin/src/command/commands/cmd_stop.rs index 9e3ada0b8..6368988c1 100644 --- a/pumpkin/src/command/commands/cmd_stop.rs +++ b/pumpkin/src/command/commands/cmd_stop.rs @@ -6,7 +6,7 @@ use crate::command::args::ConsumedArgs; use crate::command::tree::CommandTree; use crate::command::tree_builder::require; use crate::command::{CommandError, CommandExecutor, CommandSender}; -use crate::entity::player::PermissionLvl; +use pumpkin_core::permission::PermissionLvl; const NAMES: [&str; 1] = ["stop"]; diff --git a/pumpkin/src/command/commands/cmd_teleport.rs b/pumpkin/src/command/commands/cmd_teleport.rs index d94681473..59ccc9606 100644 --- a/pumpkin/src/command/commands/cmd_teleport.rs +++ b/pumpkin/src/command/commands/cmd_teleport.rs @@ -12,7 +12,7 @@ use crate::command::tree::CommandTree; use crate::command::tree_builder::{argument, literal, require}; use crate::command::CommandError; use crate::command::{CommandExecutor, CommandSender}; -use crate::entity::player::PermissionLvl; +use pumpkin_core::permission::PermissionLvl; const NAMES: [&str; 2] = ["teleport", "tp"]; const DESCRIPTION: &str = "Teleports entities, including players."; // todo diff --git a/pumpkin/src/command/commands/cmd_time.rs b/pumpkin/src/command/commands/cmd_time.rs index 669a32940..9cc45ec67 100644 --- a/pumpkin/src/command/commands/cmd_time.rs +++ b/pumpkin/src/command/commands/cmd_time.rs @@ -5,13 +5,11 @@ use pumpkin_core::text::TextComponent; use crate::command::args::arg_bounded_num::BoundedNumArgumentConsumer; use crate::command::args::FindArgDefaultName; use crate::command::tree_builder::{argument_default_name, literal}; -use crate::{ - command::{ - tree::CommandTree, tree_builder::require, CommandError, CommandExecutor, CommandSender, - ConsumedArgs, - }, - entity::player::PermissionLvl, +use crate::command::{ + tree::CommandTree, tree_builder::require, CommandError, CommandExecutor, CommandSender, + ConsumedArgs, }; +use pumpkin_core::permission::PermissionLvl; const NAMES: [&str; 1] = ["time"]; diff --git a/pumpkin/src/command/commands/cmd_transfer.rs b/pumpkin/src/command/commands/cmd_transfer.rs index e35b0329a..8aa412511 100644 --- a/pumpkin/src/command/commands/cmd_transfer.rs +++ b/pumpkin/src/command/commands/cmd_transfer.rs @@ -13,7 +13,7 @@ use crate::command::tree_builder::{argument, argument_default_name, require}; use crate::command::{ args::ConsumedArgs, tree::CommandTree, CommandError, CommandExecutor, CommandSender, }; -use crate::entity::player::PermissionLvl; +use pumpkin_core::permission::PermissionLvl; const NAMES: [&str; 1] = ["transfer"]; diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 6ea8c01a0..5b4635937 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -7,6 +7,7 @@ pub mod cmd_help; pub mod cmd_kick; pub mod cmd_kill; pub mod cmd_list; +pub mod cmd_op; pub mod cmd_pumpkin; pub mod cmd_say; pub mod cmd_seed; diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 11eea1974..c05017ed5 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -4,17 +4,19 @@ use std::sync::Arc; use crate::command::commands::cmd_seed; use crate::command::commands::{cmd_bossbar, cmd_transfer}; use crate::command::dispatcher::CommandDispatcher; -use crate::entity::player::{PermissionLvl, Player}; +use crate::entity::player::Player; use crate::server::Server; use crate::world::World; use args::ConsumedArgs; use async_trait::async_trait; +use commands::cmd_op; use commands::{ cmd_clear, cmd_fill, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list, cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, cmd_time, cmd_worldborder, }; use dispatcher::CommandError; use pumpkin_core::math::vector3::Vector3; +use pumpkin_core::permission::PermissionLvl; use pumpkin_core::text::TextComponent; pub mod args; @@ -76,7 +78,7 @@ impl<'a> CommandSender<'a> { pub fn permission_lvl(&self) -> PermissionLvl { match self { CommandSender::Console | CommandSender::Rcon(_) => PermissionLvl::Four, - CommandSender::Player(p) => p.permission_lvl(), + CommandSender::Player(p) => p.permission_lvl.load(), } } @@ -84,7 +86,7 @@ impl<'a> CommandSender<'a> { pub fn has_permission_lvl(&self, lvl: PermissionLvl) -> bool { match self { CommandSender::Console | CommandSender::Rcon(_) => true, - CommandSender::Player(p) => (p.permission_lvl() as i8) >= (lvl as i8), + CommandSender::Player(p) => p.permission_lvl.load().ge(&lvl), } } @@ -128,6 +130,7 @@ pub fn default_dispatcher() -> CommandDispatcher { dispatcher.register(cmd_seed::init_command_tree()); dispatcher.register(cmd_transfer::init_command_tree()); dispatcher.register(cmd_fill::init_command_tree()); + dispatcher.register(cmd_op::init_command_tree()); dispatcher } diff --git a/pumpkin/src/data/mod.rs b/pumpkin/src/data/mod.rs new file mode 100644 index 000000000..7faf2d16f --- /dev/null +++ b/pumpkin/src/data/mod.rs @@ -0,0 +1,88 @@ +use std::{env, fs, path::Path}; + +use serde::{Deserialize, Serialize}; + +const DATA_FOLDER: &str = "data/"; + +pub mod op_data; + +pub trait LoadJSONConfiguration { + #[must_use] + fn load() -> Self + where + Self: Sized + Default + Serialize + for<'de> Deserialize<'de>, + { + let exe_dir = env::current_dir().unwrap(); + let data_dir = exe_dir.join(DATA_FOLDER); + if !data_dir.exists() { + log::debug!("creating new data root folder"); + fs::create_dir(&data_dir).expect("Failed to create data root folder"); + } + let path = data_dir.join(Self::get_path()); + + let config = if path.exists() { + let file_content = fs::read_to_string(&path) + .unwrap_or_else(|_| panic!("Couldn't read configuration file at {path:?}")); + + serde_json::from_str(&file_content).unwrap_or_else(|err| { + panic!( + "Couldn't parse data config at {path:?}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.", + ) + }) + } else { + let content = Self::default(); + + if let Err(err) = fs::write(&path, serde_json::to_string_pretty(&content).unwrap()) { + log::error!( + "Couldn't write default data config to {path:?}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.", + ); + } + + content + }; + + config.validate(); + config + } + + fn get_path() -> &'static Path; + + fn validate(&self); +} + +pub trait SaveJSONConfiguration: LoadJSONConfiguration { + // suppress clippy warning + + fn save(&self) + where + Self: Sized + Default + Serialize + for<'de> Deserialize<'de>, + { + let exe_dir = env::current_dir().unwrap(); + let data_dir = exe_dir.join(DATA_FOLDER); + if !data_dir.exists() { + log::debug!("creating new data root folder"); + fs::create_dir(&data_dir).expect("Failed to create data root folder"); + } + let path = data_dir.join(Self::get_path()); + + let content = match serde_json::to_string_pretty(self) { + Ok(content) => content, + Err(err) => { + log::warn!( + "Couldn't serialize operator data config to {:?}. Reason: {}", + path, + err + ); + return; + } + }; + + if let Err(err) = std::fs::write(&path, content) { + log::warn!( + "Couldn't write operator config to {:?}. Reason: {}", + path, + err + ); + } + } +} diff --git a/pumpkin/src/data/op_data.rs b/pumpkin/src/data/op_data.rs new file mode 100644 index 000000000..c1dada051 --- /dev/null +++ b/pumpkin/src/data/op_data.rs @@ -0,0 +1,26 @@ +use std::{path::Path, sync::LazyLock}; + +use pumpkin_config::op; +use serde::{Deserialize, Serialize}; + +use super::{LoadJSONConfiguration, SaveJSONConfiguration}; + +pub static OPERATOR_CONFIG: LazyLock> = + LazyLock::new(|| tokio::sync::RwLock::new(OperatorConfig::load())); + +#[derive(Deserialize, Serialize, Default)] +#[serde(transparent)] +pub struct OperatorConfig { + pub ops: Vec, +} + +impl LoadJSONConfiguration for OperatorConfig { + fn get_path() -> &'static Path { + Path::new("ops.json") + } + fn validate(&self) { + // TODO: Validate the operator configuration + } +} + +impl SaveJSONConfiguration for OperatorConfig {} diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index dbc84e74d..eae83b3ed 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -8,7 +8,7 @@ use std::{ }; use crossbeam::atomic::AtomicCell; -use num_derive::{FromPrimitive, ToPrimitive}; +use num_derive::FromPrimitive; use num_traits::Pow; use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG}; use pumpkin_core::{ @@ -18,6 +18,7 @@ use pumpkin_core::{ vector2::Vector2, vector3::Vector3, }, + permission::PermissionLvl, text::TextComponent, GameMode, }; @@ -54,11 +55,12 @@ use pumpkin_world::{ ItemStack, }, }; -use tokio::sync::{Mutex, Notify}; +use tokio::sync::{Mutex, Notify, RwLock}; use super::Entity; -use crate::{error::PumpkinError, net::GameProfile}; use crate::{ + command::{client_cmd_suggestions, dispatcher::CommandDispatcher}, + data::op_data::OPERATOR_CONFIG, net::{ combat::{self, player_attack_sound, AttackType}, Client, PlayerConfig, @@ -66,6 +68,7 @@ use crate::{ server::Server, world::World, }; +use crate::{error::PumpkinError, net::GameProfile}; use super::living::LivingEntity; @@ -116,12 +119,10 @@ pub struct Player { pub last_keep_alive_time: AtomicCell, /// Amount of ticks since last attack pub last_attacked_ticks: AtomicU32, - + /// The players op permission level + pub permission_lvl: AtomicCell, /// Tell tasks to stop if we are closing cancel_tasks: Notify, - - /// the players op permission level - permission_lvl: PermissionLvl, } impl Player { @@ -143,6 +144,8 @@ impl Player { }, |profile| profile, ); + + let gameprofile_clone = gameprofile.clone(); let config = client.config.lock().await.clone().unwrap_or_default(); let bounding_box_size = BoundingBoxSize { width: 0.6, @@ -186,8 +189,17 @@ impl Player { last_keep_alive_time: AtomicCell::new(std::time::Instant::now()), last_attacked_ticks: AtomicU32::new(0), cancel_tasks: Notify::new(), - // TODO: change this - permission_lvl: PermissionLvl::Four, + // Minecraft has no why to change the default permission level of new players. + // Minecrafts default permission level is 0 + permission_lvl: OPERATOR_CONFIG + .read() + .await + .ops + .iter() + .find(|op| op.uuid == gameprofile_clone.id) + .map_or(AtomicCell::new(PermissionLvl::Zero), |op| { + AtomicCell::new(op.level) + }), } } @@ -431,20 +443,20 @@ impl Player { self.client .send_packet(&CEntityStatus::new( self.entity_id(), - 24 + self.permission_lvl as i8, + 24 + self.permission_lvl.load() as i8, )) .await; } /// sets the players permission level and syncs it with the client - pub async fn set_permission_lvl(&mut self, lvl: PermissionLvl) { - self.permission_lvl = lvl; + pub async fn set_permission_lvl( + self: &Arc, + lvl: PermissionLvl, + command_dispatcher: &RwLock, + ) { + self.permission_lvl.store(lvl); self.send_permission_lvl_update().await; - } - - /// get the players permission level - pub fn permission_lvl(&self) -> PermissionLvl { - self.permission_lvl + client_cmd_suggestions::send_c_commands_packet(self, command_dispatcher).await; } /// Sends the world time to just the player. @@ -821,19 +833,3 @@ pub enum ChatMode { /// All messages should be hidden Hidden, } - -/// the player's permission level -#[derive(Debug, FromPrimitive, ToPrimitive, Clone, Copy)] -#[repr(i8)] -pub enum PermissionLvl { - /// `normal`: Player can use basic commands. - Zero = 0, - /// `moderator`: Player can bypass spawn protection. - One = 1, - /// `gamemaster`: Player or executor can use more commands and player can use command blocks. - Two = 2, - /// `admin`: Player or executor can use commands related to multiplayer management. - Three = 3, - /// `owner`: Player or executor can use all of the commands, including commands related to server management. - Four = 4, -} diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index 5009e96f3..d0dbe4019 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -55,6 +55,7 @@ use std::time::Instant; pub mod block; pub mod command; +pub mod data; pub mod entity; pub mod error; pub mod net;