Skip to content

Commit ddc9b42

Browse files
committed
feat(player-data): move PlayerDataStorage to pumpkin-world
1 parent c3cd3f2 commit ddc9b42

File tree

9 files changed

+373
-369
lines changed

9 files changed

+373
-369
lines changed

pumpkin-world/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ bytes.workspace = true
2020
tokio.workspace = true
2121
rayon.workspace = true
2222
derive_more.workspace = true
23+
uuid.workspace = true
2324
thiserror.workspace = true
2425
serde.workspace = true
2526
serde_json.workspace = true

pumpkin-world/src/data/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod player_data;

pumpkin-world/src/data/player_data.rs

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use pumpkin_config::ADVANCED_CONFIG;
2+
use pumpkin_nbt::compound::NbtCompound;
3+
use std::fs::{File, create_dir_all};
4+
use std::io;
5+
use std::path::PathBuf;
6+
use uuid::Uuid;
7+
8+
/// Manages the storage and retrieval of player data from disk and memory cache.
9+
///
10+
/// This struct provides functions to load and save player data to/from NBT files,
11+
/// with a memory cache to handle player disconnections temporarily.
12+
pub struct PlayerDataStorage {
13+
/// Path to the directory where player data is stored
14+
data_path: PathBuf,
15+
/// Whether player data saving is enabled
16+
pub save_enabled: bool,
17+
}
18+
19+
#[derive(Debug, thiserror::Error)]
20+
pub enum PlayerDataError {
21+
#[error("IO error: {0}")]
22+
Io(#[from] io::Error),
23+
#[error("NBT error: {0}")]
24+
Nbt(String),
25+
}
26+
27+
impl PlayerDataStorage {
28+
/// Creates a new `PlayerDataStorage` with the specified data path and cache expiration time.
29+
pub fn new(data_path: impl Into<PathBuf>) -> Self {
30+
let path = data_path.into();
31+
if !path.exists() {
32+
if let Err(e) = create_dir_all(&path) {
33+
log::error!(
34+
"Failed to create player data directory at {:?}: {}",
35+
path,
36+
e
37+
);
38+
}
39+
}
40+
41+
let config = &ADVANCED_CONFIG.player_data;
42+
43+
Self {
44+
data_path: path,
45+
save_enabled: config.save_player_data,
46+
}
47+
}
48+
49+
/// Returns the path for a player's data file based on their UUID.
50+
fn get_player_data_path(&self, uuid: &Uuid) -> PathBuf {
51+
self.data_path.join(format!("{uuid}.dat"))
52+
}
53+
54+
/// Loads player data from NBT file or cache.
55+
///
56+
/// This function first checks if player data exists in the cache.
57+
/// If not, it attempts to load the data from a .dat file on disk.
58+
///
59+
/// # Arguments
60+
///
61+
/// * `uuid` - The UUID of the player to load data for.
62+
///
63+
/// # Returns
64+
///
65+
/// A Result containing either the player's NBT data or an error.
66+
pub async fn load_player_data(&self, uuid: &Uuid) -> Result<NbtCompound, PlayerDataError> {
67+
// If player data saving is disabled, return empty data
68+
if !self.save_enabled {
69+
return Ok(NbtCompound::new());
70+
}
71+
72+
// If not in cache, load from disk
73+
let path = self.get_player_data_path(uuid);
74+
if !path.exists() {
75+
log::debug!("No player data file found for {}", uuid);
76+
return Ok(NbtCompound::new());
77+
}
78+
79+
// Offload file I/O to a separate tokio task
80+
let uuid_copy = *uuid;
81+
let nbt = tokio::task::spawn_blocking(move || -> Result<NbtCompound, PlayerDataError> {
82+
match File::open(&path) {
83+
Ok(file) => {
84+
// Read directly from the file with GZip decompression
85+
pumpkin_nbt::nbt_compress::read_gzip_compound_tag(file)
86+
.map_err(|e| PlayerDataError::Nbt(e.to_string()))
87+
}
88+
Err(e) => Err(PlayerDataError::Io(e)),
89+
}
90+
})
91+
.await
92+
.unwrap_or_else(|e| {
93+
log::error!(
94+
"Task error when loading player data for {}: {}",
95+
uuid_copy,
96+
e
97+
);
98+
Err(PlayerDataError::Nbt(format!("Task join error: {e}")))
99+
})?;
100+
101+
log::debug!("Loaded player data for {} from disk", uuid);
102+
Ok(nbt)
103+
}
104+
105+
/// Saves player data to NBT file and updates cache.
106+
///
107+
/// This function saves the player's data to a .dat file on disk and also
108+
/// updates the in-memory cache with the latest data.
109+
///
110+
/// # Arguments
111+
///
112+
/// * `uuid` - The UUID of the player to save data for.
113+
/// * `data` - The NBT compound data to save.
114+
///
115+
/// # Returns
116+
///
117+
/// A Result indicating success or the error that occurred.
118+
pub async fn save_player_data(
119+
&self,
120+
uuid: &Uuid,
121+
data: NbtCompound,
122+
) -> Result<(), PlayerDataError> {
123+
// Skip saving if disabled in config
124+
if !self.save_enabled {
125+
return Ok(());
126+
}
127+
128+
let path = self.get_player_data_path(uuid);
129+
130+
// Run disk I/O in a separate tokio task
131+
let uuid_copy = *uuid;
132+
let data_clone = data;
133+
134+
match tokio::spawn(async move {
135+
// Ensure parent directory exists
136+
if let Some(parent) = path.parent() {
137+
if let Err(e) = create_dir_all(parent) {
138+
log::error!(
139+
"Failed to create player data directory for {}: {}",
140+
uuid_copy,
141+
e
142+
);
143+
return Err(PlayerDataError::Io(e));
144+
}
145+
}
146+
147+
// Create the file and write directly with GZip compression
148+
match File::create(&path) {
149+
Ok(file) => {
150+
if let Err(e) =
151+
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(&data_clone, file)
152+
{
153+
log::error!(
154+
"Failed to write compressed player data for {}: {}",
155+
uuid_copy,
156+
e
157+
);
158+
Err(PlayerDataError::Nbt(e.to_string()))
159+
} else {
160+
log::debug!("Saved player data for {} to disk", uuid_copy);
161+
Ok(())
162+
}
163+
}
164+
Err(e) => {
165+
log::error!("Failed to create player data file for {}: {}", uuid_copy, e);
166+
Err(PlayerDataError::Io(e))
167+
}
168+
}
169+
})
170+
.await
171+
{
172+
Ok(result) => result,
173+
Err(e) => {
174+
log::error!("Task panicked while saving player data for {}: {}", uuid, e);
175+
Err(PlayerDataError::Nbt(format!("Task join error: {e}")))
176+
}
177+
}
178+
}
179+
}

pumpkin-world/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod block;
55
pub mod chunk;
66
pub mod coordinates;
77
pub mod cylindrical_chunk_iterator;
8+
pub mod data;
89
pub mod dimension;
910
mod generation;
1011
pub mod item;

pumpkin/src/data/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub mod op_data;
99
pub mod banlist_serializer;
1010
pub mod banned_ip_data;
1111
pub mod banned_player_data;
12-
pub mod player_data;
12+
pub mod player_server_data;
1313

1414
pub trait LoadJSONConfiguration {
1515
#[must_use]

0 commit comments

Comments
 (0)