steel_core/player/advancement/
mod.rs1pub 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#[derive(Debug, Clone)]
30pub struct PlayerAdvancement {
31 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 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 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 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 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 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 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 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 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 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 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 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 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}