Pumpkin/pumpkin/src/data/player_server.rs
Alexander Medvedev a36240dd40 fix: height maps
2026-04-27 23:16:19 +02:00

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);
}
}