Merge branch 'master' into tokio

This commit is contained in:
Snowiiii 2024-10-19 17:52:33 +02:00
commit 2287564973
28 changed files with 552 additions and 124 deletions

View File

@ -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"] }

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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(),
}
}
}

View File

@ -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,
}

View File

@ -0,0 +1,18 @@
use pumpkin_macros::packet;
use serde::Serialize;
#[derive(Serialize)]
#[packet(0x44)]
pub struct CResetScore {
entity_name: String,
objective_name: Option<String>,
}
impl CResetScore {
pub fn new(entity_name: String, objective_name: Option<String>) -> Self {
Self {
entity_name,
objective_name,
}
}
}

View File

@ -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 }
}
}

View File

@ -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<NumberFormat<'a>>,
}
impl<'a> CUpdateObjectives<'a> {
pub fn new(
objective_name: &'a str,
mode: Mode,
display_name: TextComponent<'a>,
render_type: RenderType,
number_format: Option<NumberFormat<'a>>,
) -> 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,
}

View File

@ -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<TextComponent<'a>>,
number_format: Option<NumberFormat<'a>>,
}
impl<'a> CUpdateScore<'a> {
pub fn new(
entity_name: &'a str,
objective_name: &'a str,
value: VarInt,
display_name: Option<TextComponent<'a>>,
number_format: Option<NumberFormat<'a>>,
) -> Self {
Self {
entity_name,
objective_name,
value,
display_name,
number_format,
}
}
}

View File

@ -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::*;

View File

@ -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>),
}

View File

@ -15,7 +15,6 @@ itertools.workspace = true
thiserror = "1.0"
futures = "0.3"
# Compression
flate2 = "1.0"
lz4 = "1.11.1"

View File

@ -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<Self>, 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 /{}",

View File

@ -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<String> {
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
_ => {
pub fn consume_arg_player(
src: &CommandSender,
server: &Server,
args: &mut RawArgs,
) -> Result<String, Option<String>> {
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
if let Player(player) = src {
let profile = &player.gameprofile;
if profile.name == s {
return Some(s.into());
};
};
None
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<Arc<crate::entity::player::Player>, 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())))
}
}

View File

@ -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<String> {
let s = args.pop()?;
if let Ok(id) = s.parse::<u8>() {
pub fn consume_arg_gamemode(
_src: &CommandSender,
_server: &Server,
args: &mut RawArgs,
) -> Result<String, Option<String>> {
if let Some(arg) = args.pop() {
if let Ok(id) = arg.parse::<u8>() {
match GameMode::from_u8(id) {
None | Some(GameMode::Undefined) => {}
Some(_) => return Some(s.into()),
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<GameMode, InvalidTreeError> {
@ -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
)));
}

View File

@ -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<String> {
fn consume_arg_command(
_src: &CommandSender,
_server: &Server,
_args: &mut RawArgs,
) -> Result<String, Option<String>> {
// 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(())

View File

@ -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(())
}),
)
}

View File

@ -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<String> {
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();

View File

@ -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<usize>,
tree: &CommandTree,
mut raw_args: RawArgs,
) -> Result<bool, InvalidTreeError> {
) -> Result<Result<(), Option<String>>, 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.

View File

@ -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<String>),
Console,
Player(&'a Player),
Player(Arc<Player>),
}
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<Arc<Player>> {
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
}

View File

@ -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<String>;
/// 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<String, Option<String>>;
pub struct Node<'a> {
pub(crate) children: Vec<usize>,

View File

@ -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<P: AsRef<Path>>(path: P) -> Result<String, Box<dyn error::Error>> {
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<String, Box<dyn error::Error>> {
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<P: AsRef<Path>>(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
}
}

View File

@ -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<Mutex<Level>>,
/// A map of active players within the world, keyed by their unique token.
pub current_players: Arc<Mutex<HashMap<usize, Arc<Player>>>>,
pub scoreboard: Mutex<Scoreboard>,
// 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()),
}
}

View File

@ -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<String, ScoreboardObjective<'static>>,
// teams: HashMap<String, Team>,
}
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<NumberFormat<'a>>,
}
impl<'a> ScoreboardObjective<'a> {
pub const fn new(
name: &'a str,
display_name: TextComponent<'a>,
render_type: RenderType,
number_format: Option<NumberFormat<'a>>,
) -> 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<TextComponent<'a>>,
number_format: Option<NumberFormat<'a>>,
}
impl<'a> ScoreboardScore<'a> {
pub const fn new(
entity_name: &'a str,
objective_name: &'a str,
value: VarInt,
display_name: Option<TextComponent<'a>>,
number_format: Option<NumberFormat<'a>>,
) -> Self {
Self {
entity_name,
objective_name,
value,
display_name,
number_format,
}
}
}