diff --git a/README.md b/README.md index 7e4df0ef..e6259515 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) @@ -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 @@ -87,7 +88,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 +96,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 diff --git a/docker-compose.yml b/docker-compose.yml index a8c019a0..418bdac0 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 d3ce22e4..c9bf9ba8 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 5e82ae33..94feda0b 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 00000000..edbd71bc --- /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 8ffd25b2..8116211b 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/math/position.rs b/pumpkin-core/src/math/position.rs index 83989064..a3b82cbe 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-core/src/permission.rs b/pumpkin-core/src/permission.rs new file mode 100644 index 00000000..7229cbcb --- /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-core/src/text/click.rs b/pumpkin-core/src/text/click.rs index 0e917e15..caa78f61 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 282ac129..e065722f 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-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index bf56dca6..f1273b05 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 ec68578f..3025ed92 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-nbt/src/deserializer.rs b/pumpkin-nbt/src/deserializer.rs index ae017b82..80e3b96f 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 b2be0d65..5ed48da9 100644 --- a/pumpkin-nbt/src/lib.rs +++ b/pumpkin-nbt/src/lib.rs @@ -200,3 +200,62 @@ 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 c167df95..f04d7731 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-world/Cargo.toml b/pumpkin-world/Cargo.toml index 495ee3ff..1c7eacd8 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 930363ec..ede2d616 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 8e26b106..ec23b151 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 00000000..7864cf71 --- /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 00000000..bf4b262f --- /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, +} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index a3f4a14e..87b03020 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", diff --git a/pumpkin/src/block/block_manager.rs b/pumpkin/src/block/block_manager.rs index 92395bb8..0fb26ede 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 470c4610..1be42eca 100644 --- a/pumpkin/src/block/blocks/chest.rs +++ b/pumpkin/src/block/blocks/chest.rs @@ -3,7 +3,10 @@ use pumpkin_core::math::position::WorldPosition; use pumpkin_inventory::{Chest, OpenContainer, WindowType}; use pumpkin_macros::{pumpkin_block, sound}; use pumpkin_protocol::{client::play::CBlockAction, codec::var_int::VarInt}; -use pumpkin_world::{block::block_registry::get_block, item::item_registry::Item}; +use pumpkin_world::{ + block::block_registry::{get_block, Block}, + item::item_registry::Item, +}; use crate::{ block::{block_manager::BlockActionResult, pumpkin_block::PumpkinBlock}, @@ -16,8 +19,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) @@ -26,16 +36,35 @@ 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) @@ -52,27 +81,20 @@ impl PumpkinBlock for ChestBlock { 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; if let Some(e) = get_block("minecraft:chest").cloned() { server diff --git a/pumpkin/src/block/blocks/crafting_table.rs b/pumpkin/src/block/blocks/crafting_table.rs index 962d5662..01ad4179 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 8fc47949..1ecdf995 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 3c52395b..4836c4be 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 4bdfa819..b0b12802 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 1849ea5a..c0a65c31 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/command/commands/cmd_fill.rs b/pumpkin/src/command/commands/cmd_fill.rs index 140e8729..4f5e3d06 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 2b627862..1b586130 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 cdc3334b..d3f91baf 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 8d77bf3d..51832258 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 00000000..614ffcc7 --- /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 fad4b64b..ffeb9c88 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 c4ffc375..50fe6ecd 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 8b7cf973..82e81392 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 9e3ada0b..6368988c 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 d9468147..59ccc960 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 669a3294..9cc45ec6 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 e35b0329..8aa41251 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 6ea8c01a..5b463593 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 11eea197..c05017ed 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 00000000..7faf2d16 --- /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 00000000..c1dada05 --- /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 dbc84e74..eae83b3e 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 5009e96f..d0dbe401 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; diff --git a/pumpkin/src/net/container.rs b/pumpkin/src/net/container.rs index 9e06a46d..c157b5e2 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 ec01ee04..baf90465 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 2ae5804f..96090ae8 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() }