341 lines
11 KiB
Rust
341 lines
11 KiB
Rust
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<PlayerDataStorage>,
|
|
save_interval: Duration,
|
|
last_save: AtomicCell<Instant>,
|
|
}
|
|
|
|
impl ServerPlayerData {
|
|
/// Creates a new `ServerPlayerData` with specified configuration.
|
|
pub fn new(data_path: impl Into<PathBuf>, 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<Option<PNbtCompound>, 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);
|
|
}
|
|
}
|