Merge branch 'master' into tokio
This commit is contained in:
commit
2287564973
@ -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"] }
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
pumpkin-protocol/src/client/play/c_display_objective.rs
Normal file
43
pumpkin-protocol/src/client/play/c_display_objective.rs
Normal 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,
|
||||
}
|
||||
18
pumpkin-protocol/src/client/play/c_reset_score.rs
Normal file
18
pumpkin-protocol/src/client/play/c_reset_score.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
71
pumpkin-protocol/src/client/play/c_update_objectives.rs
Normal file
71
pumpkin-protocol/src/client/play/c_update_objectives.rs
Normal 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,
|
||||
}
|
||||
33
pumpkin-protocol/src/client/play/c_update_score.rs
Normal file
33
pumpkin-protocol/src/client/play/c_update_score.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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::*;
|
||||
|
||||
@ -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>),
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ itertools.workspace = true
|
||||
thiserror = "1.0"
|
||||
futures = "0.3"
|
||||
|
||||
|
||||
# Compression
|
||||
flate2 = "1.0"
|
||||
lz4 = "1.11.1"
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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 /{}",
|
||||
|
||||
@ -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
|
||||
_ => {
|
||||
// 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<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
|
||||
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())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>() {
|
||||
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<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 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
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@ -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(())
|
||||
|
||||
27
pumpkin/src/commands/cmd_kick.rs
Normal file
27
pumpkin/src/commands/cmd_kick.rs
Normal 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(())
|
||||
}),
|
||||
)
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
123
pumpkin/src/world/scoreboard.rs
Normal file
123
pumpkin/src/world/scoreboard.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user