chore: added gradient and rainbow to TextComponent

ofc did also a bunch of other things, the JavaClient implementation should be cleaner now and does not lock the player anymore
This commit is contained in:
Alexander Medvedev 2026-01-25 16:41:46 +01:00
parent 2853a1e741
commit 622d0599df
16 changed files with 294 additions and 225 deletions

14
Cargo.lock generated
View File

@ -2064,9 +2064,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.105" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2312,9 +2312,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.43" version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2338,7 +2338,7 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.10.0-rc.7" version = "0.10.0-rc.7"
source = "git+https://github.com/rust-random/rand#6e80534233d4bb09ed0abc086edeacaae6f54dc0" source = "git+https://github.com/rust-random/rand#7a81944044f3f1d2dc5c85a8ca9bd30429f46bd0"
dependencies = [ dependencies = [
"chacha20", "chacha20",
"getrandom 0.4.0-rc.0", "getrandom 0.4.0-rc.0",
@ -3302,9 +3302,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.19.0" version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"js-sys", "js-sys",

View File

@ -45,7 +45,7 @@ futures = { version = "0.3", default-features = false, features = ["executor"] }
rayon = "1.11" rayon = "1.11"
crossbeam = "0.8" crossbeam = "0.8"
uuid = { version = "1.19", features = ["serde", "v3", "v4"] } uuid = { version = "1.20", features = ["serde", "v3", "v4"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -1,45 +0,0 @@
### Pumpkin Text
Here we build Mojang's Textcomponent, Which is used across many places, Often where text is send to the player.
### Features
- Colors
- [x] RBG
- [x] Black
- [x] Dark Blue
- [x] Dark Green
- [x] Dark Aqua
- [x] Dark Red
- [x] Dark Purple
- [x] Gold
- [x] Gray
- [x] Dark Gray
- [x] Blue
- [x] Green
- [x] Aqua
- [x] Red
- [x] Light Purple
- [x] Yellow
- [x] White
- Style
- [x] Bold
- [x] Italic
- [x] Underlined
- [x] Strikethrough
- [x] Obfuscated
- [x] Insertion
- [x] Shadow Color
- Click Event
- [x] Open URL
- [x] Run Command
- [x] Suggest Command
- [x] Change Pange (Book)
- [x] Copy to Clipboard
- Hover Event
- [x] Show Text
- [x] Show Item
- [x] ShowEntity
- Fonts
- [x] Default
- [x] Uniform (Unicode)
- [x] Alt
- [x] Illageralt

View File

@ -16,6 +16,25 @@ pub enum Color {
Named(NamedColor), Named(NamedColor),
} }
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
let c = v * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let m = v - c;
let (r, g, b) = match (h as i32 / 60) % 6 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
(
((r + m) * 255.0) as u8,
((g + m) * 255.0) as u8,
((b + m) * 255.0) as u8,
)
}
impl<'de> Deserialize<'de> for Color { impl<'de> Deserialize<'de> for Color {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?; let s = String::deserialize(deserializer)?;
@ -75,9 +94,9 @@ impl Color {
#[derive(Debug, Deserialize, Clone, Copy, Eq, Hash, PartialEq)] #[derive(Debug, Deserialize, Clone, Copy, Eq, Hash, PartialEq)]
pub struct RGBColor { pub struct RGBColor {
red: u8, pub red: u8,
green: u8, pub green: u8,
blue: u8, pub blue: u8,
} }
impl RGBColor { impl RGBColor {
@ -142,6 +161,29 @@ pub enum NamedColor {
White, White,
} }
impl NamedColor {
pub fn to_rgb(&self) -> RGBColor {
match self {
NamedColor::Black => RGBColor::new(0, 0, 0),
NamedColor::DarkBlue => RGBColor::new(0, 0, 170),
NamedColor::DarkGreen => RGBColor::new(0, 170, 0),
NamedColor::DarkAqua => RGBColor::new(0, 170, 170),
NamedColor::DarkRed => RGBColor::new(170, 0, 0),
NamedColor::DarkPurple => RGBColor::new(170, 0, 170),
NamedColor::Gold => RGBColor::new(255, 170, 0),
NamedColor::Gray => RGBColor::new(170, 170, 170),
NamedColor::DarkGray => RGBColor::new(85, 85, 85),
NamedColor::Blue => RGBColor::new(85, 85, 255),
NamedColor::Green => RGBColor::new(85, 255, 85),
NamedColor::Aqua => RGBColor::new(85, 255, 255),
NamedColor::Red => RGBColor::new(255, 85, 85),
NamedColor::LightPurple => RGBColor::new(255, 85, 255),
NamedColor::Yellow => RGBColor::new(255, 255, 85),
NamedColor::White => RGBColor::new(255, 255, 255),
}
}
}
impl TryFrom<&str> for NamedColor { impl TryFrom<&str> for NamedColor {
type Error = (); type Error = ();

View File

@ -1,4 +1,4 @@
use crate::text::color::ARGBColor; use crate::text::color::{ARGBColor, hsv_to_rgb};
use crate::translation::{ use crate::translation::{
Locale, get_translation, get_translation_text, reorder_substitutions, translation_to_pretty, Locale, get_translation, get_translation_text, reorder_substitutions, translation_to_pretty,
}; };
@ -352,6 +352,82 @@ impl TextComponent {
self self
} }
/// Applies a color gradient to the text
pub fn gradient_named(self, colors: &[color::NamedColor]) -> Self {
let rgb_colors: Vec<color::RGBColor> = colors.iter().map(|c| c.to_rgb()).collect();
self.gradient(&rgb_colors)
}
/// Applies a color gradient to the text
pub fn gradient(self, colors: &[color::RGBColor]) -> Self {
if colors.len() < 2 {
return self;
}
self.apply_color_effect(|i, len| {
if len <= 1 {
return colors[0];
}
let total_segments = colors.len() - 1;
let position = i as f32 / (len - 1) as f32;
let segment_f = position * total_segments as f32;
let segment_index = (segment_f.floor() as usize).min(total_segments - 1);
let local_t = segment_f - segment_index as f32;
let start = colors[segment_index];
let end = colors[segment_index + 1];
// LERP logic
color::RGBColor::new(
(start.red as f32 + (end.red as f32 - start.red as f32) * local_t) as u8,
(start.green as f32 + (end.green as f32 - start.green as f32) * local_t) as u8,
(start.blue as f32 + (end.blue as f32 - start.blue as f32) * local_t) as u8,
)
})
}
/// Applies a rainbow effect to the text
pub fn rainbow(self) -> Self {
self.apply_color_effect(|i, len| {
let hue = (i as f32 / len as f32) * 360.0;
let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0);
color::RGBColor::new(r, g, b)
})
}
fn apply_color_effect<F>(mut self, color_gen: F) -> Self
where
F: Fn(usize, usize) -> color::RGBColor,
{
// TODO
let raw_text = self.0.clone().get_text(Locale::EnUs);
let chars: Vec<char> = raw_text.chars().collect();
let len = chars.len();
if len == 0 {
return self;
}
let mut colored_extra = Vec::new();
for (i, c) in chars.into_iter().enumerate() {
let rgb = color_gen(i, len);
let mut char_base = TextComponentBase {
content: TextContent::Text {
text: Cow::Owned(c.to_string()),
},
style: self.0.style.clone(),
extra: vec![],
};
char_base.style.color = Some(Color::Rgb(rgb));
colored_extra.push(char_base);
}
self.0.content = TextContent::Text { text: "".into() };
self.0.extra = colored_extra;
self
}
/// Makes the text bold /// Makes the text bold
pub fn bold(mut self) -> Self { pub fn bold(mut self) -> Self {
self.0.style.bold = Some(true); self.0.style.bold = Some(true);

View File

@ -886,7 +886,6 @@ impl ProtoChunk {
block_registry: &dyn BlockRegistryExt, block_registry: &dyn BlockRegistryExt,
random_config: &GlobalRandomConfig, random_config: &GlobalRandomConfig,
) { ) {
// 1. Efficiently collect unique biomes from the flat map
let (center_x, center_z, min_y, height, biomes_in_chunk) = { let (center_x, center_z, min_y, height, biomes_in_chunk) = {
let chunk = cache.get_center_chunk(); let chunk = cache.get_center_chunk();
let mut unique_biomes = Vec::with_capacity(4); // Usually few biomes per chunk let mut unique_biomes = Vec::with_capacity(4); // Usually few biomes per chunk
@ -911,13 +910,9 @@ impl ProtoChunk {
let population_seed = let population_seed =
Xoroshiro::get_population_seed(random_config.seed, start_block_x, start_block_z); Xoroshiro::get_population_seed(random_config.seed, start_block_x, start_block_z);
// Vanilla has 11 steps (0..10)
for step in 0..11 { for step in 0..11 {
// --- STEP A: STRUCTURE PIECES ---
// Structures are handled first because features (like trees) need to avoid clipping into them
Self::generate_structure_step(cache, step, population_seed, random_config.seed as i64); Self::generate_structure_step(cache, step, population_seed, random_config.seed as i64);
// --- STEP B: PLACED FEATURES ---
let mut features_to_run = Vec::new(); let mut features_to_run = Vec::new();
for biome_id in &biomes_in_chunk { for biome_id in &biomes_in_chunk {
if let Some(biome) = Biome::from_id(*biome_id) if let Some(biome) = Biome::from_id(*biome_id)
@ -930,7 +925,6 @@ impl ProtoChunk {
} }
} }
// CRITICAL: Determinism. Sort features so the index 'p' always refers to the same feature.
features_to_run.sort_unstable(); features_to_run.sort_unstable();
features_to_run.dedup(); features_to_run.dedup();

View File

@ -24,7 +24,7 @@ pub type BlockStateId = u16;
pub const CURRENT_MC_VERSION: &str = "1.21.11"; pub const CURRENT_MC_VERSION: &str = "1.21.11";
pub const LOWEST_SUPPRORTED_PROTOCOL_VERSION: u32 = 773; pub const LOWEST_SUPPRORTED_PROTOCOL_VERSION: u32 = 773;
pub const CURRENT_BEDROCK_MC_VERSION: &str = "1.21.130"; pub const CURRENT_BEDROCK_MC_VERSION: &str = "1.21.132";
pub const CURRENT_BEDROCK_MC_PROTOCOL: u32 = 898; pub const CURRENT_BEDROCK_MC_PROTOCOL: u32 = 898;
#[macro_export] #[macro_export]

View File

@ -381,6 +381,8 @@ pub struct Player {
pub keep_alive_id: AtomicI64, pub keep_alive_id: AtomicI64,
/// The last time we sent a keep alive packet. /// The last time we sent a keep alive packet.
pub last_keep_alive_time: AtomicCell<Instant>, pub last_keep_alive_time: AtomicCell<Instant>,
/// The ping in millis.
pub ping: AtomicU32,
/// The amount of ticks since the player's last attack. /// The amount of ticks since the player's last attack.
pub last_attacked_ticks: AtomicU32, pub last_attacked_ticks: AtomicU32,
/// The player's last known experience level. /// The player's last known experience level.
@ -486,6 +488,7 @@ impl Player {
wait_for_keep_alive: AtomicBool::new(false), wait_for_keep_alive: AtomicBool::new(false),
keep_alive_id: AtomicI64::new(0), keep_alive_id: AtomicI64::new(0),
last_keep_alive_time: AtomicCell::new(std::time::Instant::now()), last_keep_alive_time: AtomicCell::new(std::time::Instant::now()),
ping: AtomicU32::new(0),
last_attacked_ticks: AtomicU32::new(0), last_attacked_ticks: AtomicU32::new(0),
client_loaded: AtomicBool::new(false), client_loaded: AtomicBool::new(false),
client_loaded_timeout: AtomicU32::new(60), client_loaded_timeout: AtomicU32::new(60),
@ -1402,15 +1405,8 @@ impl Player {
self.set_client_loaded(false); self.set_client_loaded(false);
let uuid = self.gameprofile.id; let uuid = self.gameprofile.id;
current_world.remove_player(self, false).await; let player = current_world.remove_player(self, false).await.unwrap();
new_world.players.write().await.insert(uuid, Arc::new(Self::new( new_world.players.write().await.insert(uuid, player);
self.client.clone(),
self.gameprofile.clone(),
self.config.read().await.clone(),
new_world.clone(),
self.gamemode.load(),
)
.await));
self.unload_watched_chunks(&current_world).await; self.unload_watched_chunks(&current_world).await;
self.chunk_manager.lock().await.change_world(&current_world.level, &new_world.level); self.chunk_manager.lock().await.change_world(&current_world.level, &new_world.level);

View File

@ -2,18 +2,16 @@
#![allow(unused_labels)] #![allow(unused_labels)]
use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper}; use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper};
use crate::net::DisconnectReason;
use crate::net::bedrock::BedrockClient; use crate::net::bedrock::BedrockClient;
use crate::net::java::JavaClient; use crate::net::java::{JavaClient, PacketHandlerResult};
use crate::net::{ClientPlatform, DisconnectReason};
use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer}; use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer};
use crate::server::{Server, ticker::Ticker}; use crate::server::{Server, ticker::Ticker};
use log::{Level, LevelFilter}; use log::{Level, LevelFilter};
use net::authentication::fetch_mojang_public_keys;
use plugin::PluginManager; use plugin::PluginManager;
use plugin::server::server_command::ServerCommandEvent; use plugin::server::server_command::ServerCommandEvent;
use pumpkin_config::{AdvancedConfiguration, BasicConfiguration}; use pumpkin_config::{AdvancedConfiguration, BasicConfiguration};
use pumpkin_macros::send_cancellable; use pumpkin_macros::send_cancellable;
use pumpkin_protocol::ConnectionState::Play;
use pumpkin_util::permission::{PermissionManager, PermissionRegistry}; use pumpkin_util::permission::{PermissionManager, PermissionRegistry};
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use rustyline::Editor; use rustyline::Editor;
@ -279,13 +277,6 @@ impl PumpkinServer {
tcp_listener = Some(listener); tcp_listener = Some(listener);
} }
if server.basic_config.allow_chat_reports {
let mojang_public_keys =
fetch_mojang_public_keys(&server.advanced_config.networking.authentication)
.unwrap();
*server.mojang_public_keys.lock().await = mojang_public_keys;
}
// Ticker // Ticker
{ {
let ticker_server = server.clone(); let ticker_server = server.clone();
@ -405,33 +396,36 @@ impl PumpkinServer {
format!("{client_addr}") format!("{client_addr}")
}; };
log::debug!("Accepted connection from Java Edition: {formatted_address} (id {client_id})"); log::debug!("Accepted connection from Java Edition: {formatted_address} (id {client_id})");
let mut java_client = JavaClient::new(connection, client_addr, client_id);
java_client.start_outgoing_packet_task();
let java_client = Arc::new(java_client);
let server_clone = self.server.clone(); let server_clone = self.server.clone();
tasks.spawn(async move { tasks.spawn(async move {
java_client.process_packets(&server_clone).await; let mut java_client = JavaClient::new(connection, client_addr, client_id);
java_client.start_outgoing_packet_task();
let login_result = java_client.handle_login_sequence(&server_clone).await;
match login_result {
PacketHandlerResult::Stop => {
java_client.close(); java_client.close();
java_client.await_tasks().await; java_client.await_tasks().await;
},
let player = java_client.player.lock().await; PacketHandlerResult::ReadyToPlay(profile,config) => {
if let Some(player) = player.as_ref() { if let Some((player, world)) = server_clone
log::debug!("Cleaning up player for id {client_id}"); .add_player(ClientPlatform::Java(java_client), profile, Some(config))
if let Err(e) = server_clone.player_data_storage
.handle_player_leave(player)
.await .await
{ {
log::error!("Failed to save player data on disconnect: {e}"); world
.spawn_java_player(&server_clone.basic_config, &player, &server_clone)
.await;
if let ClientPlatform::Java(client) = &player.client {
client.progress_player_packets(&player, &server_clone).await;
// Close when done
client.close();
client.await_tasks().await;
} }
player.remove().await; player.remove().await;
server_clone.remove_player(player).await; server_clone.remove_player(&player).await;
} else if java_client.connection_state.load() == Play { }
log::error!("No player found for id {client_id}. This should not happen!"); },
} }
}); });
} }

View File

@ -1,8 +1,11 @@
use std::{num::NonZeroU8, sync::Arc}; use std::num::NonZeroU8;
use crate::{ use crate::{
entity::player::ChatMode, entity::player::ChatMode,
net::{ClientPlatform, PlayerConfig, can_not_join, java::JavaClient}, net::{
PlayerConfig, can_not_join,
java::{JavaClient, PacketHandlerResult},
},
server::Server, server::Server,
}; };
use core::str; use core::str;
@ -189,7 +192,7 @@ impl JavaClient {
self.send_packet_now(&CFinishConfig).await; self.send_packet_now(&CFinishConfig).await;
} }
pub async fn handle_config_acknowledged(self: &Arc<Self>, server: &Server) { pub async fn handle_config_acknowledged(&self, server: &Server) -> PacketHandlerResult {
log::debug!("Handling config acknowledgement"); log::debug!("Handling config acknowledgement");
self.connection_state.store(ConnectionState::Play); self.connection_state.store(ConnectionState::Play);
@ -199,19 +202,10 @@ impl JavaClient {
if let Some(reason) = can_not_join(&profile, &address, server).await { if let Some(reason) = can_not_join(&profile, &address, server).await {
self.kick(reason).await; self.kick(reason).await;
return; return PacketHandlerResult::Stop;
} }
let config = self.config.lock().await; let config = self.config.lock().await;
PacketHandlerResult::ReadyToPlay(profile, config.clone().unwrap_or_default())
if let Some((player, world)) = server
.add_player(ClientPlatform::Java(self.clone()), profile, config.clone())
.await
{
world
.spawn_java_player(&server.basic_config, player.clone(), server)
.await;
*self.player.lock().await = Some(player);
}
} }
} }

View File

@ -79,7 +79,6 @@ pub struct JavaClient {
pub address: Mutex<SocketAddr>, pub address: Mutex<SocketAddr>,
/// The client's brand or modpack information, Optional. /// The client's brand or modpack information, Optional.
pub brand: Mutex<Option<String>>, pub brand: Mutex<Option<String>>,
pub player: Mutex<Option<Arc<Player>>>,
/// A collection of tasks associated with this client. The tasks await completion when removing the client. /// A collection of tasks associated with this client. The tasks await completion when removing the client.
tasks: TaskTracker, tasks: TaskTracker,
/// An notifier that is triggered when this client is closed. /// An notifier that is triggered when this client is closed.
@ -94,6 +93,12 @@ pub struct JavaClient {
network_reader: Mutex<TCPNetworkDecoder<BufReader<OwnedReadHalf>>>, network_reader: Mutex<TCPNetworkDecoder<BufReader<OwnedReadHalf>>>,
} }
pub enum PacketHandlerResult {
Stop,
// Signal to spawn the player
ReadyToPlay(GameProfile, PlayerConfig),
}
impl JavaClient { impl JavaClient {
#[must_use] #[must_use]
pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self { pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self {
@ -114,7 +119,6 @@ impl JavaClient {
network_writer: Arc::new(Mutex::new(TCPNetworkEncoder::new(BufWriter::new(write)))), network_writer: Arc::new(Mutex::new(TCPNetworkEncoder::new(BufWriter::new(write)))),
network_reader: Mutex::new(TCPNetworkDecoder::new(BufReader::new(read))), network_reader: Mutex::new(TCPNetworkDecoder::new(BufReader::new(read))),
brand: Mutex::new(None), brand: Mutex::new(None),
player: Mutex::new(None),
} }
} }
pub async fn set_encryption( pub async fn set_encryption(
@ -158,9 +162,15 @@ impl JavaClient {
/// # Arguments /// # Arguments
/// ///
/// * `server`: A reference to the `Server` instance. /// * `server`: A reference to the `Server` instance.
pub async fn process_packets(self: &Arc<Self>, server: &Arc<Server>) { pub async fn handle_login_sequence(&self, server: &Server) -> PacketHandlerResult {
while let Some(packet) = self.get_packet().await { while let Some(packet) = self.get_packet().await {
if let Err(error) = self.handle_packet(server, &packet).await { match self.handle_packet(server, &packet).await {
Ok(result) => {
if let Some(result) = result {
return result;
}
}
Err(error) => {
let text = format!("Error while reading incoming packet {error}"); let text = format!("Error while reading incoming packet {error}");
log::error!( log::error!(
"Failed to read incoming packet with id {}: {}", "Failed to read incoming packet with id {}: {}",
@ -171,6 +181,29 @@ impl JavaClient {
} }
} }
} }
PacketHandlerResult::Stop
}
pub async fn progress_player_packets(&self, player: &Arc<Player>, server: &Arc<Server>) {
while let Some(packet) = self.get_packet().await {
match self.handle_play_packet(player, server, &packet).await {
Ok(()) => {}
Err(e) => {
if e.is_kick() {
if let Some(kick_reason) = e.client_kick_reason() {
self.kick(TextComponent::text(kick_reason)).await;
} else {
self.kick(TextComponent::text(format!(
"Error while handling incoming packet {e}"
)))
.await;
}
}
e.log();
}
}
}
}
pub async fn await_tasks(&self) { pub async fn await_tasks(&self) {
self.tasks.close(); self.tasks.close();
@ -327,10 +360,10 @@ impl JavaClient {
/// ///
/// Returns a `DeserializerError` if an error occurs during packet deserialization. /// Returns a `DeserializerError` if an error occurs during packet deserialization.
pub async fn handle_packet( pub async fn handle_packet(
self: &Arc<Self>, &self,
server: &Arc<Server>, server: &Server,
packet: &RawPacket, packet: &RawPacket,
) -> Result<(), ReadingError> { ) -> Result<Option<PacketHandlerResult>, ReadingError> {
match self.connection_state.load() { match self.connection_state.load() {
ConnectionState::HandShake => self.handle_handshake_packet(packet).await, ConnectionState::HandShake => self.handle_handshake_packet(packet).await,
ConnectionState::Status => self.handle_status_packet(server, packet).await, ConnectionState::Status => self.handle_status_packet(server, packet).await,
@ -339,37 +372,20 @@ impl JavaClient {
self.handle_login_packet(server, packet).await self.handle_login_packet(server, packet).await
} }
ConnectionState::Config => self.handle_config_packet(server, packet).await, ConnectionState::Config => self.handle_config_packet(server, packet).await,
ConnectionState::Play => { ConnectionState::Play => Ok(None),
if let Some(player) = self.player.lock().await.as_ref() {
match self.handle_play_packet(player, server, packet).await {
Ok(()) => {}
Err(e) => {
if e.is_kick() {
if let Some(kick_reason) = e.client_kick_reason() {
self.kick(TextComponent::text(kick_reason)).await;
} else {
self.kick(TextComponent::text(format!(
"Error while handling incoming packet {e}"
)))
.await;
}
}
e.log();
}
}
}
Ok(())
}
} }
} }
async fn handle_handshake_packet(&self, packet: &RawPacket) -> Result<(), ReadingError> { async fn handle_handshake_packet(
&self,
packet: &RawPacket,
) -> Result<Option<PacketHandlerResult>, ReadingError> {
log::debug!("Handling handshake group"); log::debug!("Handling handshake group");
let payload = &packet.payload[..]; let payload = &packet.payload[..];
match packet.id { match packet.id {
0 => { 0 => {
self.handle_handshake(SHandShake::read(payload)?).await; self.handle_handshake(SHandShake::read(payload)?).await;
Ok(()) Ok(None)
} }
_ => Err(ReadingError::Message(format!( _ => Err(ReadingError::Message(format!(
"Failed to handle packet id {} in Handshake State", "Failed to handle packet id {} in Handshake State",
@ -382,18 +398,18 @@ impl JavaClient {
&self, &self,
server: &Server, server: &Server,
packet: &RawPacket, packet: &RawPacket,
) -> Result<(), ReadingError> { ) -> Result<Option<PacketHandlerResult>, ReadingError> {
log::debug!("Handling status group"); log::debug!("Handling status group");
let payload = &packet.payload[..]; let payload = &packet.payload[..];
match packet.id { match packet.id {
SStatusRequest::PACKET_ID => { SStatusRequest::PACKET_ID => {
self.handle_status_request(server).await; self.handle_status_request(server).await;
Ok(()) Ok(None)
} }
SStatusPingRequest::PACKET_ID => { SStatusPingRequest::PACKET_ID => {
self.handle_ping_request(SStatusPingRequest::read(payload)?) self.handle_ping_request(SStatusPingRequest::read(payload)?)
.await; .await;
Ok(()) Ok(None)
} }
_ => Err(ReadingError::Message(format!( _ => Err(ReadingError::Message(format!(
"Failed to handle java client packet id {} in Status State", "Failed to handle java client packet id {} in Status State",
@ -456,7 +472,7 @@ impl JavaClient {
&self, &self,
server: &Server, server: &Server,
packet: &RawPacket, packet: &RawPacket,
) -> Result<(), ReadingError> { ) -> Result<Option<PacketHandlerResult>, ReadingError> {
log::debug!("Handling login group for id"); log::debug!("Handling login group for id");
let payload = &packet.payload[..]; let payload = &packet.payload[..];
match packet.id { match packet.id {
@ -485,14 +501,14 @@ impl JavaClient {
); );
} }
} }
Ok(()) Ok(None)
} }
async fn handle_config_packet( async fn handle_config_packet(
self: &Arc<Self>, &self,
server: &Server, server: &Server,
packet: &RawPacket, packet: &RawPacket,
) -> Result<(), ReadingError> { ) -> Result<Option<PacketHandlerResult>, ReadingError> {
log::debug!("Handling config group"); log::debug!("Handling config group");
let payload = &packet.payload[..]; let payload = &packet.payload[..];
match packet.id { match packet.id {
@ -505,7 +521,7 @@ impl JavaClient {
.await; .await;
} }
SAcknowledgeFinishConfig::PACKET_ID => { SAcknowledgeFinishConfig::PACKET_ID => {
self.handle_config_acknowledged(server).await; return Ok(Some(self.handle_config_acknowledged(server).await));
} }
SKnownPacks::PACKET_ID => { SKnownPacks::PACKET_ID => {
self.handle_known_packs(SKnownPacks::read(payload)?).await; self.handle_known_packs(SKnownPacks::read(payload)?).await;
@ -524,7 +540,7 @@ impl JavaClient {
); );
} }
} }
Ok(()) Ok(None)
} }
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]

View File

@ -1407,6 +1407,12 @@ impl JavaClient {
if player.wait_for_keep_alive.load(Ordering::Relaxed) if player.wait_for_keep_alive.load(Ordering::Relaxed)
&& keep_alive.keep_alive_id == player.keep_alive_id.load(Ordering::Relaxed) && keep_alive.keep_alive_id == player.keep_alive_id.load(Ordering::Relaxed)
{ {
let ping = player.last_keep_alive_time.load().elapsed();
// Vanilla logic
player.ping.store(
(player.ping.load(Ordering::Relaxed) * 3 + ping.as_millis() as u32) / 4,
Ordering::Relaxed,
);
player.wait_for_keep_alive.store(false, Ordering::Relaxed); player.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else { } else {
self.kick(TextComponent::text( self.kick(TextComponent::text(

View File

@ -86,15 +86,10 @@ impl Default for PlayerConfig {
} }
} }
pub enum PacketHandlerState {
PacketReady,
Stop,
}
/// This is just a Wrapper for both Java & Bedrock connections /// This is just a Wrapper for both Java & Bedrock connections
#[derive(Clone)] #[expect(clippy::large_enum_variant)]
pub enum ClientPlatform { pub enum ClientPlatform {
Java(Arc<JavaClient>), Java(JavaClient),
Bedrock(Arc<BedrockClient>), Bedrock(Arc<BedrockClient>),
} }
@ -119,7 +114,7 @@ impl ClientPlatform {
/// This function should only be used where you know that the client is java! /// This function should only be used where you know that the client is java!
#[inline] #[inline]
#[must_use] #[must_use]
pub fn java(&self) -> &Arc<JavaClient> { pub fn java(&self) -> &JavaClient {
if let Self::Java(client) = self { if let Self::Java(client) = self {
return client; return client;
} }

View File

@ -4,6 +4,7 @@ use crate::command::commands::defaultgamemode::DefaultGamemode;
use crate::data::player_server::ServerPlayerData; use crate::data::player_server::ServerPlayerData;
use crate::entity::{EntityBase, NBTStorage}; use crate::entity::{EntityBase, NBTStorage};
use crate::item::registry::ItemRegistry; use crate::item::registry::ItemRegistry;
use crate::net::authentication::fetch_mojang_public_keys;
use crate::net::{ClientPlatform, DisconnectReason, EncryptionError, GameProfile, PlayerConfig}; use crate::net::{ClientPlatform, DisconnectReason, EncryptionError, GameProfile, PlayerConfig};
use crate::plugin::player::player_login::PlayerLoginEvent; use crate::plugin::player::player_login::PlayerLoginEvent;
use crate::plugin::server::server_broadcast::ServerBroadcastEvent; use crate::plugin::server::server_broadcast::ServerBroadcastEvent;
@ -39,7 +40,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32};
use std::{future::Future, sync::atomic::Ordering, time::Duration}; use std::{future::Future, sync::atomic::Ordering, time::Duration};
use tokio::sync::{Mutex, OnceCell, RwLock}; use tokio::sync::{Mutex, OnceCell, RwLock};
use tokio::task::JoinHandle; use tokio::task::{JoinHandle, JoinSet};
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
mod connection_cache; mod connection_cache;
@ -172,6 +173,12 @@ impl Server {
let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps)); let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps));
let mojang_public_keys = if basic_config.allow_chat_reports {
fetch_mojang_public_keys(&advanced_config.networking.authentication).unwrap()
} else {
Vec::new()
};
let server = Self { let server = Self {
basic_config, basic_config,
advanced_config, advanced_config,
@ -198,7 +205,7 @@ impl Server {
tick_count: AtomicI32::new(0), tick_count: AtomicI32::new(0),
tasks: TaskTracker::new(), tasks: TaskTracker::new(),
server_guid: rand::random(), server_guid: rand::random(),
mojang_public_keys: Mutex::new(Vec::new()), mojang_public_keys: Mutex::new(mojang_public_keys),
world_info_writer: Arc::new(AnvilLevelInfo), world_info_writer: Arc::new(AnvilLevelInfo),
level_info: level_info.clone(), level_info: level_info.clone(),
_locker: Arc::new(locker), _locker: Arc::new(locker),
@ -697,21 +704,21 @@ impl Server {
} }
/// Ticks the game logic for all worlds. This is the part that is affected by `/tick freeze`. /// Ticks the game logic for all worlds. This is the part that is affected by `/tick freeze`.
pub async fn tick_worlds(self: &Arc<Self>) { pub async fn tick_worlds(self: &Arc<Self>) {
let worlds = self.worlds.read().await.clone(); let worlds = self.worlds.read().await;
let mut handles = Vec::with_capacity(worlds.len()); let mut set = JoinSet::new();
for world in &worlds {
for world in worlds.iter() {
let world = world.clone(); let world = world.clone();
let server = self.clone(); let server = self.clone();
handles.push(tokio::spawn(async move {
set.spawn(async move {
world.tick(&server).await; world.tick(&server).await;
})); });
}
for handle in handles {
// Wait for all world ticks to complete
let _ = handle.await;
} }
// Global periodic tasks set.join_all().await;
// Global tasks
if let Err(e) = self.player_data_storage.tick(self).await { if let Err(e) = self.player_data_storage.tick(self).await {
log::error!("Error ticking player data: {e}"); log::error!("Error ticking player data: {e}");
} }

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
CInitializeWorldBorder, CSetBorderCenter, CSetBorderLerpSize, CSetBorderSize, CInitializeWorldBorder, CSetBorderCenter, CSetBorderLerpSize, CSetBorderSize,
CSetBorderWarningDelay, CSetBorderWarningDistance, CSetBorderWarningDelay, CSetBorderWarningDistance,
@ -46,7 +44,7 @@ impl Worldborder {
} }
} }
pub async fn init_client(&self, client: &Arc<JavaClient>) { pub async fn init_client(&self, client: &JavaClient) {
client client
.enqueue_packet(&CInitializeWorldBorder::new( .enqueue_packet(&CInitializeWorldBorder::new(
self.center_x, self.center_x,

View File

@ -1427,7 +1427,7 @@ impl World {
pub async fn spawn_java_player( pub async fn spawn_java_player(
&self, &self,
base_config: &BasicConfiguration, base_config: &BasicConfiguration,
player: Arc<Player>, player: &Arc<Player>,
server: &Server, server: &Server,
) { ) {
let dimensions: Vec<ResourceLocation> = server let dimensions: Vec<ResourceLocation> = server
@ -1478,10 +1478,7 @@ impl World {
.await; .await;
// Send the current ticking state to the new player so they are in sync. // Send the current ticking state to the new player so they are in sync.
server server.tick_rate_manager.update_joining_player(player).await;
.tick_rate_manager
.update_joining_player(&player)
.await;
// Permissions, i.e. the commands a player may use. // Permissions, i.e. the commands a player may use.
player.send_permission_lvl_update().await; player.send_permission_lvl_update().await;
@ -1490,12 +1487,12 @@ impl World {
player.send_difficulty_update().await; player.send_difficulty_update().await;
{ {
let command_dispatcher = server.command_dispatcher.read().await; let command_dispatcher = server.command_dispatcher.read().await;
client_suggestions::send_c_commands_packet(&player, &command_dispatcher).await; client_suggestions::send_c_commands_packet(player, &command_dispatcher).await;
}; };
// Spawn in initial chunks // Spawn in initial chunks
// This is made before the player teleport so that the player doesn't glitch out when spawning // This is made before the player teleport so that the player doesn't glitch out when spawning
chunker::update_position(&player).await; chunker::update_position(player).await;
// Teleport // Teleport
let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) {
@ -1775,7 +1772,7 @@ impl World {
.await; .await;
player.send_active_effects().await; player.send_active_effects().await;
self.send_player_equipment(&player).await; self.send_player_equipment(player).await;
} }
async fn send_player_equipment(&self, from: &Player) { async fn send_player_equipment(&self, from: &Player) {
@ -2370,16 +2367,13 @@ impl World {
/// ///
/// - This function assumes `broadcast_packet_expect` and `remove_entity` are defined elsewhere. /// - This function assumes `broadcast_packet_expect` and `remove_entity` are defined elsewhere.
/// - The disconnect message sending is currently optional. Consider making it a configurable option. /// - The disconnect message sending is currently optional. Consider making it a configurable option.
pub async fn remove_player(&self, player: &Arc<Player>, fire_event: bool) { pub async fn remove_player(
if self &self,
.players player: &Arc<Player>,
.write() fire_event: bool,
.await ) -> Option<Arc<Player>> {
.remove(&player.gameprofile.id) let player = self.players.write().await.remove(&player.gameprofile.id);
.is_none() if let Some(player) = player {
{
return;
}
let uuid = player.gameprofile.id; let uuid = player.gameprofile.id;
self.broadcast_packet_all(&CRemovePlayerInfo::new(&[uuid])) self.broadcast_packet_all(&CRemovePlayerInfo::new(&[uuid]))
.await; .await;
@ -2406,6 +2400,8 @@ impl World {
} }
} }
} }
None
}
/// Adds an entity to the world. /// Adds an entity to the world.
pub async fn spawn_entity(&self, entity: Arc<dyn EntityBase>) { pub async fn spawn_entity(&self, entity: Arc<dyn EntityBase>) {