use crate::block::registry::BlockRegistry; use crate::command::commands::default_dispatcher; use crate::command::commands::defaultgamemode::DefaultGamemode; use crate::data::player_server_data::ServerPlayerData; use crate::entity::NBTStorage; use crate::item::registry::ItemRegistry; use crate::net::EncryptionError; use crate::plugin::player::player_login::PlayerLoginEvent; use crate::plugin::server::server_broadcast::ServerBroadcastEvent; use crate::world::custom_bossbar::CustomBossbars; use crate::{ command::dispatcher::CommandDispatcher, entity::player::Player, net::Client, world::World, }; use bytes::Bytes; use connection_cache::{CachedBranding, CachedStatus}; use key_store::KeyStore; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_macros::send_cancellable; use pumpkin_protocol::client::login::CEncryptionRequest; use pumpkin_protocol::client::play::CChangeDifficulty; use pumpkin_protocol::{ClientPacket, client::config::CPluginMessage}; use pumpkin_registry::{DimensionType, Registry}; use pumpkin_util::Difficulty; use pumpkin_util::math::vector2::Vector2; use pumpkin_util::text::TextComponent; use pumpkin_world::dimension::Dimension; use pumpkin_world::lock::LevelLocker; use pumpkin_world::lock::anvil::AnvilLevelLocker; use pumpkin_world::world_info::anvil::{ AnvilLevelInfo, LEVEL_DAT_BACKUP_FILE_NAME, LEVEL_DAT_FILE_NAME, }; use pumpkin_world::world_info::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter}; use rand::seq::IndexedRandom; use rsa::RsaPublicKey; use std::fs; use std::net::IpAddr; use std::sync::atomic::{AtomicBool, AtomicU32}; use std::{ sync::{Arc, atomic::Ordering}, time::Duration, }; use tokio::sync::{Mutex, RwLock}; use tokio::task::JoinHandle; use tokio_util::task::TaskTracker; mod connection_cache; mod key_store; pub mod seasonal_events; pub mod ticker; pub const CURRENT_MC_VERSION: &str = "1.21.5"; /// Represents a Minecraft server instance. pub struct Server { /// Handles cryptographic keys for secure communication. key_store: KeyStore, /// Manages server status information. listing: Mutex, /// Saves server branding information. branding: CachedBranding, /// Saves and dispatches commands to appropriate handlers. pub command_dispatcher: RwLock, /// Block behaviour. pub block_registry: Arc, /// Item behaviour. pub item_registry: Arc, /// Manages multiple worlds within the server. pub worlds: RwLock>>, /// All the dimensions that exist on the server. pub dimensions: Vec, /// Caches game registries for efficient access. pub cached_registry: Vec, /// Assigns unique IDs to containers. container_id: AtomicU32, /// Manages authentication with an authentication server, if enabled. pub auth_client: Option, /// Mojang's public keys, used for chat session signing /// Pulled from Mojang API on startup pub mojang_public_keys: Mutex>, /// The server's custom bossbars pub bossbars: Mutex, /// The default gamemode when a player joins the server (reset every restart) pub defaultgamemode: Mutex, /// Manages player data storage pub player_data_storage: ServerPlayerData, // Whether the server whitelist is on or off pub white_list: AtomicBool, tasks: TaskTracker, // world stuff which maybe should be put into a struct pub level_info: Arc>, world_info_writer: Arc, // Gets unlocked when dropped // TODO: Make this a trait _locker: Arc, } impl Server { #[allow(clippy::new_without_default)] #[must_use] pub async fn new() -> Self { let auth_client = BASIC_CONFIG.online_mode.then(|| { reqwest::Client::builder() .connect_timeout(Duration::from_millis(u64::from( advanced_config().networking.authentication.connect_timeout, ))) .read_timeout(Duration::from_millis(u64::from( advanced_config().networking.authentication.read_timeout, ))) .build() .expect("Failed to to make reqwest client") }); // First register the default commands. After that, plugins can put in their own. let command_dispatcher = RwLock::new(default_dispatcher().await); let world_path = BASIC_CONFIG.get_world_path(); let block_registry = super::block::default_registry(); let level_info = AnvilLevelInfo.read_world_info(&world_path); if let Err(error) = &level_info { match error { // If it doesn't exist, just make a new one WorldInfoError::InfoNotFound => (), WorldInfoError::UnsupportedVersion(version) => { log::error!("Failed to load world info!, {version}"); log::error!("{error}"); panic!("Unsupported world data! See the logs for more info."); } e => { panic!("World Error {e}"); } } } else { let dat_path = world_path.join(LEVEL_DAT_FILE_NAME); if dat_path.exists() { let backup_path = world_path.join(LEVEL_DAT_BACKUP_FILE_NAME); fs::copy(dat_path, backup_path).unwrap(); } } let level_info = level_info.unwrap_or_default(); // TODO: Improve error handling let seed = level_info.world_gen_settings.seed; log::info!("Loading Overworld: {seed}"); let overworld = World::load( Dimension::Overworld.into_level(world_path.clone(), block_registry.clone(), seed), level_info.clone(), DimensionType::Overworld, block_registry.clone(), ); log::info!("Loading Nether: {seed}"); let nether = World::load( Dimension::Nether.into_level(world_path.clone(), block_registry.clone(), seed), level_info.clone(), DimensionType::TheNether, block_registry.clone(), ); log::info!("Loading End: {seed}"); let end = World::load( Dimension::End.into_level(world_path.clone(), block_registry.clone(), seed), level_info.clone(), DimensionType::TheEnd, block_registry.clone(), ); // if we fail to lock, lets crash ???. maybe not the best solution when we have a large server with many worlds and one is locked. // So TODO let locker = AnvilLevelLocker::lock(&world_path).expect("Failed to lock level"); let world_name = world_path.to_str().unwrap(); Self { cached_registry: Registry::get_synced(), container_id: 0.into(), worlds: RwLock::new(vec![Arc::new(overworld), Arc::new(nether), Arc::new(end)]), dimensions: vec![ DimensionType::Overworld, DimensionType::OverworldCaves, DimensionType::TheNether, DimensionType::TheEnd, ], command_dispatcher, block_registry, item_registry: super::item::items::default_registry(), auth_client, key_store: KeyStore::new(), listing: Mutex::new(CachedStatus::new()), branding: CachedBranding::new(), bossbars: Mutex::new(CustomBossbars::new()), defaultgamemode: Mutex::new(DefaultGamemode { gamemode: BASIC_CONFIG.default_gamemode, }), player_data_storage: ServerPlayerData::new( format!("{world_name}/playerdata"), Duration::from_secs(advanced_config().player_data.save_player_cron_interval), ), white_list: AtomicBool::new(BASIC_CONFIG.white_list), tasks: TaskTracker::new(), mojang_public_keys: Mutex::new(Vec::new()), world_info_writer: Arc::new(AnvilLevelInfo), level_info: Arc::new(RwLock::new(level_info)), _locker: Arc::new(locker), } } const SPAWN_CHUNK_RADIUS: i32 = 1; #[must_use] pub fn spawn_chunks() -> Box<[Vector2]> { (-Self::SPAWN_CHUNK_RADIUS..=Self::SPAWN_CHUNK_RADIUS) .flat_map(|x| { (-Self::SPAWN_CHUNK_RADIUS..=Self::SPAWN_CHUNK_RADIUS) .map(move |z| Vector2::new(x, z)) }) .collect() } /// Spawns a task associated with this server. All tasks spawned with this method are awaited /// when the server stops. This means tasks should complete in a reasonable (no looping) amount of time. pub fn spawn_task(&self, task: F) -> JoinHandle where F: Future + Send + 'static, F::Output: Send + 'static, { self.tasks.spawn(task) } pub async fn get_world_from_dimension(&self, dimension: DimensionType) -> Arc { // TODO: this is really bad let world_guard = self.worlds.read().await; let world = match dimension { DimensionType::Overworld => world_guard.first(), DimensionType::OverworldCaves => todo!(), DimensionType::TheEnd => world_guard.get(2), DimensionType::TheNether => world_guard.get(1), }; world.cloned().unwrap() } #[allow(clippy::if_then_some_else_none)] /// Adds a new player to the server. /// /// This function takes an `Arc` representing the connected client and performs the following actions: /// /// 1. Generates a new entity ID for the player. /// 2. Determines the player's gamemode (defaulting to Survival if not specified in configuration). /// 3. **(TODO: Select default from config)** Selects the world for the player (currently uses the first world). /// 4. Creates a new `Player` instance using the provided information. /// 5. Adds the player to the chosen world. /// 6. **(TODO: Config if we want increase online)** Optionally updates server listing information based on the player's configuration. /// /// # Arguments /// /// * `client`: An `Arc` representing the connected client. /// /// # Returns /// /// A tuple containing: /// /// - `Arc`: A reference to the newly created player object. /// - `Arc`: A reference to the world the player was added to. /// /// # Note /// /// You still have to spawn the `Player` in a `World` to let them join and make them visible. pub async fn add_player(&self, client: Client) -> Option<(Arc, Arc)> { let gamemode = self.defaultgamemode.lock().await.gamemode; let uuid = client.gameprofile.lock().await.as_ref().unwrap().id; let (world, nbt) = if let Ok(Some(data)) = self.player_data_storage.load_data(&uuid) { if let Some(dimension_key) = data.get_string("Dimension") { if let Some(dimension) = DimensionType::from_name(dimension_key) { let world = self.get_world_from_dimension(dimension).await; (world, Some(data)) } else { log::warn!("Invalid dimension key in player data: {dimension_key}"); let default_world_guard = self.worlds.read().await; let default_world = default_world_guard .first() .expect("Default world should exist"); (default_world.clone(), Some(data)) } } else { // Player data exists but doesn't have a "Dimension" key. let default_world_guard = self.worlds.read().await; let default_world = default_world_guard .first() .expect("Default world should exist"); (default_world.clone(), Some(data)) } } else { // No player data found or an error occurred, default to the Overworld. let default_world_guard = self.worlds.read().await; let default_world = default_world_guard .first() .expect("Default world should exist"); (default_world.clone(), None) }; let mut player = Player::new(client, world.clone(), gamemode).await; if let Some(mut nbt_data) = nbt { player.read_nbt(&mut nbt_data).await; } // Wrap in Arc after data is loaded let player = Arc::new(player); send_cancellable! {{ PlayerLoginEvent::new(player.clone(), TextComponent::text("You have been kicked from the server")); 'after: { player.screen_handler_sync_handler.store_player(player.clone()).await; if world .add_player(player.gameprofile.id, player.clone()) .await.is_ok() { // TODO: Config if we want increase online if let Some(config) = player.client.config.lock().await.as_ref() { // TODO: Config so we can also just ignore this hehe if config.server_listing { self.listing.lock().await.add_player(&player); } } Some((player, world.clone())) } else { None } } 'cancelled: { player.kick(event.kick_message).await; None } }} } pub async fn remove_player(&self, player: &Player) { // TODO: Config if we want decrease online self.listing.lock().await.remove_player(player); } pub async fn shutdown(&self) { self.tasks.close(); log::debug!("Awaiting tasks for server"); self.tasks.wait().await; log::debug!("Done awaiting tasks for server"); log::info!("Starting worlds"); for world in self.worlds.read().await.iter() { world.shutdown().await; } // then lets save the world info if let Err(err) = self.world_info_writer.write_world_info( &*self.level_info.read().await, &BASIC_CONFIG.get_world_path(), ) { log::error!("Failed to save level.dat: {err}"); } log::info!("Completed worlds"); } /// Broadcasts a packet to all players in all worlds. /// /// This function sends the specified packet to every connected player in every world managed by the server. /// /// # Arguments /// /// * `packet`: A reference to the packet to be broadcast. The packet must implement the `ClientPacket` trait. pub async fn broadcast_packet_all

(&self, packet: &P) where P: ClientPacket, { let mut packet_buf = Vec::new(); if let Err(err) = packet.write(&mut packet_buf) { log::error!("Failed to serialize packet {}: {}", P::PACKET_ID, err); return; } let packet_data: Bytes = packet_buf.into(); for world in self.worlds.read().await.iter() { let current_players = world.players.read().await; for player in current_players.values() { player.client.enqueue_packet_data(packet_data.clone()).await; } } } pub async fn broadcast_message( &self, message: &TextComponent, sender_name: &TextComponent, chat_type: u8, target_name: Option<&TextComponent>, ) { send_cancellable! {{ ServerBroadcastEvent::new(message.clone(), sender_name.clone()); 'after: { for world in self.worlds.read().await.iter() { world .broadcast_message(&event.message, &event.sender, chat_type, target_name) .await; } } }} } /// Sets the difficulty of the server. /// /// This function updates the difficulty level of the server and broadcasts the change to all players. /// It also iterates through all worlds to ensure the difficulty is applied consistently. /// If `force_update` is `Some(true)`, the difficulty will be set regardless of the current state. /// If `force_update` is `Some(false)` or `None`, the difficulty will only be updated if it is not locked. /// /// # Arguments /// /// * `difficulty`: The new difficulty level to set. This should be one of the variants of the `Difficulty` enum. /// * `force_update`: An optional boolean that, if set to `Some(true)`, forces the difficulty to be updated even if it is currently locked. /// /// # Note /// /// This function does not handle the actual mob spawn options update, which is a TODO item for future implementation. pub async fn set_difficulty(&self, difficulty: Difficulty, force_update: Option) { let mut level_info = self.level_info.write().await; if force_update.unwrap_or_default() || !level_info.difficulty_locked { level_info.difficulty = if BASIC_CONFIG.hardcore { Difficulty::Hard } else { difficulty }; // Minecraft server updates mob spawn options here // but its not implemented in Pumpkin yet // todo: update mob spawn options for world in &*self.worlds.read().await { world.level_info.write().await.difficulty = level_info.difficulty; } self.broadcast_packet_all(&CChangeDifficulty::new( level_info.difficulty as u8, level_info.difficulty_locked, )) .await; } } /// Searches for a player by their username across all worlds. /// /// This function iterates through each world managed by the server and attempts to find a player with the specified username. /// If a player is found in any world, it returns an `Arc` reference to that player. Otherwise, it returns `None`. /// /// # Arguments /// /// * `name`: The username of the player to search for. /// /// # Returns /// /// An `Option>` containing the player if found, or `None` if not found. pub async fn get_player_by_name(&self, name: &str) -> Option> { for world in self.worlds.read().await.iter() { if let Some(player) = world.get_player_by_name(name).await { return Some(player); } } None } pub async fn get_players_by_ip(&self, ip: IpAddr) -> Vec> { let mut players = Vec::>::new(); for world in self.worlds.read().await.iter() { for player in world.players.read().await.values() { if player.client.address.lock().await.ip() == ip { players.push(player.clone()); } } } players } /// Returns all players from all worlds. pub async fn get_all_players(&self) -> Vec> { let mut players = Vec::>::new(); for world in self.worlds.read().await.iter() { for player in world.players.read().await.values() { players.push(player.clone()); } } players } /// Returns a random player from any of the worlds, or `None` if all worlds are empty. pub async fn get_random_player(&self) -> Option> { let players = self.get_all_players().await; players.choose(&mut rand::rng()).map(Arc::<_>::clone) } /// Searches for a player by their UUID across all worlds. /// /// This function iterates through each world managed by the server and attempts to find a player with the specified UUID. /// If a player is found in any world, it returns an `Arc` reference to that player. Otherwise, it returns `None`. /// /// # Arguments /// /// * `id`: The UUID of the player to search for. /// /// # Returns /// /// An `Option>` containing the player if found, or `None` if not found. pub async fn get_player_by_uuid(&self, id: uuid::Uuid) -> Option> { for world in self.worlds.read().await.iter() { if let Some(player) = world.get_player_by_uuid(id).await { return Some(player); } } None } /// Counts the total number of players across all worlds. /// /// This function iterates through each world and sums up the number of players currently connected to that world. /// /// # Returns /// /// The total number of players connected to the server. pub async fn get_player_count(&self) -> usize { let mut count = 0; for world in self.worlds.read().await.iter() { count += world.players.read().await.len(); } count } /// Similar to [`Server::get_player_count`] >= n, but may be more efficient since it stops its iteration through all worlds as soon as n players were found. pub async fn has_n_players(&self, n: usize) -> bool { let mut count = 0; for world in self.worlds.read().await.iter() { count += world.players.read().await.len(); if count >= n { return true; } } false } /// Generates a new container id. pub fn new_container_id(&self) -> u32 { self.container_id.fetch_add(1, Ordering::SeqCst) } pub fn get_branding(&self) -> CPluginMessage<'_> { self.branding.get_branding() } pub fn get_status(&self) -> &Mutex { &self.listing } pub fn encryption_request<'a>( &'a self, verification_token: &'a [u8; 4], should_authenticate: bool, ) -> CEncryptionRequest<'a> { self.key_store .encryption_request("", verification_token, should_authenticate) } pub fn decrypt(&self, data: &[u8]) -> Result, EncryptionError> { self.key_store.decrypt(data) } pub fn digest_secret(&self, secret: &[u8]) -> String { self.key_store.get_digest(secret) } async fn tick(&self) { for world in self.worlds.read().await.iter() { world.tick(self).await; } if let Err(e) = self.player_data_storage.tick(self).await { log::error!("Error ticking player data: {e}"); } } }