Skip to main content

steel_core/player/advancement/
mod.rs

1//! advancement linked to the player.
2//!
3//! Groups the advancementPlayer, advancement progress and visibility evaluator
4
5pub mod progress;
6pub mod rewards;
7mod saving;
8mod visibility_evaluator;
9
10pub(crate) use saving::{PersistedAdvancements, PersistedAdvancementsRef};
11
12use crate::entity::Entity;
13use crate::player::Player;
14use crate::player::advancement::rewards::grant_reward;
15use progress::{AdvancementProgress, AdvancementProgressMap};
16use rustc_hash::{FxHashMap, FxHashSet};
17use std::time::UNIX_EPOCH;
18use steel_protocol::packets::game::CSelectAdvancementsTab;
19use steel_protocol::packets::game::CUpdateAdvancements;
20use steel_registry::REGISTRY;
21use steel_registry::advancement::registry::{AdvancementNodeRef, AdvancementRef};
22use steel_registry::advancement::{AdvancementProgressData, Criteria};
23use steel_registry::vanilla_game_rules::SHOW_ADVANCEMENT_MESSAGES;
24use steel_utils::Identifier;
25
26/// Manages a player's collection of advancements.
27///
28/// This handles saving, loading, and tracking the state of granted / revoked advancements.
29#[derive(Debug, Clone)]
30pub struct PlayerAdvancement {
31    /// the progress of the player for each advancement
32    pub(crate) progress: AdvancementProgressMap,
33    is_first_packet: bool,
34    roots_to_update: FxHashSet<AdvancementNodeRef>,
35    visible: FxHashSet<AdvancementRef>,
36    progress_changed: FxHashSet<AdvancementRef>,
37    /// represent the las selected advancement tab
38    pub last_selected_tab: Option<AdvancementRef>,
39}
40
41impl Default for PlayerAdvancement {
42    fn default() -> Self {
43        Self {
44            is_first_packet: true,
45            progress: AdvancementProgressMap::default(),
46            roots_to_update: FxHashSet::default(),
47            visible: FxHashSet::default(),
48            progress_changed: FxHashSet::default(),
49            last_selected_tab: Option::default(),
50        }
51    }
52}
53
54impl PlayerAdvancement {
55    /// Award a criterion to a player. If this completes the advancement,
56    /// grant the advancement along with its rewards and display the corresponding chat message.
57    pub(crate) fn award(
58        &mut self,
59        player: &Player,
60        advancement: AdvancementRef,
61        criterion: &str,
62    ) -> bool {
63        let mut result = false;
64        let progress = self.progress.get_mut_or_start_progress(advancement);
65        let was_done = progress.is_done();
66        if progress.grant_progress(criterion) {
67            //TODO: register advancement listener
68            self.progress_changed.insert(advancement);
69            result = true;
70            if !was_done && progress.is_done() {
71                grant_reward(player, advancement.rewards());
72                if let Some(display) = advancement.display()
73                    && display.announce_chat
74                    && player
75                        .level()
76                        .is_some_and(|level| level.get_game_rule(&SHOW_ADVANCEMENT_MESSAGES))
77                {
78                    player.server().broadcast_system_chat(
79                        &display
80                            .frame_type
81                            .create_announcement(advancement, player.display_name()),
82                        None,
83                    );
84                }
85            }
86        }
87
88        if !was_done && progress.is_done() {
89            self.mark_for_visibility_update(advancement);
90        }
91        result
92    }
93
94    /// revoke the specified criterion for a player.
95    pub fn revoke(&mut self, advancement: AdvancementRef, criterion: &str) -> bool {
96        let mut result = false;
97        let progress = self.progress.get_mut_or_start_progress(advancement);
98        let was_done = progress.is_done();
99        if progress.revoke_progress(criterion) {
100            //TODO: unregister advancement listener
101            self.progress_changed.insert(advancement);
102            result = true;
103        }
104
105        if was_done && !progress.is_done() {
106            self.mark_for_visibility_update(advancement);
107        }
108
109        result
110    }
111
112    /// mark the advancement to be sent to the client next tick
113    fn mark_for_visibility_update(&mut self, advancement: AdvancementRef) {
114        let node = REGISTRY.advancements.by_key(advancement.key());
115        if let Some(node) = node {
116            self.roots_to_update
117                .insert(REGISTRY.advancements.root(node));
118        }
119    }
120
121    /// modify the added and the removed from the available tree status
122    fn update_tree_visibility(
123        &mut self,
124        root: AdvancementNodeRef,
125        added: &mut Vec<AdvancementNodeRef>,
126        removed: &mut Vec<Identifier>,
127    ) {
128        visibility_evaluator::evaluate_visibility(
129            root,
130            self,
131            &mut |player_advancement, node| {
132                player_advancement
133                    .progress
134                    .get_mut_or_start_progress(node.value())
135                    .is_done()
136            },
137            &mut move |player_advancement, node, should_be_visible| {
138                let advancement = node.value();
139                if should_be_visible {
140                    if player_advancement.visible.insert(advancement) {
141                        added.push(node);
142                        if player_advancement.progress.has_progress(advancement) {
143                            player_advancement.progress_changed.insert(advancement);
144                        }
145                    }
146                } else if player_advancement.visible.remove(advancement) {
147                    removed.push(advancement.key().clone());
148                }
149            },
150        );
151    }
152
153    /// send the advancement update packet to a player with the updated tree
154    pub fn flush_dirty(&mut self, player: &Player, show_advancement: bool) {
155        if self.is_first_packet
156            || !self.roots_to_update.is_empty()
157            || !self.progress_changed.is_empty()
158        {
159            let mut progress: FxHashMap<Identifier, &AdvancementProgress> = FxHashMap::default();
160            let mut added: Vec<AdvancementNodeRef> = Vec::new();
161            let mut removed: Vec<Identifier> = Vec::new();
162            for root in self.roots_to_update.clone() {
163                self.update_tree_visibility(root, &mut added, &mut removed);
164            }
165            self.roots_to_update.clear();
166            for advancement in &self.progress_changed {
167                if self.visible.contains(advancement) {
168                    progress.insert(
169                        advancement.key().clone(),
170                        self.progress.get_progress(advancement).unwrap_or_else(|| {
171                            unreachable!("progress changed should only contains valid advancement")
172                        }),
173                    );
174                }
175            }
176            self.progress_changed.clear();
177            // even if it's empty it must be updated when switching domain
178            if self.is_first_packet
179                || !progress.is_empty()
180                || !added.is_empty()
181                || !removed.is_empty()
182            {
183                let parsed_progress: Vec<AdvancementProgressData> = progress
184                    .into_iter()
185                    .map(|(key, val)| AdvancementProgressData {
186                        id: key,
187                        progress: val
188                            .criteria()
189                            .iter()
190                            .map(|(key, val)| Criteria {
191                                criterion_id: key.clone(),
192                                achieve_date: val.0.map(|time| {
193                                    time.duration_since(UNIX_EPOCH)
194                                        .map_or(0, |d| d.as_millis() as i64)
195                                }),
196                            })
197                            .collect(),
198                    })
199                    .collect();
200                player.send_packet(CUpdateAdvancements::new(
201                    self.is_first_packet,
202                    added,
203                    parsed_progress,
204                    removed,
205                    show_advancement,
206                ));
207            }
208        }
209        self.is_first_packet = false;
210    }
211
212    /// change the selected tab and update the client with it
213    pub fn set_selected_tab(&mut self, player: &Player, advancement: Option<AdvancementRef>) {
214        let old = self.last_selected_tab;
215        if let Some(value) = advancement
216            && value.is_root()
217            && value.display().is_some()
218        {
219            self.last_selected_tab = advancement;
220        } else {
221            self.last_selected_tab = None;
222        }
223
224        if old != self.last_selected_tab {
225            player.send_packet(CSelectAdvancementsTab::new(
226                self.last_selected_tab.map(|e| e.key().clone()),
227            ));
228        }
229    }
230
231    /// Snapshots the advancements that still have progress for persistence.
232    pub(crate) fn persisted_progress(&self) -> Vec<(Identifier, AdvancementProgress)> {
233        self.progress
234            .iter()
235            .filter(|(_, progress)| progress.has_progress())
236            .map(|(advancement, progress)| (advancement.key().clone(), progress.clone()))
237            .collect()
238    }
239
240    /// Replaces the current progress with entries loaded from storage.
241    ///
242    /// The first flush after this call sends the full tree to the client, so a
243    /// domain switch also clears progress that belonged to the previous domain.
244    pub(crate) fn apply_persisted_progress(
245        &mut self,
246        entries: impl IntoIterator<Item = (Identifier, AdvancementProgress)>,
247    ) {
248        self.progress.clear();
249        self.visible.clear();
250        self.roots_to_update.clear();
251        self.progress_changed.clear();
252        self.last_selected_tab = None;
253        self.is_first_packet = true;
254
255        for (advancement_id, progress) in entries {
256            let Some(advancement) = REGISTRY.advancements.value_by_key(&advancement_id) else {
257                tracing::warn!(
258                    "Ignored advancement '{}' in saved progress - it doesn't exist anymore?",
259                    advancement_id
260                );
261                continue;
262            };
263            self.progress.start_progress(advancement, progress);
264            self.progress_changed.insert(advancement);
265            self.mark_for_visibility_update(advancement);
266        }
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use steel_registry::init_vanilla_registry;
274
275    fn advancement_with_criteria() -> AdvancementRef {
276        REGISTRY
277            .advancements
278            .advancements()
279            .iter()
280            .copied()
281            .find(|advancement| !advancement.requirements().names().is_empty())
282            .expect("vanilla registry should contain an advancement with criteria")
283    }
284
285    #[test]
286    fn persisted_progress_skips_advancements_without_progress() {
287        init_vanilla_registry();
288
289        let advancement = advancement_with_criteria();
290        let untouched = REGISTRY
291            .advancements
292            .advancements()
293            .iter()
294            .copied()
295            .find(|candidate| *candidate != advancement)
296            .expect("vanilla registry should contain more than one advancement");
297
298        let mut player_advancement = PlayerAdvancement::default();
299        let criterion = advancement
300            .requirements()
301            .names()
302            .into_iter()
303            .next()
304            .expect("advancement should have a criterion");
305        player_advancement
306            .progress
307            .get_mut_or_start_progress(advancement)
308            .grant_progress(&criterion);
309        player_advancement
310            .progress
311            .get_mut_or_start_progress(untouched)
312            .grant_progress("steel_never_completed");
313
314        let persisted = player_advancement.persisted_progress();
315
316        assert_eq!(persisted.len(), 1);
317        assert_eq!(persisted[0].0, *advancement.key());
318    }
319
320    #[test]
321    fn apply_persisted_progress_restores_progress_and_resets_client_state() {
322        init_vanilla_registry();
323
324        let advancement = advancement_with_criteria();
325        let criterion = advancement
326            .requirements()
327            .names()
328            .into_iter()
329            .next()
330            .expect("advancement should have a criterion");
331
332        let mut source = PlayerAdvancement::default();
333        source
334            .progress
335            .get_mut_or_start_progress(advancement)
336            .grant_progress(&criterion);
337        let persisted = source.persisted_progress();
338
339        let mut restored = PlayerAdvancement {
340            is_first_packet: false,
341            ..PlayerAdvancement::default()
342        };
343        restored.apply_persisted_progress(persisted);
344
345        assert!(restored.is_first_packet, "client state must be reset");
346        assert_eq!(restored.progress.len(), 1);
347        let progress = restored
348            .progress
349            .get_progress(advancement)
350            .expect("progress should be restored");
351        assert_eq!(
352            progress.get_completed_criteria().collect::<Vec<_>>(),
353            vec![&*criterion]
354        );
355    }
356
357    #[test]
358    fn apply_persisted_progress_ignores_unknown_advancements() {
359        init_vanilla_registry();
360
361        let unknown = Identifier::vanilla_static("steel_unknown_advancement");
362        let mut player_advancement = PlayerAdvancement::default();
363
364        player_advancement.apply_persisted_progress([(unknown, AdvancementProgress::default())]);
365
366        assert!(player_advancement.progress.is_empty());
367    }
368}