mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add saving/loading advancements (#2257)
* implementing the saving and loading advancements for the different players * fix format
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2677,6 +2677,7 @@ dependencies = [
|
||||
"hmac 0.13.0",
|
||||
"hyper",
|
||||
"image",
|
||||
"indexmap",
|
||||
"libloading",
|
||||
"notify",
|
||||
"num-bigint",
|
||||
|
||||
@@ -111,6 +111,8 @@ tracing-serde-structured = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
image = { version = "0.25.10", default-features = false, features = ["png"] }
|
||||
|
||||
indexmap.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
|
||||
86
pumpkin/src/data/advancement_data.rs
Normal file
86
pumpkin/src/data/advancement_data.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use crate::entity::player::Player;
|
||||
use crate::entity::player::advancement::{AdvancementDataError, PlayerAdvancement};
|
||||
use pumpkin_data::Advancement;
|
||||
use pumpkin_util::identifier::Identifier;
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Manages player advancements, including data creation and saving.
|
||||
pub struct AdvancementManager {
|
||||
pub advancement_path: PathBuf,
|
||||
pub save_enabled: bool,
|
||||
}
|
||||
|
||||
impl AdvancementManager {
|
||||
/// Creates a new instance of `AdvancementManager` using the player data path.
|
||||
pub fn new(player_data_path: impl Into<PathBuf>, save_enabled: bool) -> Self {
|
||||
let path = player_data_path.into().join("advancements");
|
||||
if !path.exists()
|
||||
&& let Err(e) = create_dir_all(&path)
|
||||
{
|
||||
error!(
|
||||
"Failed to create player data directory at {}: {e}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Self {
|
||||
advancement_path: path,
|
||||
save_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the list of all available advancements in the game.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn get_advancements(&self) -> Vec<Identifier> {
|
||||
Advancement::get_list().to_vec()
|
||||
}
|
||||
|
||||
/// Creates and returns a new instance of `PlayerAdvancement` with the configured path.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn new_player_advancement(self: Arc<Self>, owner: Uuid) -> PlayerAdvancement {
|
||||
PlayerAdvancement::new(self, owner)
|
||||
}
|
||||
|
||||
/// Saves the advancements of all provided players.
|
||||
pub async fn save_all_players(players: Vec<Arc<Player>>) -> Result<(), AdvancementDataError> {
|
||||
for player in players {
|
||||
player.advancements.lock().await.save()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves the advancements of a specific player.
|
||||
pub async fn save_player(player: &Player) -> Result<(), AdvancementDataError> {
|
||||
player.advancements.lock().await.save()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn advancement_manager_new() {
|
||||
let path = PathBuf::from("test_data");
|
||||
let manager = AdvancementManager::new(path, true);
|
||||
assert_eq!(
|
||||
manager.advancement_path,
|
||||
PathBuf::from("test_data/advancements")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_advancement_path() {
|
||||
let path = PathBuf::from("world/playerdata");
|
||||
let manager = AdvancementManager::new(path, true);
|
||||
let advancement_path = manager.advancement_path;
|
||||
assert!(advancement_path.ends_with("advancements"));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const DATA_FOLDER: &str = "data/";
|
||||
|
||||
pub mod op;
|
||||
|
||||
pub mod advancement_data;
|
||||
pub mod banlist_serializer;
|
||||
pub mod banned_ip;
|
||||
pub mod banned_player;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod advancement;
|
||||
|
||||
use core::f32;
|
||||
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
|
||||
use std::f64::consts::TAU;
|
||||
@@ -29,6 +31,7 @@ use tokio::task::JoinHandle;
|
||||
use tracing::{debug, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use advancement::PlayerAdvancement;
|
||||
use pumpkin_data::attributes::Attributes;
|
||||
use pumpkin_data::block_properties::{BlockProperties, HorizontalFacing};
|
||||
use pumpkin_data::damage::DamageType;
|
||||
@@ -497,6 +500,7 @@ pub struct Player {
|
||||
pub tab_list_order: AtomicI32,
|
||||
pub tab_list_latency: AtomicI32,
|
||||
pub tab_list_listed: AtomicBool,
|
||||
pub advancements: Arc<Mutex<PlayerAdvancement>>,
|
||||
pub enchantment_seed: AtomicI32,
|
||||
pub fishing_bobber: AtomicI32,
|
||||
pub bedrock_skin: arc_swap::ArcSwap<pumpkin_protocol::bedrock::client::Skin>,
|
||||
@@ -618,6 +622,12 @@ impl Player {
|
||||
Self {
|
||||
living_entity,
|
||||
config: ArcSwap::new(Arc::new(config)),
|
||||
advancements: Arc::new(Mutex::new(
|
||||
server
|
||||
.advancement_manager
|
||||
.clone()
|
||||
.new_player_advancement(gameprofile.id),
|
||||
)),
|
||||
gameprofile,
|
||||
client,
|
||||
awaiting_teleport: Mutex::new(None),
|
||||
|
||||
468
pumpkin/src/entity/player/advancement.rs
Normal file
468
pumpkin/src/entity/player/advancement.rs
Normal file
@@ -0,0 +1,468 @@
|
||||
use crate::data::advancement_data::AdvancementManager;
|
||||
use crate::entity::EntityBase;
|
||||
use crate::entity::player::Player;
|
||||
use indexmap::IndexMap;
|
||||
use pumpkin_data::Advancement;
|
||||
use pumpkin_data::advancement_data::{AdvancementNode, AdvancementReward};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json::{from_reader, to_writer_pretty};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Weak};
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Represents the progress of a given advancement for a player.
|
||||
///
|
||||
/// Tracks whether the advancement has been fully completed. In the future,
|
||||
/// this will also track specific criteria progress.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
||||
pub struct AdvancementProgress {
|
||||
/// Indicates if the advancement is fully completed.
|
||||
pub complete: bool,
|
||||
}
|
||||
|
||||
impl AdvancementProgress {
|
||||
/// Returns `true` if the advancement is completely done.
|
||||
#[must_use]
|
||||
pub const fn is_done(&self) -> bool {
|
||||
self.complete
|
||||
}
|
||||
|
||||
/// Returns `true` if the advancement has any progress. Currently just returns if it is fully complete.
|
||||
#[must_use]
|
||||
pub const fn has_progress(&self) -> bool {
|
||||
self.complete
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages a player's collection of advancements.
|
||||
///
|
||||
/// This handles saving, loading, and tracking the state of granted / revoked advancements.
|
||||
pub struct PlayerAdvancement {
|
||||
progress: IndexMap<&'static Advancement, AdvancementProgress>,
|
||||
is_first_packet: bool,
|
||||
roots_to_update: HashSet<&'static AdvancementNode>,
|
||||
visible: HashSet<&'static Advancement>,
|
||||
progress_changed: HashSet<&'static Advancement>,
|
||||
manager: Arc<AdvancementManager>,
|
||||
path: PathBuf,
|
||||
last_selected_tab: Option<&'static Advancement>,
|
||||
/// A weak reference to the player who owns these advancements.
|
||||
pub player: Weak<Player>,
|
||||
}
|
||||
|
||||
/// Errors that can occur when saving or loading advancement data.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AdvancementDataError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(std::io::Error),
|
||||
#[error("JSON error: {0}")]
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl PlayerAdvancement {
|
||||
/// Creates a new instance of `PlayerAdvancement`.
|
||||
#[must_use]
|
||||
pub fn new(manager: Arc<AdvancementManager>, uuid: Uuid) -> Self {
|
||||
Self {
|
||||
progress: IndexMap::new(),
|
||||
path: manager.advancement_path.join(format!("{}.json", &uuid)),
|
||||
manager,
|
||||
player: Weak::new(),
|
||||
is_first_packet: true,
|
||||
roots_to_update: HashSet::default(),
|
||||
visible: HashSet::default(),
|
||||
progress_changed: HashSet::default(),
|
||||
last_selected_tab: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Associates the `PlayerAdvancement` data with the given player.
|
||||
pub fn set_player(&mut self, player: &Arc<Player>) {
|
||||
self.player = Arc::downgrade(player);
|
||||
}
|
||||
|
||||
/// Returns whether advancement saving is enabled for this player.
|
||||
#[must_use]
|
||||
pub fn is_save_enabled(&self) -> bool {
|
||||
self.manager.save_enabled
|
||||
}
|
||||
|
||||
///reload the advancements from the file
|
||||
pub fn reload(&mut self) -> Result<(), AdvancementDataError> {
|
||||
//self.stopListening(); TODO
|
||||
self.progress.clear();
|
||||
self.visible.clear();
|
||||
self.roots_to_update.clear();
|
||||
self.progress_changed.clear();
|
||||
self.is_first_packet = true;
|
||||
self.last_selected_tab = None;
|
||||
self.load()
|
||||
}
|
||||
|
||||
/// Saves the player's advancement progress to disk as JSON.
|
||||
pub fn save(&self) -> Result<(), AdvancementDataError> {
|
||||
if !self.is_save_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(parent) = &self.path.parent()
|
||||
&& let Err(e) = create_dir_all(parent)
|
||||
{
|
||||
let file_name = self
|
||||
.path
|
||||
.file_prefix()
|
||||
.and_then(|prefix| prefix.to_str())
|
||||
.unwrap_or("unknown");
|
||||
error!(
|
||||
"Failed to create player advancement directory for {}: {e}",
|
||||
file_name
|
||||
);
|
||||
return Err(AdvancementDataError::Io(e));
|
||||
}
|
||||
let file = std::fs::File::create(&self.path).map_err(AdvancementDataError::Io)?;
|
||||
|
||||
to_writer_pretty(file, &self).map_err(AdvancementDataError::Json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads the player's advancement progress from disk.
|
||||
pub fn load(&mut self) -> Result<(), AdvancementDataError> {
|
||||
if !self.path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let file = std::fs::File::open(&self.path).map_err(AdvancementDataError::Io)?;
|
||||
|
||||
let loaded_data: HashMap<String, AdvancementProgress> =
|
||||
from_reader(file).map_err(AdvancementDataError::Json)?;
|
||||
|
||||
self.progress.clear();
|
||||
for (advancement_id, progress) in loaded_data {
|
||||
if let Some(advancement_ref) = Advancement::from_minecraft_name(&advancement_id) {
|
||||
self.progress.insert(advancement_ref, progress);
|
||||
} else {
|
||||
warn!("The Advancement name {} is invalid", advancement_id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current progress for a given advancement, creating a default uncompleted progress if it doesn't exist.
|
||||
pub fn get_or_start_progress(
|
||||
&mut self,
|
||||
advancement: &'static Advancement,
|
||||
) -> &AdvancementProgress {
|
||||
self.get_mut_or_start_progress(advancement)
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the current progress for a given advancement. Creates the state entry if missing.
|
||||
pub fn get_mut_or_start_progress(
|
||||
&mut self,
|
||||
advancement: &'static Advancement,
|
||||
) -> &mut AdvancementProgress {
|
||||
self.progress.entry(advancement).or_default()
|
||||
}
|
||||
|
||||
/// Grants the rewards (like experience) associated with completing an advancement.
|
||||
pub async fn grant_reward(player: Arc<Player>, reward: &AdvancementReward) {
|
||||
player.add_experience_points(reward.experience).await;
|
||||
}
|
||||
|
||||
/// Fully awards an advancement to the player, updating its status to complete and granting rewards if applicable.
|
||||
pub async fn award(&mut self, advancement: &'static Advancement) {
|
||||
//TODO call and creates Events for plugins
|
||||
let player = self.player.upgrade().unwrap().clone();
|
||||
let progress = self.get_mut_or_start_progress(advancement);
|
||||
let is_done = progress.is_done();
|
||||
if !progress.is_done() {
|
||||
progress.complete = true;
|
||||
Self::grant_reward(player.clone(), advancement.reward).await;
|
||||
if let Some(display) = advancement.display
|
||||
&& display.announce_to_chat
|
||||
{
|
||||
let component = TextComponent::translate(
|
||||
format!("chat.type.advancement.{}", display.frame_type.get_name()),
|
||||
[player.get_display_name().await, advancement.name()],
|
||||
);
|
||||
player
|
||||
.world()
|
||||
.broadcast_system_message(&component, false)
|
||||
.await; //send translate component for the event
|
||||
}
|
||||
}
|
||||
if !is_done && progress.is_done() {
|
||||
//TODO self.mark_for_visibility_update(advancement);
|
||||
}
|
||||
}
|
||||
|
||||
/// Revokes a previously awarded advancement, clearing its progress state.
|
||||
pub fn revoke(&mut self, advancement: &'static Advancement) {
|
||||
let progress = self.get_mut_or_start_progress(advancement);
|
||||
let was_done = progress.is_done();
|
||||
if progress.is_done() {
|
||||
progress.complete = false;
|
||||
}
|
||||
|
||||
if was_done && !progress.is_done() {
|
||||
//TODO self.mark_for_visibility_update(advancement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PlayerAdvancement {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.progress.len()))?;
|
||||
|
||||
for (advancement, progress) in &self.progress {
|
||||
map.serialize_entry(&advancement.id, progress)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data::advancement_data::AdvancementManager;
|
||||
use pumpkin_data::Advancement;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn advancement_progress() {
|
||||
let progress = AdvancementProgress { complete: false };
|
||||
assert!(!progress.is_done());
|
||||
assert!(!progress.has_progress());
|
||||
|
||||
let complete_progress = AdvancementProgress { complete: true };
|
||||
assert!(complete_progress.is_done());
|
||||
assert!(complete_progress.has_progress());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_player_advancement() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let pa = PlayerAdvancement::new(manager, id);
|
||||
assert!(pa.is_save_enabled());
|
||||
assert!(pa.is_first_packet);
|
||||
assert!(pa.roots_to_update.is_empty());
|
||||
assert!(pa.progress.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_progress() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
let adv = Advancement::STORY_ROOT;
|
||||
let progress = pa.get_or_start_progress(adv);
|
||||
assert!(
|
||||
!progress.is_done(),
|
||||
"New progress should not be marked done by default"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_advancement() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
let adv = Advancement::STORY_ROOT;
|
||||
{
|
||||
let progress_mut = pa.get_mut_or_start_progress(adv);
|
||||
progress_mut.complete = true;
|
||||
};
|
||||
assert!(pa.get_or_start_progress(adv).is_done());
|
||||
pa.revoke(adv);
|
||||
assert!(!pa.get_or_start_progress(adv).is_done());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_advancement_progress() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
|
||||
// Add some advancement progress
|
||||
let adv = Advancement::STORY_ROOT;
|
||||
{
|
||||
let progress_mut = pa.get_mut_or_start_progress(adv);
|
||||
progress_mut.complete = true;
|
||||
};
|
||||
|
||||
// Save should succeed
|
||||
assert!(pa.save().is_ok(), "Save should succeed");
|
||||
|
||||
// File should exist
|
||||
assert!(pa.path.exists(), "Saved file should exist");
|
||||
|
||||
// Content should be valid JSON
|
||||
let content = std::fs::read_to_string(&pa.path).unwrap();
|
||||
assert!(!content.is_empty(), "Saved file should not be empty");
|
||||
let _: HashMap<String, AdvancementProgress> =
|
||||
serde_json::from_str(&content).expect("Saved content should be valid JSON");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_disabled() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), false));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
|
||||
// Add some advancement progress
|
||||
let adv = Advancement::STORY_ROOT;
|
||||
{
|
||||
let progress_mut = pa.get_mut_or_start_progress(adv);
|
||||
progress_mut.complete = true;
|
||||
};
|
||||
|
||||
// Save should return Ok but not actually save
|
||||
assert!(
|
||||
pa.save().is_ok(),
|
||||
"Save with disabled saving should return Ok"
|
||||
);
|
||||
assert!(
|
||||
!pa.path.exists(),
|
||||
"File should not be created when saving is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_nonexistent_file() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
|
||||
// Load from nonexistent file should return Ok (not error)
|
||||
assert!(
|
||||
pa.load().is_ok(),
|
||||
"Loading from nonexistent file should return Ok"
|
||||
);
|
||||
assert!(pa.progress.is_empty(), "Advancements should remain empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_advancement_progress() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
// Create a JSON file with advancement data
|
||||
let adv = Advancement::STORY_ROOT;
|
||||
let data = serde_json::json!({ adv.id.to_string(): { "complete": true } });
|
||||
std::fs::write(&pa.path, data.to_string()).unwrap();
|
||||
|
||||
// Load the file
|
||||
assert!(pa.load().is_ok(), "Load should succeed");
|
||||
|
||||
// Verify the advancement was loaded
|
||||
let progress = pa.get_or_start_progress(adv);
|
||||
assert!(
|
||||
progress.is_done(),
|
||||
"Loaded advancement should be marked complete"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_load_roundtrip() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
|
||||
// Create and save advancements
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager.clone(), id);
|
||||
|
||||
let adv = Advancement::STORY_ROOT;
|
||||
{
|
||||
let progress_mut = pa.get_mut_or_start_progress(adv);
|
||||
progress_mut.complete = true;
|
||||
};
|
||||
|
||||
assert!(pa.save().is_ok(), "Save should succeed");
|
||||
|
||||
// Load the saved advancements into a new instance
|
||||
let mut pa_loaded = PlayerAdvancement::new(manager, id);
|
||||
assert!(pa_loaded.load().is_ok(), "Load should succeed");
|
||||
|
||||
// Verify the loaded data matches the saved data
|
||||
let loaded_progress = pa_loaded.get_or_start_progress(adv);
|
||||
assert!(
|
||||
loaded_progress.is_done(),
|
||||
"Loaded progress should match saved progress"
|
||||
);
|
||||
assert_eq!(
|
||||
pa_loaded.progress.len(),
|
||||
pa.progress.len(),
|
||||
"Loaded advancements count should match"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_invalid_advancement_id() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
|
||||
// Create a JSON file with invalid advancement ID
|
||||
let data = serde_json::json!({
|
||||
"invalid_advancement_id_12345": { "complete": true }
|
||||
});
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
std::fs::write(&pa.path, data.to_string()).unwrap();
|
||||
|
||||
// Load should still succeed but skip the invalid entry
|
||||
|
||||
assert!(
|
||||
pa.load().is_ok(),
|
||||
"Load should succeed even with invalid IDs"
|
||||
);
|
||||
assert!(
|
||||
pa.progress.is_empty(),
|
||||
"Invalid advancements should be skipped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_multiple_advancements() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
|
||||
let id = Uuid::new_v4();
|
||||
let mut pa = PlayerAdvancement::new(manager, id);
|
||||
|
||||
// Add multiple advancements
|
||||
let adv1 = Advancement::STORY_ROOT;
|
||||
let adv2 = Advancement::NETHER_ROOT;
|
||||
|
||||
{
|
||||
let progress_mut1 = pa.get_mut_or_start_progress(adv1);
|
||||
progress_mut1.complete = true;
|
||||
};
|
||||
{
|
||||
let progress_mut2 = pa.get_mut_or_start_progress(adv2);
|
||||
progress_mut2.complete = false;
|
||||
};
|
||||
|
||||
assert!(pa.save().is_ok(), "Save should succeed");
|
||||
|
||||
// Verify both were saved
|
||||
let content = std::fs::read_to_string(&pa.path).unwrap();
|
||||
let saved_data: HashMap<String, AdvancementProgress> =
|
||||
serde_json::from_str(&content).unwrap();
|
||||
assert_eq!(saved_data.len(), 2, "Should have saved both advancements");
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ pub use recipe::RecipeManager;
|
||||
use crate::command::args::entities::{
|
||||
EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition,
|
||||
};
|
||||
use crate::data::advancement_data::AdvancementManager;
|
||||
use crate::server::scheduler::TaskScheduler;
|
||||
|
||||
/// Represents a Minecraft server instance.
|
||||
@@ -116,6 +117,8 @@ pub struct Server {
|
||||
pub defaultgamemode: Mutex<DefaultGamemode>,
|
||||
/// Manages player data storage
|
||||
pub player_data_storage: ServerPlayerData,
|
||||
// Manages player advancement
|
||||
pub advancement_manager: Arc<AdvancementManager>,
|
||||
// Whether the server whitelist is on or off
|
||||
pub white_list: AtomicBool,
|
||||
/// Manages the server's tick rate, freezing, and sprinting
|
||||
@@ -198,6 +201,7 @@ impl Server {
|
||||
Duration::from_secs(advanced_config.player_data.save_player_cron_interval),
|
||||
advanced_config.player_data.save_player_data,
|
||||
);
|
||||
let advancement_manager = Arc::new(AdvancementManager::new(world_path.clone(), true));
|
||||
let white_list = AtomicBool::new(basic_config.white_list);
|
||||
|
||||
let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps));
|
||||
@@ -246,6 +250,7 @@ impl Server {
|
||||
map_manager: MapManager::new(),
|
||||
defaultgamemode,
|
||||
player_data_storage,
|
||||
advancement_manager,
|
||||
white_list,
|
||||
tick_rate_manager,
|
||||
tick_times_nanos: Mutex::new([0; 100]),
|
||||
@@ -492,6 +497,12 @@ impl Server {
|
||||
|
||||
// Wrap in Arc after data is loaded
|
||||
let player = Arc::new(player);
|
||||
let mut advancements = player.advancements.lock().await;
|
||||
if let Err(e) = advancements.load() {
|
||||
warn!("Error loading player {}: {e}", player.gameprofile.id);
|
||||
}
|
||||
advancements.player = Arc::downgrade(&player);
|
||||
drop(advancements);
|
||||
|
||||
send_cancellable! {{
|
||||
self;
|
||||
|
||||
Reference in New Issue
Block a user