use crate::{ entity::{NBTStorage, player::Player}, server::Server, }; use crossbeam::atomic::AtomicCell; use pumpkin_inventory::screen_handler::ScreenHandler; use pumpkin_nbt::pnbt::PNbtCompound; use pumpkin_world::data::player_data::{PlayerDataError, PlayerDataStorage}; use std::sync::Arc; use std::{ path::PathBuf, time::{Duration, Instant}, }; use tracing::{debug, error}; /// Helper for managing player data in the server context. /// /// This struct provides server-wide access to the `PlayerDataStorage` and /// convenience methods for player handling. pub struct ServerPlayerData { storage: Arc, save_interval: Duration, last_save: AtomicCell, } impl ServerPlayerData { /// Creates a new `ServerPlayerData` with specified configuration. pub fn new(data_path: impl Into, save_interval: Duration, enabled: bool) -> Self { Self { storage: Arc::new(PlayerDataStorage::new(data_path, enabled)), save_interval, last_save: AtomicCell::new(Instant::now()), } } /// Handles a player leaving the server. /// /// This function saves player data when they disconnect. /// /// # Arguments /// /// * `player` - The player who left. /// /// # Returns /// /// A Result indicating success or the error that occurred. pub async fn handle_player_leave(&self, player: &Player) -> Result<(), PlayerDataError> { player .player_screen_handler .lock() .await .on_closed(player) .await; player.on_handled_screen_closed().await; let mut nbt = PNbtCompound::new(); player.write_nbt(&mut nbt).await; let storage = self.storage.clone(); let uuid = player.gameprofile.id; // Save to disk tokio::task::spawn_blocking(move || storage.save_player_data(&uuid, nbt)) .await .expect("Player data save panicked")?; Ok(()) } /// Performs periodic maintenance tasks. /// /// This function should be called regularly to save player data and clean /// expired cache entries. pub async fn tick(&self, server: &Server) -> Result<(), PlayerDataError> { let now = Instant::now(); // Only save players periodically based on save_interval let last_save = self.last_save.load(); let should_save = now.duration_since(last_save) >= self.save_interval; if should_save && self.storage.is_save_enabled() { self.last_save.store(now); // Save all online players periodically across all worlds for world in server.worlds.load().iter() { for player in world.players.load().iter() { let mut nbt = PNbtCompound::new(); player.write_nbt(&mut nbt).await; let storage = self.storage.clone(); let uuid = player.gameprofile.id; // Save to disk periodically to prevent data loss on server crash if let Err(e) = tokio::task::spawn_blocking(move || storage.save_player_data(&uuid, nbt)) .await .expect("Player data periodic save panicked") { error!( "Failed to save player data for {}: {e}", player.gameprofile.id, ); } } } debug!("Periodic player data save completed"); } Ok(()) } /// Saves all players' data immediately. /// /// This function immediately saves all online players' data to disk. /// Useful for server shutdown or backup operations. pub async fn save_all_players(&self, server: &Server) -> Result<(), PlayerDataError> { let mut total_players = 0; // Save players from all worlds for world in server.worlds.load().iter() { for player in world.players.load().iter() { self.extract_data_and_save_player(player).await?; total_players += 1; } } debug!("Saved data for {total_players} online players"); Ok(()) } /// Loads player data and applies it to a player. /// /// This function loads a player's data and applies it to their Player instance. /// For new players, it creates default data without errors. /// /// # Arguments /// /// * `player` - The player to load data for and apply to. /// /// # Returns /// /// A Result indicating success or the error that occurred. pub async fn load_data( &self, uuid: &uuid::Uuid, ) -> Result, PlayerDataError> { let storage = self.storage.clone(); let uuid = *uuid; let result = tokio::task::spawn_blocking(move || storage.load_player_data(&uuid)) .await .expect("Player data load panicked"); match result { Ok((should_load, data)) => { if !should_load { // No data to load, continue with default data return Ok(None); } Ok(Some(data)) } Err(e) => { if self.storage.is_save_enabled() { // Only log as error if player data saving is enabled error!("Error loading player data for {uuid}: {e}"); } else { // Otherwise just log as info since it's expected debug!("Not loading player data for {uuid} (saving disabled)"); } // Continue with default data even if there's an error Ok(None) } } } /// Extracts and saves data from a player. /// /// This function extracts NBT data from a player and saves it to disk. /// /// # Arguments /// /// * `player` - The player to extract and save data for. /// /// # Returns /// /// A Result indicating success or the error that occurred. pub async fn extract_data_and_save_player( &self, player: &Player, ) -> Result<(), PlayerDataError> { if !self.storage.is_save_enabled() { return Ok(()); } let uuid = player.gameprofile.id; let mut nbt = PNbtCompound::new(); player.write_nbt(&mut nbt).await; let storage = self.storage.clone(); tokio::task::spawn_blocking(move || storage.save_player_data(&uuid, nbt)) .await .expect("Player data extract and save panicked")?; Ok(()) } } #[cfg(test)] mod test { use crate::data::player_server::ServerPlayerData; use pumpkin_nbt::pnbt::PNbtCompound; use pumpkin_world::data::player_data::PlayerDataStorage; use std::time::Duration; use std::time::Instant; use tempfile::tempdir; use uuid::Uuid; #[tokio::test] async fn player_data_storage_new() { // Create a temporary directory for testing let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let storage = PlayerDataStorage::new(path.clone(), true); assert_eq!(storage.get_data_path().as_path(), path.as_path()); // Note: save_enabled might be configured differently in your actual code } #[tokio::test] async fn player_data_storage_get_player_data_path() { let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let storage = PlayerDataStorage::new(path.clone(), true); let uuid = Uuid::new_v4(); let expected_path = path.join(format!("{uuid}.dat")); assert_eq!(storage.get_player_data_path(&uuid), expected_path); } #[tokio::test] async fn player_data_storage_save_and_load() { let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let storage = PlayerDataStorage::new(path, true); // Ensure saving is enabled for this test let uuid = Uuid::new_v4(); // Create test data let mut nbt = PNbtCompound::new(); nbt.put_string("TestValue"); nbt.put_i32(42); // Save the data storage.save_player_data(&uuid, nbt).unwrap(); // Load the data let (load_success, mut loaded_nbt) = storage.load_player_data(&uuid).unwrap(); assert!(load_success); assert_eq!(loaded_nbt.get_string().unwrap(), "TestValue"); assert_eq!(loaded_nbt.get_i32().unwrap(), 42); } #[tokio::test] async fn player_data_storage_load_nonexistent() { let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let storage = PlayerDataStorage::new(path, true); // Ensure saving is enabled for this test let uuid = Uuid::new_v4(); // Try to load non-existent data let (load_success, empty_nbt) = storage.load_player_data(&uuid).unwrap(); assert!(!load_success); assert_eq!(empty_nbt.as_bytes().len(), 0); } #[tokio::test] async fn player_data_storage_disabled() { let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let storage = PlayerDataStorage::new(path, false); let uuid = Uuid::new_v4(); let mut nbt = PNbtCompound::new(); nbt.put_string("TestValue"); // Save should succeed but do nothing let save_result = storage.save_player_data(&uuid, nbt); assert!(save_result.is_ok()); // Load should return empty data let (load_success, empty_nbt) = storage.load_player_data(&uuid).unwrap(); assert!(!load_success); assert_eq!(empty_nbt.as_bytes().len(), 0); } #[tokio::test] async fn server_player_data_new() { let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let save_interval = Duration::from_mins(5); let player_data = ServerPlayerData::new(path, save_interval, true); assert_eq!(player_data.save_interval, save_interval); assert!( Instant::now().duration_since(player_data.last_save.load()) < Duration::from_secs(1) ); } #[tokio::test] async fn player_data_file_structure() { let temp_dir = tempdir().unwrap(); let path = temp_dir.path().to_path_buf(); let uuid = Uuid::new_v4(); let storage = PlayerDataStorage::new(path, true); // Create and save player data let mut nbt = PNbtCompound::new(); nbt.put_string("TestPlayer"); nbt.put_i32(42); storage.save_player_data(&uuid, nbt).unwrap(); // Verify the file exists let player_data_path = storage.get_player_data_path(&uuid); assert!(player_data_path.exists()); // Load it again and verify content let (success, mut loaded_data) = storage.load_player_data(&uuid).unwrap(); assert!(success); assert_eq!(loaded_data.get_string().unwrap(), "TestPlayer"); assert_eq!(loaded_data.get_i32().unwrap(), 42); } }