diff --git a/Cargo.toml b/Cargo.toml index 79767e8e3..d32f8e1a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ rayon = "1.10.0" parking_lot = "0.12.3" crossbeam = "0.8.4" -uuid = { version = "1.10.0", features = ["serde", "v3", "v4"] } +uuid = { version = "1.11.0", features = ["serde", "v3", "v4"] } derive_more = { version = "1.0.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index b5f78af75..106d8426e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - **Performance**: Leveraging multi-threading for maximum speed and efficiency. - **Compatibility**: Supports the latest Minecraft server version and adheres to vanilla game mechanics. -- **Security**: Prioritizes security by preventing known exploits. +- **Security**: Prioritizes security by preventing known security exploits. - **Flexibility**: Highly configurable, with the ability to disable unnecessary features. - **Extensibility**: Provides a foundation for plugin development. @@ -48,6 +48,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [x] Entity Spawning - [x] Chunk Loading - [x] Chunk Generation + - [x] Scoreboard - [ ] World Borders - [ ] World Saving - Player diff --git a/pumpkin/icon.png b/assets/default_icon.png similarity index 100% rename from pumpkin/icon.png rename to assets/default_icon.png diff --git a/docs/about/introduction.md b/docs/about/introduction.md index 8f4c216b2..11703d96c 100644 --- a/docs/about/introduction.md +++ b/docs/about/introduction.md @@ -9,7 +9,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - **Performance**: Leveraging multi-threading for maximum speed and efficiency. - **Compatibility**: Supports the latest Minecraft server version and adheres to vanilla game mechanics. -- **Security**: Prioritizes security by preventing known exploits. +- **Security**: Prioritizes security by preventing known security exploits. - **Flexibility**: Highly configurable, with the ability to disable unnecessary features. - **Extensibility**: Provides a foundation for plugin development. @@ -20,3 +20,13 @@ and customizable experience. It prioritizes performance and player enjoyment whi > [!IMPORTANT] > Pumpkin is currently under heavy development. Check out our [Github Project](https://github.com/users/Snowiiii/projects/12/views/3) to see current progress + +## Vanilla + +Pumpkin is designed to replicate vanilla Minecraft logic as closely as possible, +ensuring a familiar gameplay experience. However, we've also added new features to enhance your gameplay. + +### Redstone + +Unlike other forks that may compromise vanilla redstone mechanics, Pumpkin maintains the original redstone behavior. +If you're looking to experiment with redstone modifications or seek performance optimizations, you have the flexibility to do so through our configurable settings. diff --git a/docs/config/basic.md b/docs/config/basic.md index 7d646c33c..7eda8546c 100644 --- a/docs/config/basic.md +++ b/docs/config/basic.md @@ -100,6 +100,22 @@ The server's description displayed on the status screen. motd=true ``` +## Use favicon + +Whether to use a server favicon or not + +```toml +use_favicon=true +``` + +## Favicon path + +The path to the server's favicon + +```toml +favicon_path=./icon.png +``` + ## Default gamemode The default game mode for players diff --git a/docs/troubleshooting/common_issues.md b/docs/troubleshooting/common_issues.md index 57c513a48..98786f7fa 100644 --- a/docs/troubleshooting/common_issues.md +++ b/docs/troubleshooting/common_issues.md @@ -18,7 +18,7 @@ **Cause:** The server is currently not calculating hit boxes for blocks, we're working on that. -3. The Server is unresponsive +3. ### The Server is unresponsive **Issue:** You have to wait before reconnecting or can't do basic things while chunks are loading. diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index 498218640..ebe9c99cf 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -97,6 +97,12 @@ pub struct BasicConfiguration { /// Whether to remove IPs from logs or not #[serde_inline_default(true)] pub scrub_ips: bool, + /// Whether to use a server favicon + #[serde_inline_default(true)] + pub use_favicon: bool, + /// Path to server favicon + #[serde_inline_default("icon.png".to_string())] + pub favicon_path: String, } fn default_server_address() -> SocketAddr { @@ -119,6 +125,8 @@ impl Default for BasicConfiguration { motd: "A Blazing fast Pumpkin Server!".to_string(), default_gamemode: GameMode::Survival, scrub_ips: true, + use_favicon: true, + favicon_path: "icon.png".to_string(), } } } diff --git a/pumpkin-protocol/src/client/play/c_display_objective.rs b/pumpkin-protocol/src/client/play/c_display_objective.rs new file mode 100644 index 000000000..a5181c088 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_display_objective.rs @@ -0,0 +1,43 @@ +use pumpkin_macros::packet; +use serde::Serialize; + +use crate::VarInt; + +#[derive(Serialize)] +#[packet(0x57)] +pub struct CDisplayObjective<'a> { + position: VarInt, + score_name: &'a str, +} + +impl<'a> CDisplayObjective<'a> { + pub fn new(position: DisplaySlot, score_name: &'a str) -> Self { + Self { + position: VarInt(position as i32), + score_name, + } + } +} + +#[repr(i32)] +pub enum DisplaySlot { + List, + Sidebar, + BelowName, + TeamBlack, + TeamDarkBlue, + TeamDarkGreen, + TeanDarkAqua, + TeamDarkRed, + TeamDarkPurple, + TeamGold, + TeamGray, + TeamDarkGray, + TeamBlue, + TeamGreen, + TeamAqua, + TeamRed, + TeamLightPurple, + TeamYellow, + TeamWhite, +} diff --git a/pumpkin-protocol/src/client/play/c_reset_score.rs b/pumpkin-protocol/src/client/play/c_reset_score.rs new file mode 100644 index 000000000..1dcfbc8b6 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_reset_score.rs @@ -0,0 +1,18 @@ +use pumpkin_macros::packet; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(0x44)] +pub struct CResetScore { + entity_name: String, + objective_name: Option, +} + +impl CResetScore { + pub fn new(entity_name: String, objective_name: Option) -> Self { + Self { + entity_name, + objective_name, + } + } +} diff --git a/pumpkin-protocol/src/client/play/c_system_chat_message.rs b/pumpkin-protocol/src/client/play/c_system_chat_message.rs index 2751b40c7..07c627178 100644 --- a/pumpkin-protocol/src/client/play/c_system_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_system_chat_message.rs @@ -5,12 +5,12 @@ use serde::Serialize; #[derive(Serialize)] #[packet(0x6C)] pub struct CSystemChatMessage<'a> { - content: TextComponent<'a>, + content: &'a TextComponent<'a>, overlay: bool, } impl<'a> CSystemChatMessage<'a> { - pub fn new(content: TextComponent<'a>, overlay: bool) -> Self { + pub fn new(content: &'a TextComponent<'a>, overlay: bool) -> Self { Self { content, overlay } } } diff --git a/pumpkin-protocol/src/client/play/c_update_objectives.rs b/pumpkin-protocol/src/client/play/c_update_objectives.rs new file mode 100644 index 000000000..a43500e33 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_update_objectives.rs @@ -0,0 +1,71 @@ +use pumpkin_core::text::TextComponent; +use pumpkin_macros::packet; + +use crate::{ClientPacket, NumberFormat, VarInt}; + +#[packet(0x5E)] +pub struct CUpdateObjectives<'a> { + objective_name: &'a str, + mode: u8, + display_name: TextComponent<'a>, + render_type: VarInt, + number_format: Option>, +} + +impl<'a> CUpdateObjectives<'a> { + pub fn new( + objective_name: &'a str, + mode: Mode, + display_name: TextComponent<'a>, + render_type: RenderType, + number_format: Option>, + ) -> Self { + Self { + objective_name, + mode: mode as u8, + display_name, + render_type: VarInt(render_type as i32), + number_format, + } + } +} + +impl<'a> ClientPacket for CUpdateObjectives<'a> { + fn write(&self, bytebuf: &mut crate::bytebuf::ByteBuffer) { + bytebuf.put_string(self.objective_name); + bytebuf.put_u8(self.mode); + if self.mode == 0 || self.mode == 2 { + bytebuf.put_slice(&self.display_name.encode()); + bytebuf.put_var_int(&self.render_type); + bytebuf.put_option(&self.number_format, |p, v| { + match v { + NumberFormat::Blank => { + p.put_var_int(&VarInt(0)); + } + NumberFormat::Styled(style) => { + p.put_var_int(&VarInt(1)); + // TODO + p.put_slice(&fastnbt::to_bytes(style).unwrap()); + } + NumberFormat::Fixed(text_component) => { + p.put_var_int(&VarInt(2)); + p.put_slice(&text_component.encode()); + } + } + }); + } + } +} + +#[repr(u8)] +pub enum Mode { + Add, + Remove, + Update, +} + +#[repr(i32)] +pub enum RenderType { + Integer, + Hearts, +} diff --git a/pumpkin-protocol/src/client/play/c_update_score.rs b/pumpkin-protocol/src/client/play/c_update_score.rs new file mode 100644 index 000000000..605d43b9e --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_update_score.rs @@ -0,0 +1,33 @@ +use pumpkin_core::text::TextComponent; +use pumpkin_macros::packet; +use serde::Serialize; + +use crate::{NumberFormat, VarInt}; + +#[derive(Serialize)] +#[packet(0x61)] +pub struct CUpdateScore<'a> { + entity_name: &'a str, + objective_name: &'a str, + value: VarInt, + display_name: Option>, + number_format: Option>, +} + +impl<'a> CUpdateScore<'a> { + pub fn new( + entity_name: &'a str, + objective_name: &'a str, + value: VarInt, + display_name: Option>, + number_format: Option>, + ) -> Self { + Self { + entity_name, + objective_name, + value, + display_name, + number_format, + } + } +} diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 47838aed6..9ce3111dc 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -7,6 +7,7 @@ mod c_change_difficulty; mod c_chunk_data; mod c_close_container; mod c_disguised_chat_message; +mod c_display_objective; mod c_entity_animation; mod c_entity_metadata; mod c_entity_status; @@ -25,6 +26,7 @@ mod c_player_chat_message; mod c_player_info_update; mod c_player_remove; mod c_remove_entities; +mod c_reset_score; mod c_set_container_content; mod c_set_container_property; mod c_set_container_slot; @@ -40,6 +42,8 @@ mod c_unload_chunk; mod c_update_entity_pos; mod c_update_entity_pos_rot; mod c_update_entity_rot; +mod c_update_objectives; +mod c_update_score; mod c_worldevent; mod player_action; @@ -52,6 +56,7 @@ pub use c_change_difficulty::*; pub use c_chunk_data::*; pub use c_close_container::*; pub use c_disguised_chat_message::*; +pub use c_display_objective::*; pub use c_entity_animation::*; pub use c_entity_metadata::*; pub use c_entity_status::*; @@ -70,6 +75,7 @@ pub use c_player_chat_message::*; pub use c_player_info_update::*; pub use c_player_remove::*; pub use c_remove_entities::*; +pub use c_reset_score::*; pub use c_set_container_content::*; pub use c_set_container_property::*; pub use c_set_container_slot::*; @@ -85,5 +91,7 @@ pub use c_unload_chunk::*; pub use c_update_entity_pos::*; pub use c_update_entity_pos_rot::*; pub use c_update_entity_rot::*; +pub use c_update_objectives::*; +pub use c_update_score::*; pub use c_worldevent::*; pub use player_action::*; diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index a59645ec0..f4ef93ce1 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -1,5 +1,6 @@ use bytebuf::{packet_id::Packet, ByteBuffer, DeserializerError}; use bytes::Buf; +use pumpkin_core::text::{style::Style, TextComponent}; use serde::{Deserialize, Serialize}; use std::io::{self, Write}; use thiserror::Error; @@ -244,3 +245,13 @@ pub struct KnownPack<'a> { pub id: &'a str, pub version: &'a str, } + +#[derive(Serialize)] +pub enum NumberFormat<'a> { + /// Show nothing + Blank, + /// The styling to be used when formatting the score number + Styled(Style<'a>), + /// The text to be used as placeholder. + Fixed(TextComponent<'a>), +} diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index c3eed26cb..311438a0b 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -15,7 +15,6 @@ itertools.workspace = true thiserror = "1.0" futures = "0.3" - # Compression flate2 = "1.0" lz4 = "1.11.1" diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 45669b78e..66ad44558 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -53,7 +53,7 @@ thiserror = "1.0" # icon loading base64 = "0.22.1" -png = "0.17.14" +png = "0.17.14" # logging simple_logger = { version = "5.0.0", features = ["threads"] } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index cc5fdc819..c300a1350 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -1,4 +1,4 @@ -use std::f32::consts::PI; +use std::{f32::consts::PI, sync::Arc}; use crate::{ commands::CommandSender, @@ -227,9 +227,13 @@ impl Player { .await; } - pub fn handle_chat_command(&self, server: &Server, command: SChatCommand) { + pub fn handle_chat_command(self: &Arc, server: &Server, command: SChatCommand) { let dispatcher = server.command_dispatcher.clone(); - dispatcher.handle_command(&mut CommandSender::Player(self), server, &command.command); + dispatcher.handle_command( + &mut CommandSender::Player(self.clone()), + server, + &command.command, + ); if ADVANCED_CONFIG.commands.log_console { log::info!( "Player ({}): executed command /{}", diff --git a/pumpkin/src/commands/arg_player.rs b/pumpkin/src/commands/arg_player.rs index 24c91a7c4..316e152cc 100644 --- a/pumpkin/src/commands/arg_player.rs +++ b/pumpkin/src/commands/arg_player.rs @@ -1,39 +1,50 @@ +use std::sync::Arc; + use crate::commands::dispatcher::InvalidTreeError; use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError; use crate::commands::tree::{ConsumedArgs, RawArgs}; use crate::commands::CommandSender; -use crate::commands::CommandSender::Player; use crate::server::Server; /// todo: implement (so far only own name + @s/@p is implemented) -pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option { - let s = args.pop()?; - - match s { - "@s" if src.is_player() => Some(s.into()), - "@p" if src.is_player() => Some(s.into()), - "@r" => None, // todo: implement random player target selector - "@a" | "@e" => None, // todo: implement all players target selector - _ => { - // todo: implement any other player than sender - if let Player(player) = src { - let profile = &player.gameprofile; - if profile.name == s { - return Some(s.into()); - }; - }; - None +pub fn consume_arg_player( + src: &CommandSender, + server: &Server, + args: &mut RawArgs, +) -> Result> { + if let Some(arg) = args.pop() { + match arg { + "@s" => { + if src.is_player() { + return Ok(arg.into()); + } else { + return Err(Some("You are not a Player".into())); + } + } + "@p" if src.is_player() => return Ok(arg.into()), + "@r" => todo!(), // todo: implement random player target selector + "@a" | "@e" => todo!(), // todo: implement all players target selector + name => { + // todo: implement any other player than sender + for world in &server.worlds { + if world.get_player_by_name(name).is_some() { + return Ok(name.into()); + } + } + return Err(Some(format!("Player not found: {}", arg))); + } } } + Err(None) } /// todo: implement (so far only own name + @s/@p is implemented) -pub fn parse_arg_player<'a>( - src: &'a mut CommandSender, - _server: &Server, +pub fn parse_arg_player( + src: &mut CommandSender, + server: &Server, arg_name: &str, consumed_args: &ConsumedArgs, -) -> Result<&'a crate::entity::player::Player, InvalidTreeError> { +) -> Result, InvalidTreeError> { let s = consumed_args .get(arg_name) .ok_or(InvalidConsumptionError(None))? @@ -44,14 +55,12 @@ pub fn parse_arg_player<'a>( "@p" if src.is_player() => Ok(src.as_mut_player().unwrap()), "@r" => Err(InvalidConsumptionError(Some(s.into()))), // todo: implement random player target selector "@a" | "@e" => Err(InvalidConsumptionError(Some(s.into()))), // todo: implement all players target selector - _ => { - // todo: implement any other player than sender - if let Player(player) = src { - let profile = &player.gameprofile; - if profile.name == s { + name => { + for world in &server.worlds { + if let Some(player) = world.get_player_by_name(name) { return Ok(player); - }; - }; + } + } Err(InvalidConsumptionError(Some(s.into()))) } } diff --git a/pumpkin/src/commands/cmd_gamemode.rs b/pumpkin/src/commands/cmd_gamemode.rs index 484d85c0e..f86bc9771 100644 --- a/pumpkin/src/commands/cmd_gamemode.rs +++ b/pumpkin/src/commands/cmd_gamemode.rs @@ -14,6 +14,7 @@ use crate::commands::tree::{CommandTree, ConsumedArgs, RawArgs}; use crate::commands::tree_builder::{argument, require}; use crate::commands::CommandSender; use crate::commands::CommandSender::Player; +use crate::server::Server; const NAMES: [&str; 1] = ["gamemode"]; @@ -22,20 +23,27 @@ const DESCRIPTION: &str = "Change a player's gamemode."; const ARG_GAMEMODE: &str = "gamemode"; const ARG_TARGET: &str = "target"; -pub fn consume_arg_gamemode(_src: &CommandSender, args: &mut RawArgs) -> Option { - let s = args.pop()?; - - if let Ok(id) = s.parse::() { - match GameMode::from_u8(id) { - None | Some(GameMode::Undefined) => {} - Some(_) => return Some(s.into()), +pub fn consume_arg_gamemode( + _src: &CommandSender, + _server: &Server, + args: &mut RawArgs, +) -> Result> { + if let Some(arg) = args.pop() { + if let Ok(id) = arg.parse::() { + match GameMode::from_u8(id) { + None | Some(GameMode::Undefined) => {} + Some(_) => return Ok(arg.into()), + }; }; - }; - match GameMode::from_str(s) { - Err(_) | Ok(GameMode::Undefined) => None, - Ok(_) => Some(s.into()), + match GameMode::from_str(arg) { + Err(_) | Ok(GameMode::Undefined) => { + return Err(Some(format!("Gamemode not found: {}", arg))) + } + Ok(_) => return Ok(arg.into()), + } } + Err(None) } pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result { @@ -66,14 +74,14 @@ pub fn init_command_tree<'a>() -> CommandTree<'a> { return if let Player(target) = sender { if target.gamemode.load() == gamemode { - target.send_system_message(TextComponent::text(&format!( + target.send_system_message(&TextComponent::text(&format!( "You already in {:?} gamemode", gamemode ))); } else { // TODO target.set_gamemode(gamemode); - target.send_system_message(TextComponent::text(&format!( + target.send_system_message(&TextComponent::text(&format!( "Game mode was set to {:?}", gamemode ))); @@ -90,16 +98,16 @@ pub fn init_command_tree<'a>() -> CommandTree<'a> { let target = parse_arg_player(sender, server, ARG_TARGET, args)?; if target.gamemode.load() == gamemode { - target.send_system_message(TextComponent::text(&format!( - "You already in {:?} gamemode", - gamemode + sender.send_message(TextComponent::text(&format!( + "{} is already in {:?} gamemode", + target.gameprofile.name, gamemode ))); } else { // TODO target.set_gamemode(gamemode); - target.send_system_message(TextComponent::text(&format!( - "Game mode was set to {:?}", - gamemode + sender.send_message(TextComponent::text(&format!( + "{}'s Game mode was set to {:?}", + target.gameprofile.name, gamemode ))); } diff --git a/pumpkin/src/commands/cmd_help.rs b/pumpkin/src/commands/cmd_help.rs index f224f0854..18dddc6e7 100644 --- a/pumpkin/src/commands/cmd_help.rs +++ b/pumpkin/src/commands/cmd_help.rs @@ -3,6 +3,8 @@ use crate::commands::dispatcher::{CommandDispatcher, InvalidTreeError}; use crate::commands::tree::{Command, CommandTree, ConsumedArgs, RawArgs}; use crate::commands::tree_builder::argument; use crate::commands::CommandSender; +use crate::server::Server; +use pumpkin_core::text::TextComponent; const NAMES: [&str; 3] = ["help", "h", "?"]; @@ -10,12 +12,16 @@ const DESCRIPTION: &str = "Print a help message."; const ARG_COMMAND: &str = "command"; -fn consume_arg_command(_src: &CommandSender, _args: &mut RawArgs) -> Option { +fn consume_arg_command( + _src: &CommandSender, + _server: &Server, + _args: &mut RawArgs, +) -> Result> { // let s = args.pop()?; // dispatcher.get_tree(s).ok().map(|tree| tree.names[0].into()) // TODO - None + Err(None) } fn parse_arg_command<'a>( @@ -37,12 +43,12 @@ pub fn init_command_tree<'a>() -> CommandTree<'a> { argument(ARG_COMMAND, consume_arg_command).execute(&|sender, server, args| { let tree = parse_arg_command(args, &server.command_dispatcher)?; - // sender.send_message(TextComponent::text(&format!( - // "{} - {} Usage: {}", - // tree.names.join("/"), - // tree.description, - // tree - // ))); + sender.send_message(TextComponent::text(&format!( + "{} - {} Usage: {}", + tree.names.join("/"), + tree.description, + tree + ))); Ok(()) }), @@ -56,12 +62,12 @@ pub fn init_command_tree<'a>() -> CommandTree<'a> { continue; }; - // sender.send_message(TextComponent::text(&format!( - // "{} - {} Usage: {}", - // tree.names.join("/"), - // tree.description, - // tree - // ))); + sender.send_message(TextComponent::text(&format!( + "{} - {} Usage: {}", + tree.names.join("/"), + tree.description, + tree + ))); } Ok(()) diff --git a/pumpkin/src/commands/cmd_kick.rs b/pumpkin/src/commands/cmd_kick.rs new file mode 100644 index 000000000..43a24a27a --- /dev/null +++ b/pumpkin/src/commands/cmd_kick.rs @@ -0,0 +1,27 @@ +use crate::commands::arg_player::parse_arg_player; +use crate::commands::tree::CommandTree; +use crate::commands::tree_builder::argument; +use pumpkin_core::text::{color::NamedColor, TextComponent}; + +use super::arg_player::consume_arg_player; + +const NAMES: [&str; 1] = ["kick"]; +const DESCRIPTION: &str = "Kicks the target player from the server."; + +const ARG_TARGET: &str = "target"; + +pub fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(NAMES, DESCRIPTION).with_child( + argument(ARG_TARGET, consume_arg_player).execute(&|sender, server, args| { + dbg!("aa"); + let target = parse_arg_player(sender, server, ARG_TARGET, args)?; + target.kick(TextComponent::text("Kicked by an operator")); + + sender.send_message( + TextComponent::text("Player has been kicked.").color_named(NamedColor::Blue), + ); + + Ok(()) + }), + ) +} diff --git a/pumpkin/src/commands/cmd_kill.rs b/pumpkin/src/commands/cmd_kill.rs index 77c95f40e..cf4208652 100644 --- a/pumpkin/src/commands/cmd_kill.rs +++ b/pumpkin/src/commands/cmd_kill.rs @@ -1,8 +1,6 @@ use crate::commands::arg_player::{consume_arg_player, parse_arg_player}; use crate::commands::tree::CommandTree; -use crate::commands::tree::RawArgs; use crate::commands::tree_builder::argument; -use crate::commands::CommandSender; use pumpkin_core::text::{color::NamedColor, TextComponent}; const NAMES: [&str; 1] = ["kill"]; @@ -10,13 +8,10 @@ const DESCRIPTION: &str = "Kills a target player."; const ARG_TARGET: &str = "target"; -pub fn consume_arg_target(_src: &CommandSender, args: &mut RawArgs) -> Option { - consume_arg_player(_src, args) -} - pub fn init_command_tree<'a>() -> CommandTree<'a> { CommandTree::new(NAMES, DESCRIPTION).with_child( - argument(ARG_TARGET, consume_arg_target).execute(&|sender, server, args| { + argument(ARG_TARGET, consume_arg_player).execute(&|sender, server, args| { + // TODO parse entities not only players let target = parse_arg_player(sender, server, ARG_TARGET, args)?; target.living_entity.kill(); diff --git a/pumpkin/src/commands/dispatcher.rs b/pumpkin/src/commands/dispatcher.rs index 282551253..6a044388f 100644 --- a/pumpkin/src/commands/dispatcher.rs +++ b/pumpkin/src/commands/dispatcher.rs @@ -56,14 +56,17 @@ impl<'a> CommandDispatcher<'a> { println!("Error while parsing command \"{cmd}\": a requirement that was expected was not met."); return Err("Internal Error (See logs for details)".into()); } - Ok(is_fitting_path) => { - if is_fitting_path { - return Ok(()); + Ok(is_fitting_path) => match is_fitting_path { + Ok(_) => return Ok(()), + Err(error) => { + // Custom error message or not ? + if let Some(error) = error { + return Err(error); + } } - } + }, } } - Err(format!("Invalid Syntax. Usage: {}", tree)) } @@ -88,7 +91,7 @@ impl<'a> CommandDispatcher<'a> { path: Vec, tree: &CommandTree, mut raw_args: RawArgs, - ) -> Result { + ) -> Result>, InvalidTreeError> { let mut parsed_args: ConsumedArgs = HashMap::new(); for node in path.iter().map(|&i| &tree.nodes[i]) { @@ -96,36 +99,37 @@ impl<'a> CommandDispatcher<'a> { NodeType::ExecuteLeaf { run } => { return if raw_args.is_empty() { run(src, server, &parsed_args)?; - Ok(true) + Ok(Ok(())) } else { - Ok(false) + Ok(Err(None)) }; } NodeType::Literal { string, .. } => { if raw_args.pop() != Some(string) { - return Ok(false); + return Ok(Err(None)); } } NodeType::Argument { consumer: consume, name, .. - } => { - if let Some(consumed) = consume(src, &mut raw_args) { + } => match consume(src, server, &mut raw_args) { + Ok(consumed) => { parsed_args.insert(name, consumed); - } else { - return Ok(false); } - } + Err(err) => { + return Ok(Err(err)); + } + }, NodeType::Require { predicate, .. } => { if !predicate(src) { - return Ok(false); + return Ok(Err(None)); } } } } - Ok(false) + Ok(Err(None)) } /// Register a command with the dispatcher. diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index 3f7bce6c8..84ddfdd68 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use dispatcher::InvalidTreeError; use pumpkin_core::text::TextComponent; use tree::ConsumedArgs; @@ -9,6 +11,7 @@ mod arg_player; mod cmd_echest; mod cmd_gamemode; mod cmd_help; +mod cmd_kick; mod cmd_kill; mod cmd_pumpkin; mod cmd_stop; @@ -20,15 +23,15 @@ mod tree_format; pub enum CommandSender<'a> { Rcon(&'a mut Vec), Console, - Player(&'a Player), + Player(Arc), } impl<'a> CommandSender<'a> { - pub async fn send_message(&mut self, text: TextComponent<'a>) { + pub fn send_message(&mut self, text: TextComponent) { match self { // TODO: add color and stuff to console CommandSender::Console => log::info!("{}", text.to_pretty_console()), - CommandSender::Player(c) => c.send_system_message(text).await, + CommandSender::Player(c) => c.send_system_message(&text), CommandSender::Rcon(s) => s.push(text.to_pretty_console()), } } @@ -48,9 +51,9 @@ impl<'a> CommandSender<'a> { CommandSender::Rcon(_) => true, } } - pub fn as_mut_player(&mut self) -> Option<&Player> { + pub fn as_mut_player(&mut self) -> Option> { match self { - CommandSender::Player(player) => Some(player), + CommandSender::Player(player) => Some(player.clone()), CommandSender::Console => None, CommandSender::Rcon(_) => None, } @@ -75,6 +78,7 @@ pub fn default_dispatcher<'a>() -> CommandDispatcher<'a> { dispatcher.register(cmd_help::init_command_tree()); dispatcher.register(cmd_echest::init_command_tree()); dispatcher.register(cmd_kill::init_command_tree()); + dispatcher.register(cmd_kick::init_command_tree()); dispatcher } diff --git a/pumpkin/src/commands/tree.rs b/pumpkin/src/commands/tree.rs index ebbe3d801..65407dbcb 100644 --- a/pumpkin/src/commands/tree.rs +++ b/pumpkin/src/commands/tree.rs @@ -1,5 +1,5 @@ use super::RunFunctionType; -use crate::commands::CommandSender; +use crate::{commands::CommandSender, server::Server}; use std::collections::{HashMap, VecDeque}; /// see [crate::commands::tree_builder::argument] @@ -9,7 +9,9 @@ pub type RawArgs<'a> = Vec<&'a str>; pub type ConsumedArgs<'a> = HashMap<&'a str, String>; /// see [crate::commands::tree_builder::argument] -pub type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option; +/// Provide value or an Optional error message, If no Error message provided the default will be used +pub type ArgumentConsumer<'a> = + fn(&CommandSender, &Server, &mut RawArgs) -> Result>; pub struct Node<'a> { pub(crate) children: Vec, diff --git a/pumpkin/src/server/connection_cache.rs b/pumpkin/src/server/connection_cache.rs index 1a57dffed..b90a7d16b 100644 --- a/pumpkin/src/server/connection_cache.rs +++ b/pumpkin/src/server/connection_cache.rs @@ -1,4 +1,10 @@ -use std::{fs::File, path::Path}; +use core::error; +use std::{ + fs::File, + io::{Cursor, Read}, + path::Path, + sync::LazyLock, +}; use base64::{engine::general_purpose, Engine as _}; use pumpkin_config::{BasicConfiguration, BASIC_CONFIG}; @@ -9,6 +15,29 @@ use pumpkin_protocol::{ use super::CURRENT_MC_VERSION; +static DEFAULT_ICON: LazyLock<&[u8]> = + LazyLock::new(|| include_bytes!("../../../assets/default_icon.png")); + +fn load_icon_from_file>(path: P) -> Result> { + let mut icon_file = File::open(path)?; + let mut buf = Vec::new(); + icon_file.read_to_end(&mut buf)?; + load_icon_from_bytes(&buf) +} + +fn load_icon_from_bytes(png_data: &[u8]) -> Result> { + let icon = png::Decoder::new(Cursor::new(&png_data)); + let reader = icon.read_info()?; + let info = reader.info(); + assert!(info.width == 64, "Icon width must be 64"); + assert!(info.height == 64, "Icon height must be 64"); + + // Reader consumes the image. Once we verify dimensions, we want to encode the entire raw image + let mut result = "data:image/png;base64,".to_owned(); + general_purpose::STANDARD.encode_string(png_data, &mut result); + Ok(result) +} + pub struct CachedStatus { _status_response: StatusResponse, // We cache the json response here so we don't parse it every time someone makes a Status request. @@ -57,10 +86,21 @@ impl CachedStatus { } pub fn build_response(config: &BasicConfiguration) -> StatusResponse { - let icon_path = "/icon.png"; - let icon = if Path::new(icon_path).exists() { - Some(Self::load_icon(icon_path)) + let icon = if config.use_favicon { + let icon_path = &config.favicon_path; + log::info!("Loading server favicon from '{}'", icon_path); + match load_icon_from_file(icon_path).or_else(|err| { + log::warn!("Failed to load icon from '{}': {}", icon_path, err); + load_icon_from_bytes(DEFAULT_ICON.as_ref()) + }) { + Ok(result) => Some(result), + Err(err) => { + log::warn!("Failed to load default icon: {}", err); + None + } + } } else { + log::info!("Not using a server favicon"); None }; @@ -82,21 +122,4 @@ impl CachedStatus { enforce_secure_chat: false, } } - - fn load_icon>(path: P) -> String { - let icon = png::Decoder::new(File::open(path).expect("Failed to load icon")); - let mut reader = icon.read_info().unwrap(); - let info = reader.info(); - assert!(info.width == 64, "Icon width must be 64"); - assert!(info.height == 64, "Icon height must be 64"); - // Allocate the output buffer. - let mut buf = vec![0; reader.output_buffer_size()]; - // Read the next frame. An APNG might contain multiple frames. - let info = reader.next_frame(&mut buf).unwrap(); - // Grab the bytes of the image. - let bytes = &buf[..info.buffer_size()]; - let mut result = "data:image/png;base64,".to_owned(); - general_purpose::STANDARD.encode_string(bytes, &mut result); - result - } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index fbec77e6a..c31343803 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -18,9 +18,12 @@ use pumpkin_protocol::{ ClientPacket, VarInt, }; use pumpkin_world::level::Level; +use scoreboard::Scoreboard; use tokio::sync::mpsc; use tokio::sync::Mutex; +pub mod scoreboard; + /// Represents a Minecraft world, containing entities, players, and the underlying level data. /// /// Each dimension (Overworld, Nether, End) typically has its own `World`. @@ -35,6 +38,7 @@ pub struct World { pub level: Arc>, /// A map of active players within the world, keyed by their unique token. pub current_players: Arc>>>, + pub scoreboard: Mutex, // TODO: entities } @@ -43,6 +47,7 @@ impl World { Self { level: Arc::new(Mutex::new(level)), current_players: Arc::new(Mutex::new(HashMap::new())), + scoreboard: Mutex::new(Scoreboard::new()), } } diff --git a/pumpkin/src/world/scoreboard.rs b/pumpkin/src/world/scoreboard.rs new file mode 100644 index 000000000..fa758d630 --- /dev/null +++ b/pumpkin/src/world/scoreboard.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; + +use pumpkin_core::text::TextComponent; +use pumpkin_protocol::{ + client::play::{CDisplayObjective, CUpdateObjectives, CUpdateScore, RenderType}, + NumberFormat, VarInt, +}; + +use super::World; + +pub struct Scoreboard { + objectives: HashMap>, + // teams: HashMap, +} + +impl Default for Scoreboard { + fn default() -> Self { + Self::new() + } +} + +impl Scoreboard { + pub fn new() -> Self { + Self { + objectives: HashMap::new(), + } + } + + pub fn add_objective(&mut self, world: &World, objective: ScoreboardObjective) { + if self.objectives.contains_key(objective.name) { + // Maybe make this an error ? + log::warn!( + "Tried to create Objective which does already exist, {}", + &objective.name + ); + return; + } + world.broadcast_packet_all(&CUpdateObjectives::new( + objective.name, + pumpkin_protocol::client::play::Mode::Add, + objective.display_name, + objective.render_type, + objective.number_format, + )); + world.broadcast_packet_all(&CDisplayObjective::new( + pumpkin_protocol::client::play::DisplaySlot::Sidebar, + "test", + )); + } + + pub fn update_score(&self, world: &World, score: ScoreboardScore) { + if self.objectives.contains_key(score.objective_name) { + log::warn!( + "Tried to place a score into a Objective which does not exist, {}", + &score.objective_name + ); + return; + } + world.broadcast_packet_all(&CUpdateScore::new( + score.entity_name, + score.objective_name, + score.value, + score.display_name, + score.number_format, + )); + } + + // pub fn add_team(&mut self, name: String) { + // if self.teams.contains_key(&name) { + // // Maybe make this an error ? + // log::warn!("Tried to create Team which does already exist, {}", name); + // } + // } +} + +pub struct ScoreboardObjective<'a> { + name: &'a str, + display_name: TextComponent<'a>, + render_type: RenderType, + number_format: Option>, +} + +impl<'a> ScoreboardObjective<'a> { + pub const fn new( + name: &'a str, + display_name: TextComponent<'a>, + render_type: RenderType, + number_format: Option>, + ) -> Self { + Self { + name, + display_name, + render_type, + number_format, + } + } +} + +pub struct ScoreboardScore<'a> { + entity_name: &'a str, + objective_name: &'a str, + value: VarInt, + display_name: Option>, + number_format: Option>, +} + +impl<'a> ScoreboardScore<'a> { + pub const fn new( + entity_name: &'a str, + objective_name: &'a str, + value: VarInt, + display_name: Option>, + number_format: Option>, + ) -> Self { + Self { + entity_name, + objective_name, + value, + display_name, + number_format, + } + } +}