Skip to content

Commit 39e551e

Browse files
committed
feat(player-data): support inventory saving + review fix
1 parent 485b90c commit 39e551e

File tree

9 files changed

+233
-115
lines changed

9 files changed

+233
-115
lines changed

pumpkin-inventory/src/player.rs

+18
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,24 @@ impl PlayerInventory {
275275
slots.into_boxed_slice()
276276
}
277277

278+
pub fn armor_slots(&self) -> Box<[Option<&ItemStack>]> {
279+
self.armor.iter().map(|item| item.as_ref()).collect()
280+
}
281+
282+
pub fn crafting_slots(&self) -> Box<[Option<&ItemStack>]> {
283+
let mut slots = vec![self.crafting_output.as_ref()];
284+
slots.extend(self.crafting.iter().map(|c| c.as_ref()));
285+
slots.into_boxed_slice()
286+
}
287+
288+
pub fn item_slots(&self) -> Box<[Option<&ItemStack>]> {
289+
self.items.iter().map(|item| item.as_ref()).collect()
290+
}
291+
292+
pub fn offhand_slot(&self) -> Option<&ItemStack> {
293+
self.offhand.as_ref()
294+
}
295+
278296
pub fn iter_items_mut(&mut self) -> IterMut<Option<ItemStack>> {
279297
self.items.iter_mut()
280298
}

pumpkin-world/src/data/player_data.rs

+39-75
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use pumpkin_config::ADVANCED_CONFIG;
1+
use pumpkin_config::advanced_config;
22
use pumpkin_nbt::compound::NbtCompound;
33
use std::fs::{File, create_dir_all};
44
use std::io;
@@ -38,11 +38,9 @@ impl PlayerDataStorage {
3838
}
3939
}
4040

41-
let config = &ADVANCED_CONFIG.player_data;
42-
4341
Self {
4442
data_path: path,
45-
save_enabled: config.save_player_data,
43+
save_enabled: advanced_config().player_data.save_player_data,
4644
}
4745
}
4846

@@ -63,43 +61,37 @@ impl PlayerDataStorage {
6361
/// # Returns
6462
///
6563
/// 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> {
64+
pub fn load_player_data(&self, uuid: &Uuid) -> Result<(bool, NbtCompound), PlayerDataError> {
6765
// If player data saving is disabled, return empty data
6866
if !self.save_enabled {
69-
return Ok(NbtCompound::new());
67+
return Ok((false, NbtCompound::new()));
7068
}
7169

7270
// If not in cache, load from disk
7371
let path = self.get_player_data_path(uuid);
7472
if !path.exists() {
7573
log::debug!("No player data file found for {}", uuid);
76-
return Ok(NbtCompound::new());
74+
return Ok((false, NbtCompound::new()));
7775
}
7876

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)),
77+
let file = match File::open(&path) {
78+
Ok(file) => file,
79+
Err(e) => {
80+
log::error!("Failed to open player data file for {}: {}", uuid, e);
81+
return Err(PlayerDataError::Io(e));
8982
}
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-
})?;
83+
};
10084

101-
log::debug!("Loaded player data for {} from disk", uuid);
102-
Ok(nbt)
85+
match pumpkin_nbt::nbt_compress::read_gzip_compound_tag(file) {
86+
Ok(nbt) => {
87+
log::debug!("Loaded player data for {} from disk", uuid);
88+
Ok((true, nbt))
89+
}
90+
Err(e) => {
91+
log::error!("Failed to read player data for {}: {}", uuid, e);
92+
Err(PlayerDataError::Nbt(e.to_string()))
93+
}
94+
}
10395
}
10496

10597
/// Saves player data to NBT file and updates cache.
@@ -115,64 +107,36 @@ impl PlayerDataStorage {
115107
/// # Returns
116108
///
117109
/// 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> {
110+
pub fn save_player_data(&self, uuid: &Uuid, data: NbtCompound) -> Result<(), PlayerDataError> {
123111
// Skip saving if disabled in config
124112
if !self.save_enabled {
125113
return Ok(());
126114
}
127115

128116
let path = self.get_player_data_path(uuid);
129117

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-
}
118+
// Ensure parent directory exists
119+
if let Some(parent) = path.parent() {
120+
if let Err(e) = create_dir_all(parent) {
121+
log::error!("Failed to create player data directory for {}: {}", uuid, e);
122+
return Err(PlayerDataError::Io(e));
145123
}
124+
}
146125

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))
126+
// Create the file and write directly with GZip compression
127+
match File::create(&path) {
128+
Ok(file) => {
129+
if let Err(e) = pumpkin_nbt::nbt_compress::write_gzip_compound_tag(&data, file) {
130+
log::error!("Failed to write compressed player data for {}: {}", uuid, e);
131+
Err(PlayerDataError::Nbt(e.to_string()))
132+
} else {
133+
log::debug!("Saved player data for {} to disk", uuid);
134+
Ok(())
167135
}
168136
}
169-
})
170-
.await
171-
{
172-
Ok(result) => result,
173137
Err(e) => {
174-
log::error!("Task panicked while saving player data for {}: {}", uuid, e);
175-
Err(PlayerDataError::Nbt(format!("Task join error: {e}")))
138+
log::error!("Failed to create player data file for {}: {}", uuid, e);
139+
Err(PlayerDataError::Io(e))
176140
}
177141
}
178142
}

pumpkin-world/src/item/mod.rs

+38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use pumpkin_data::item::Item;
22
use pumpkin_data::tag::{RegistryKey, get_tag_values};
3+
use pumpkin_nbt::compound::NbtCompound;
34

45
mod categories;
56

@@ -100,4 +101,41 @@ impl ItemStack {
100101
}
101102
false
102103
}
104+
105+
pub fn write_item_stack(&self, compound: &mut NbtCompound) {
106+
// Minecraft 1.21.4 uses "id" as string with namespaced ID (minecraft:diamond_sword)
107+
compound.put_string("id", format!("minecraft:{}", self.item.registry_key));
108+
compound.put_int("count", self.item_count as i32);
109+
110+
// Create a tag compound for additional data
111+
let tag = NbtCompound::new();
112+
113+
// TODO: Store custom data like enchantments, display name, etc. would go here
114+
115+
// Store custom data like enchantments, display name, etc. would go here
116+
compound.put_component("components", tag);
117+
}
118+
119+
pub fn read_item_stack(compound: &NbtCompound) -> Option<Self> {
120+
// Get ID, which is a string like "minecraft:diamond_sword"
121+
let full_id = compound.get_string("id")?;
122+
123+
// Remove the "minecraft:" prefix if present
124+
let registry_key = full_id.strip_prefix("minecraft:").unwrap_or(full_id);
125+
126+
// Try to get item by registry key
127+
let item = Item::from_registry_key(registry_key)?;
128+
129+
let count = compound.get_int("count")? as u8;
130+
131+
// Create the item stack
132+
let item_stack = Self::new(count, item);
133+
134+
// Process any additional data in the components compound
135+
if let Some(_tag) = compound.get_compound("components") {
136+
// TODO: Process additional components like damage, enchantments, etc.
137+
}
138+
139+
Some(item_stack)
140+
}
103141
}

pumpkin/src/command/commands/experience.rs

+12-8
Original file line numberDiff line numberDiff line change
@@ -166,23 +166,24 @@ impl Executor {
166166
}
167167
}
168168

169-
fn handle_modify(
169+
async fn handle_modify(
170170
&self,
171171
target: &Player,
172172
amount: i32,
173173
exp_type: ExpType,
174+
mode: Mode,
174175
) -> Result<(), &'static str> {
175176
match exp_type {
176177
ExpType::Levels => {
177-
if self.mode == Mode::Add {
178-
target.add_experience_levels(amount);
178+
if mode == Mode::Add {
179+
target.add_experience_levels(amount).await;
179180
} else {
180-
target.set_experience_level(amount, true);
181+
target.set_experience_level(amount, true).await;
181182
}
182183
}
183184
ExpType::Points => {
184-
if self.mode == Mode::Add {
185-
target.add_experience_points(amount);
185+
if mode == Mode::Add {
186+
target.add_experience_points(amount).await;
186187
} else {
187188
// target.set_experience_points(amount).await; This could
188189
let current_level = target.experience_level.load(Ordering::Relaxed);
@@ -192,7 +193,7 @@ impl Executor {
192193
return Err("commands.experience.set.points.invalid");
193194
}
194195

195-
target.set_experience_points(amount);
196+
target.set_experience_points(amount).await;
196197
}
197198
}
198199
}
@@ -242,7 +243,10 @@ impl CommandExecutor for Executor {
242243
}
243244

244245
for target in targets {
245-
match self.handle_modify(target, amount, self.exp_type.unwrap()) {
246+
match self
247+
.handle_modify(target, amount, self.exp_type.unwrap(), self.mode)
248+
.await
249+
{
246250
Ok(()) => {
247251
let msg = Self::get_success_message(
248252
self.mode,

pumpkin/src/data/player_server_data.rs

+13-11
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ impl ServerPlayerData {
6161
player.write_nbt(&mut nbt).await;
6262

6363
// Save to disk
64-
self.storage
65-
.save_player_data(&player.gameprofile.id, nbt)
66-
.await?;
64+
self.storage.save_player_data(&player.gameprofile.id, nbt)?;
6765

6866
Ok(())
6967
}
@@ -89,11 +87,7 @@ impl ServerPlayerData {
8987
player.write_nbt(&mut nbt).await;
9088

9189
// Save to disk periodically to prevent data loss on server crash
92-
if let Err(e) = self
93-
.storage
94-
.save_player_data(&player.gameprofile.id, nbt)
95-
.await
96-
{
90+
if let Err(e) = self.storage.save_player_data(&player.gameprofile.id, nbt) {
9791
log::error!(
9892
"Failed to save player data for {}: {}",
9993
player.gameprofile.id,
@@ -146,8 +140,12 @@ impl ServerPlayerData {
146140
player: &mut Player,
147141
) -> Result<(), PlayerDataError> {
148142
let uuid = &player.gameprofile.id;
149-
match self.storage.load_player_data(uuid).await {
150-
Ok(mut data) => {
143+
match self.storage.load_player_data(uuid) {
144+
Ok((should_load, mut data)) => {
145+
if !should_load {
146+
// No data to load, continue with default data
147+
return Ok(());
148+
}
151149
player.read_nbt(&mut data).await;
152150
Ok(())
153151
}
@@ -180,9 +178,13 @@ impl ServerPlayerData {
180178
&self,
181179
player: &Player,
182180
) -> Result<(), PlayerDataError> {
181+
if !self.storage.save_enabled {
182+
return Ok(());
183+
}
184+
183185
let uuid = &player.gameprofile.id;
184186
let mut nbt = NbtCompound::new();
185187
player.write_nbt(&mut nbt).await;
186-
self.storage.save_player_data(uuid, nbt).await
188+
self.storage.save_player_data(uuid, nbt)
187189
}
188190
}

pumpkin/src/entity/experience_orb.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl EntityBase for ExperienceOrbEntity {
8484
if *delay == 0 {
8585
*delay = 2;
8686
player.living_entity.pickup(&self.entity, 1).await;
87-
player.add_experience_points(self.amount as i32);
87+
player.add_experience_points(self.amount as i32).await;
8888
// TODO: pickingCount for merging
8989
self.entity.remove().await;
9090
}

0 commit comments

Comments
 (0)