feat: add advancement progress (#2261)

* implementing the advancement progress so each advancement has certain requirements to be mark has complete

* cargo fmt

* fix test

---------

Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
kedor
2026-06-15 14:50:12 +02:00
committed by GitHub
parent 8394c27212
commit fb3b146fdd
5 changed files with 2358 additions and 5288 deletions

View File

@@ -5,7 +5,7 @@ use pumpkin_util::resource_location::ResourceLocation;
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::TextContent::Translate;
use quote::{ToTokens, format_ident, quote};
use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
use serde::{Deserialize, Deserializer, Serialize};
use std::cmp::PartialEq;
use std::fmt::Display;
use std::hash::{Hash, Hasher};
@@ -430,9 +430,7 @@ pub(crate) fn build() -> TokenStream {
send_telemetry : #send_telemetry,
display : #display,
reward : &#reward,
requirements: AdvancementRequirement{
requirements : &[#(#requirements),*]
}
requirements: &[#(#requirements),*],
};
}]);
let minecraft_name = identifier.to_string();
@@ -463,7 +461,7 @@ pub(crate) fn build() -> TokenStream {
pub send_telemetry : bool,
pub display : Option<&'static AdvancementDisplay>,
pub reward : &'static AdvancementReward,
pub requirements: AdvancementRequirement,
pub requirements: &'static[&'static[&'static str]],
}
impl Display for Advancement {

View File

@@ -110,7 +110,7 @@ potion_brewing = []
phf = { workspace = true, features = ["macros"] }
pumpkin-util.workspace = true
pumpkin-nbt.workspace = true
serde.workspace = true
serde = {workspace = true, features = ["rc"]}
crc-fast.workspace = true
rand.workspace = true

View File

@@ -1,12 +1,15 @@
use crate::item_stack::ItemStack;
use crate::potion_brewing::ItemRecipe;
use crate::translation::java::{
CHAT_TYPE_ADVANCEMENT_CHALLENGE, CHAT_TYPE_ADVANCEMENT_GOAL, CHAT_TYPE_ADVANCEMENT_TASK,
};
use crate::{ADVANCEMENT_TREE, Advancement};
use pumpkin_util::identifier::Identifier;
use pumpkin_util::resource_location::ResourceLocation;
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::color::{Color, NamedColor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use std::fmt::Display;
use std::hash::{Hash, Hasher};
@@ -85,11 +88,11 @@ impl FrameType {
}
}
pub fn get_name(&self) -> &'static str {
pub fn get_translation(&self) -> &'static str {
match self {
FrameType::Task => "task",
FrameType::Challenge => "challenge",
FrameType::Goal => "goal",
FrameType::Task => CHAT_TYPE_ADVANCEMENT_TASK,
FrameType::Challenge => CHAT_TYPE_ADVANCEMENT_CHALLENGE,
FrameType::Goal => CHAT_TYPE_ADVANCEMENT_GOAL,
}
}
}
@@ -190,8 +193,48 @@ pub struct Criteria {
pub criterion_id: Identifier,
pub achieve_date: Option<i64>,
}
/// Represents the requirements needed to complete an advancement.
/// To mark as done, at least one criterion from each requirement group must be satisfied.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct AdvancementRequirement {
pub requirements: &'static [&'static [&'static str]],
pub requirements: Vec<Vec<Arc<str>>>,
}
impl AdvancementRequirement {
pub fn from_const(static_requirements: &[&[&str]]) -> Self {
let requirements = static_requirements
.iter()
.map(|inner| inner.iter().map(|s| Arc::from(*s)).collect())
.collect();
AdvancementRequirement { requirements }
}
/// test if the requirements is complete
pub fn test(&self, predicate: impl Fn(&str) -> bool) -> bool {
if self.requirements.is_empty() {
false
} else {
for requirement in &self.requirements {
if !Self::any_match(requirement, &predicate) {
return false;
}
}
true
}
}
/// check if any test pass
fn any_match(requirements: &Vec<Arc<str>>, predicate: impl Fn(&str) -> bool) -> bool {
for requirement in requirements {
if predicate(requirement) {
return true;
}
}
false
}
pub fn names(&self) -> Vec<Arc<str>> {
self.requirements.iter().flatten().cloned().collect()
}
}
pub trait Criterion {}

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,8 @@ 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_data::advancement_data::{AdvancementNode, AdvancementRequirement, AdvancementReward};
use pumpkin_data::{Advancement, translation};
use pumpkin_util::text::TextComponent;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
@@ -12,30 +12,134 @@ use std::collections::{HashMap, HashSet};
use std::fs::create_dir_all;
use std::path::PathBuf;
use std::sync::{Arc, Weak};
use std::time::SystemTime;
use tracing::{error, warn};
use uuid::Uuid;
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct CriterionProgress(pub Option<SystemTime>);
impl CriterionProgress {
pub fn grant(&mut self) {
self.0 = Some(SystemTime::now());
}
pub const fn revoke(&mut self) {
self.0 = None;
}
#[must_use]
pub const fn is_done(&self) -> bool {
self.0.is_some()
}
}
/// 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)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AdvancementProgress {
/// Indicates if the advancement is fully completed.
pub complete: bool,
/// Indicates the different progress of all criteria currently only a boolean
pub criteria: HashMap<Arc<str>, CriterionProgress>,
/// The Requirement for the Advancement to be mark as complete
pub requirements: AdvancementRequirement,
}
impl AdvancementProgress {
/// Returns `true` if the advancement is completely done.
/// Returns `true` if the advancement is done.
#[must_use]
pub const fn is_done(&self) -> bool {
self.complete
pub fn is_done(&self) -> bool {
self.requirements.test(|s| self.is_criterion_done(s))
}
/// Check if a criterion his mark has complete
fn is_criterion_done(&self, criterion: &str) -> bool {
self.criteria
.get(criterion)
.is_some_and(CriterionProgress::is_done)
}
/// 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
pub fn has_progress(&self) -> bool {
for value in self.criteria.values() {
if value.is_done() {
return true;
}
}
false
}
pub fn grant_progress(&mut self, name: &str) -> bool {
if let Some(value) = self.criteria.get_mut(name)
&& !value.is_done()
{
value.grant();
true
} else {
false
}
}
pub fn revoke_progress(&mut self, name: &str) -> bool {
if let Some(value) = self.criteria.get_mut(name)
&& value.is_done()
{
value.revoke();
true
} else {
false
}
}
pub fn update(&mut self, requirements: AdvancementRequirement) {
let names = requirements.names();
self.criteria.retain(|key, _criterion| names.contains(key));
for name in names {
self.criteria.entry(name).or_default();
}
self.requirements = requirements;
}
}
#[derive(Clone, Default)]
struct AdvancementProgressMap {
pub map: IndexMap<&'static Advancement, AdvancementProgress>,
}
impl AdvancementProgressMap {
/// 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.map.entry(advancement).or_insert_with(|| {
let mut progress = AdvancementProgress::default();
progress.update(AdvancementRequirement::from_const(advancement.requirements));
progress
})
}
#[inline]
pub fn clear(&mut self) {
self.map.clear();
}
#[inline]
pub fn insert(&mut self, advancement: &'static Advancement, progress: AdvancementProgress) {
self.map.insert(advancement, progress);
}
#[inline]
pub fn len(&self) -> usize {
self.map.len()
}
#[inline]
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
@@ -43,7 +147,7 @@ impl AdvancementProgress {
///
/// This handles saving, loading, and tracking the state of granted / revoked advancements.
pub struct PlayerAdvancement {
progress: IndexMap<&'static Advancement, AdvancementProgress>,
progress: AdvancementProgressMap,
is_first_packet: bool,
roots_to_update: HashSet<&'static AdvancementNode>,
visible: HashSet<&'static Advancement>,
@@ -69,7 +173,7 @@ impl PlayerAdvancement {
#[must_use]
pub fn new(manager: Arc<AdvancementManager>, uuid: Uuid) -> Self {
Self {
progress: IndexMap::new(),
progress: AdvancementProgressMap::default(),
path: manager.advancement_path.join(format!("{}.json", &uuid)),
manager,
player: Weak::new(),
@@ -142,9 +246,14 @@ impl PlayerAdvancement {
from_reader(file).map_err(AdvancementDataError::Json)?;
self.progress.clear();
for (advancement_id, progress) in loaded_data {
for (advancement_id, mut progress) in loaded_data {
if let Some(advancement_ref) = Advancement::from_minecraft_name(&advancement_id) {
progress.update(AdvancementRequirement::from_const(
advancement_ref.requirements,
));
self.progress.insert(advancement_ref, progress);
self.progress_changed.insert(advancement_ref);
//TODO self.mark_for_visibility(advancement_ref);
} else {
warn!("The Advancement name {} is invalid", advancement_id);
}
@@ -152,65 +261,65 @@ impl PlayerAdvancement {
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) {
pub async fn award(&mut self, advancement: &'static Advancement, criterion: &str) -> bool {
//TODO call and creates Events for plugins
let mut result = false;
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
let progress = self.progress.get_mut_or_start_progress(advancement);
let was_done = progress.is_done();
if !progress.grant_progress(criterion) {
result = true;
self.progress_changed.insert(advancement);
if !was_done && progress.is_done() {
Self::grant_reward(player.clone(), advancement.reward).await;
if let Some(display) = advancement.display
&& display.announce_to_chat
&& player
.world()
.level_info
.load()
.game_rules
.show_advancement_messages
{
let component = TextComponent::translate_cross(
display.frame_type.get_translation(),
translation::bedrock::CHAT_TYPE_ACHIEVEMENT,
[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() {
if !was_done && progress.is_done() {
//TODO self.mark_for_visibility_update(advancement);
}
result
}
/// 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);
pub fn revoke(&mut self, advancement: &'static Advancement, criterion: &str) -> bool {
let mut result = false;
let progress = self.progress.get_mut_or_start_progress(advancement);
let was_done = progress.is_done();
if progress.is_done() {
progress.complete = false;
if progress.revoke_progress(criterion) {
//TODO listener
self.progress_changed.insert(advancement);
result = true;
}
if was_done && !progress.is_done() {
//TODO self.mark_for_visibility_update(advancement);
}
result
}
}
@@ -221,7 +330,7 @@ impl Serialize for PlayerAdvancement {
{
let mut map = serializer.serialize_map(Some(self.progress.len()))?;
for (advancement, progress) in &self.progress {
for (advancement, progress) in &self.progress.map {
map.serialize_entry(&advancement.id, progress)?;
}
map.end()
@@ -237,13 +346,27 @@ mod tests {
#[test]
fn advancement_progress() {
let progress = AdvancementProgress { complete: false };
let mut criteria = HashMap::new();
criteria.insert(Arc::from("testCriteria"), CriterionProgress::default());
criteria.insert(Arc::from("testCriteria2"), CriterionProgress::default());
let requirements = AdvancementRequirement {
requirements: vec![
vec![Arc::from("testCriteria")],
vec![Arc::from("testCriteria2")],
],
};
let mut progress = AdvancementProgress {
criteria,
requirements,
};
assert!(!progress.is_done());
assert!(!progress.has_progress());
let complete_progress = AdvancementProgress { complete: true };
assert!(complete_progress.is_done());
assert!(complete_progress.has_progress());
progress.grant_progress("testCriteria");
assert!(!progress.is_done());
assert!(progress.has_progress());
progress.grant_progress("testCriteria2");
assert!(progress.is_done());
assert!(progress.has_progress());
}
#[test]
@@ -265,7 +388,7 @@ mod tests {
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);
let progress = pa.progress.get_mut_or_start_progress(adv);
assert!(
!progress.is_done(),
"New progress should not be marked done by default"
@@ -280,12 +403,12 @@ mod tests {
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;
let progress_mut = pa.progress.get_mut_or_start_progress(adv);
progress_mut.grant_progress("crafting_table");
};
assert!(pa.get_or_start_progress(adv).is_done());
pa.revoke(adv);
assert!(!pa.get_or_start_progress(adv).is_done());
assert!(pa.progress.get_mut_or_start_progress(adv).is_done());
pa.revoke(adv, "crafting_table");
assert!(!pa.progress.get_mut_or_start_progress(adv).is_done());
}
#[test]
@@ -298,8 +421,8 @@ mod tests {
// Add some advancement progress
let adv = Advancement::STORY_ROOT;
{
let progress_mut = pa.get_mut_or_start_progress(adv);
progress_mut.complete = true;
let progress_mut = pa.progress.get_mut_or_start_progress(adv);
progress_mut.grant_progress("crafting_table");
};
// Save should succeed
@@ -325,8 +448,8 @@ mod tests {
// Add some advancement progress
let adv = Advancement::STORY_ROOT;
{
let progress_mut = pa.get_mut_or_start_progress(adv);
progress_mut.complete = true;
let progress_mut = pa.progress.get_mut_or_start_progress(adv);
progress_mut.grant_progress("crafting_table");
};
// Save should return Ok but not actually save
@@ -364,16 +487,19 @@ mod tests {
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 } });
let mut progress = AdvancementProgress::default();
progress.update(AdvancementRequirement::from_const(adv.requirements));
progress.grant_progress("crafting_table");
let data = serde_json::json!({ adv.id.to_string():progress });
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);
let loaded_progress = pa.progress.get_mut_or_start_progress(adv);
assert!(
progress.is_done(),
loaded_progress.is_done(),
"Loaded advancement should be marked complete"
);
}
@@ -389,8 +515,8 @@ mod tests {
let adv = Advancement::STORY_ROOT;
{
let progress_mut = pa.get_mut_or_start_progress(adv);
progress_mut.complete = true;
let progress_mut = pa.progress.get_mut_or_start_progress(adv);
progress_mut.grant_progress("crafting_table");
};
assert!(pa.save().is_ok(), "Save should succeed");
@@ -400,7 +526,7 @@ mod tests {
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);
let loaded_progress = pa_loaded.progress.get_mut_or_start_progress(adv);
assert!(
loaded_progress.is_done(),
"Loaded progress should match saved progress"
@@ -418,8 +544,17 @@ mod tests {
let manager = Arc::new(AdvancementManager::new(temp_dir.path(), true));
// Create a JSON file with invalid advancement ID
let mut criteria = HashMap::new();
criteria.insert(Arc::from("testCriteria"), CriterionProgress::default());
let requirements = AdvancementRequirement {
requirements: vec![vec![Arc::from("testCriteria")]],
};
let progress = AdvancementProgress {
criteria,
requirements,
};
let data = serde_json::json!({
"invalid_advancement_id_12345": { "complete": true }
"invalid_advancement_id_12345": progress
});
let id = Uuid::new_v4();
let mut pa = PlayerAdvancement::new(manager, id);
@@ -449,12 +584,12 @@ mod tests {
let adv2 = Advancement::NETHER_ROOT;
{
let progress_mut1 = pa.get_mut_or_start_progress(adv1);
progress_mut1.complete = true;
let progress_mut1 = pa.progress.get_mut_or_start_progress(adv1);
progress_mut1.grant_progress("crafting_table");
};
{
let progress_mut2 = pa.get_mut_or_start_progress(adv2);
progress_mut2.complete = false;
let progress_mut2 = pa.progress.get_mut_or_start_progress(adv2);
progress_mut2.grant_progress("entered_nether");
};
assert!(pa.save().is_ok(), "Save should succeed");