gdlib/ccgamemanager/structs.rs
1//! All sub-component structs of ccgamemanager::CCGameManager.
2
3use std::collections::HashMap;
4
5use bitflags::bitflags;
6use plist::{Dictionary, Value};
7
8use crate::{
9 ccgamemanager::{
10 IntMap,
11 achievements::{GDAchievement, MAX_ACHIEVEMENT_INDEX},
12 },
13 cclocallevels::{gdlevel::GDLevel, gdlist::GDList, gdobj::GDObject},
14 repr_t,
15};
16
17/// Type of a valid achievement progress value: unsigned integer that supports a maximum value of at least 10,000.
18///
19/// For achievements that are not completed, the game uses one of two formats to represent completion progress:
20/// - A value on the interval \[0, 100] which is an integer percentage
21/// - A value on that interval\[0, 10'000] which is a percentage with double decimal point precision; e.g. 1962 is 19.62%
22///
23/// A value of 100 represents a completed achievement regardless of format.
24/// It is unknown what format is used by the game for specific achievements, however it is known that for values
25/// greater than 100 that the second format is used.
26pub type AchievementProgress = u16;
27
28// ---- Substructs ----
29
30/// Player info: username, UDID, user id, all icon info
31#[derive(Debug, Default, Clone)]
32pub struct GDPlayerInfo {
33 /// Internal key: `playerName`
34 pub username: String,
35 /// Internal key: `playerUDID`
36 pub udid: String,
37 /// Internal key: `playerUserID`
38 pub user_id: i32,
39 /// Internal key: `playerFrame`
40 pub icon_cube: i32,
41 /// Internal key: `playerShip`
42 pub icon_ship: i32,
43 /// Internal key: `playerBall`
44 pub icon_ball: i32,
45 /// Internal key: `playerBird`
46 pub icon_ufo: i32,
47 /// Internal key: `playerDart`
48 pub icon_wave: i32,
49 /// Internal key: `playerRobot`
50 pub icon_robot: i32,
51 /// Internal key: `playerSpider`
52 pub icon_spider: i32,
53 /// Internal key: `playerSwing`
54 pub icon_swing: i32,
55 /// Internal key: `playerColor`
56 pub player_col1: i32,
57 /// Internal key: `playerColor2`
58 pub player_col2: i32,
59 /// Internal key: `playerColor3`
60 pub player_col_glow: i32,
61 /// Internal key: `playerStreak`
62 pub icon_streak: i32,
63 /// Internal key: `playerShipStreak`
64 pub ship_streak: i32,
65 /// Internal key: `playerDeathEffect`
66 pub death_effect: i32,
67 /// Internal key: `playerJetpack`
68 pub icon_jetpack: i32,
69 /// Internal key: `playerIconType`
70 pub icon_type: i32,
71 /// Internal key: `playerGlow`
72 pub using_glow: bool,
73 /// Internal key: `hasRP`
74 pub is_moderator: bool,
75}
76
77/// Player-specific statistics.
78#[derive(Debug, Clone)]
79pub struct GDStatistics {
80 /// Number of times this player has launched GD
81 ///
82 /// Internal key: `bootups`
83 pub bootups: i32,
84 /// All official levels that the player has progress on.
85 ///
86 /// Internal key: `GLM_01`
87 pub official_level_progresses: Vec<GDLevel>,
88 /// Internal key: `GLM_03`
89 pub online_levels_played: Vec<GDLevel>,
90 /// Internal key: `GS_20`
91 pub demon_keys: i32,
92 /// All level IDs the player has submitted difficulty ratings for
93 ///
94 /// Internal key: `GLM_13`
95 pub submitted_ratings: Vec<i32>,
96 /// All demon levels the player has submitted ratings on
97 ///
98 /// Internal key: `GLM_15`
99 pub submitted_ratings_demons: Vec<i32>,
100 /// All gauntlet levels that the player has progress on
101 ///
102 /// Internal key: `GLM_16`
103 pub gauntlet_levels_played: Vec<GDLevel>,
104 /// All completed dailies in the form `{timely id: level}`
105 ///
106 /// Internal keys: `GLM_10`
107 pub completed_dailies: IntMap<GDLevel>,
108 /// All vanilla achievements. Each element is the achievement's progress is an integer from 0 - 100 inclusive.
109 /// Use [`GDStatistics::get_vanilla_achievement`] to get an achievement's progress when using the [`GDAchievement`] enum.
110 ///
111 /// The split between vanilla achievements and custom achievements is to optimize indexing of achievements.
112 pub achievements: [AchievementProgress; MAX_ACHIEVEMENT_INDEX],
113 /// Achievements that are unaccounted for in `self.achievements`. This includes any undocumented or custom-made achievements,
114 /// including those added by mods.
115 pub custom_achievements: HashMap<String, AchievementProgress>,
116}
117
118// manual impl exists because `Default` isn't implemented for `[T; N]` such that N > 32
119// and there are a lot more than 32 achievements in GD
120impl Default for GDStatistics {
121 fn default() -> Self {
122 Self {
123 bootups: 0,
124 official_level_progresses: Vec::new(),
125 online_levels_played: Vec::new(),
126 demon_keys: 0,
127 submitted_ratings: Vec::new(),
128 submitted_ratings_demons: Vec::new(),
129 gauntlet_levels_played: Vec::new(),
130 completed_dailies: HashMap::default(),
131 achievements: [0; MAX_ACHIEVEMENT_INDEX],
132 custom_achievements: HashMap::default(),
133 }
134 }
135}
136
137/// User's configuration of the game.
138#[derive(Debug, Default, Clone)]
139pub struct GDConfig {
140 /// Internal key: `bgVolume`
141 pub bgm_volume: f32,
142 /// Internal key: `sfxVolume`
143 pub sfx_volume: f32,
144 /// Internal key: `texQuality`
145 pub texture_quality: TextureQuality,
146 /// Internal key: `resolution`
147 pub resolution: Resolution,
148 /// Internal key: `showSongMarkers`
149 pub show_song_markers: bool,
150 /// Internal key: `showProgressBar`
151 pub show_progress_bar: bool,
152 /// Internal key: `clickedGarage`
153 pub has_clicked_garage: bool,
154 /// Internal key: `clickedEditor`
155 pub has_clicked_editor: bool,
156 /// Internal key: `clickedPractice`
157 pub has_clicked_practice: bool,
158 /// Internal key: `showedEditorGuide`
159 pub seen_editor_guide: bool,
160 /// Internal key: `showedLowDetailDialog`
161 pub seen_ldm_dialog: bool,
162 /// Internal key: `showedRateStarDialog`
163 pub seen_rate_star_dialog: bool,
164 /// Internal key: `hasRatedGame`
165 pub has_rated_game: bool,
166 /// Internal key: `binaryVersion`
167 pub binary_version: i32,
168 /// Respective internal keys: `practicePosX`, `praticePosY`
169 pub practice_ui_pos: (f32, f32),
170 /// Internal key: `practiceOpacity`
171 pub practice_ui_opacity: f32,
172 /// Internal key: `customFPSTarget`
173 pub fps_target: f32,
174 /// Music offset in milliseconds
175 ///
176 /// Internal key: `timeOffset`
177 pub music_offset: i32,
178 /// Internal keys: `dpad01`, `dpad02`, `dpad03`, `dpad04`, `dpad05`
179 pub dpads: [GDPlatformerUI; 5],
180 /// Internal key: `dpad_layout`
181 pub dpad_layout: Option<GDPlatformerUI>,
182 /// List of folder names for saved online levels. Folder names are stored in order, starting from folder 1. If an unnamed folder is found at index >= 1, it is stored as a `None`.
183 ///
184 /// Internal key: `GLM_18`
185 pub saved_levels_foldernames: Vec<(i32, String)>,
186 /// List of folder names for locally created levels (found in the editor tab). Folder names are stored in order, starting from folder 1. If an unnamed folder is found at index >= 1, it is stored as a `None`.
187 ///
188 /// Internal key: `GLM_19`
189 pub local_levels_foldernames: Vec<(i32, String)>,
190 /// Raw GLM_12 key encoding optimized for size. This key has a purpose that is assumed to be related to likes, though it is unknown.
191 ///
192 /// Internal key: `GLM_12`
193 pub glm12_unknown: Vec<[i32; 4]>,
194 /// Internal key: `GLM_23`
195 pub glm23_unknown: i32,
196 /// Lists that this player has favourited.
197 ///
198 /// Internal key: `GLM_22`
199 pub favourite_lists: Vec<GDList>,
200 /// Saved custom objects from the editor. Custom objects are indexed by some negative integer in the raw dictionary which serves an unclear purpose. Each element is stored as (index, objects).
201 ///
202 /// Internal key: `customObjectDict`
203 pub custom_objects: Vec<(i32, Vec<GDObject>)>,
204 /// Search filters for online levels
205 ///
206 /// Internal key: `GLM_08`
207 pub search_filters: GDSearchFilters,
208 /// Answer to `cod3breaker` in the vault of secrets
209 ///
210 /// Internal key: `secretNumber`
211 pub secret_number: i32,
212}
213
214/// Configuration to do with the player's account and social settings
215#[derive(Debug, Default, Clone)]
216pub struct GDAccount {
217 /// Internal key: `GJA_001`
218 pub username: String,
219 /// Password in plaintext (used in 2.1 and below)
220 ///
221 /// Internal key: `GJA_002`
222 pub plaintext_password: Option<String>,
223 /// Internal key: `GJA_003`
224 pub account_id: i32,
225 /// Appears to be unused
226 ///
227 /// Internal key: `GJA_004`
228 pub session_id: Option<String>,
229 /// Password encrypted with GJP2 encryption. This can be generated with [`crate::core::crypto::generate_gjp2_hexdigest`]
230 ///
231 /// Internal key: `GJA_005`
232 pub hashed_password: Option<String>,
233 /// List of creators' account IDs that this player follows
234 ///
235 /// Internal key: `GLM_06`
236 pub following_creators: Vec<i32>,
237 /// List of levels that the player has reported.
238 ///
239 /// Internal key: `GLM_14`
240 pub reported_levels: Vec<i32>,
241}
242
243/// Temporary variables stored in the savefile that are expected to be overwritten in the future
244#[derive(Debug, Default, Clone)]
245pub struct GDCurrentValues {
246 /// Levels that were played in the last session
247 ///
248 /// Internal key: `GLM_07`
249 pub last_played_levels: Vec<i32>,
250 /// The current daily level's TimelyID
251 ///
252 /// Internal key: `GLM_11`
253 pub current_daily_level: i32,
254 /// The current weekly level's TimelyID
255 ///
256 /// Internal key: `GLM_17`
257 pub current_weekly_level: i32,
258}
259
260/// Song config
261#[derive(Debug, Default, Clone)]
262pub struct GDSongConfig {
263 /// Presumably the songs that the user has stored or downloaded, but is unknown.
264 ///
265 /// Internal key: `MDLM_001`
266 pub stored_songs: (), // Vec<SongInfoObject>: TODO
267 /// Has something to do with song priority, but is unknown.
268 ///
269 /// Internal key: `MDLM_002`
270 pub song_priority: i32,
271}
272
273// ---- Supplementary structs ----
274
275/* todo
276struct descriptor for SongInfoObject:
277
2781 ID Integer The ID of the song on Newgrounds
2792 name String The name of the song
2803 artistID Integer Newgrounds ArtistID
2814 artistName String The name of the artist who made the song
2825 size Integer Size of the song in MB, rounded to two decimal places
2836 videoID String the Video ID for the songs YouTube Video
2847 youtubeURL String The URL of the newgrounds user's youtube channel
2858 isVerified Bool if the song artist is scouted on newgrounds
2869 songPriority Integer priority over the song list
28710 link String Link to the song's mp3
28811 nongEnum Integer Type of NONG. 0 for none, 1 for NCS.
28912 extraArtistIDs Array[Integer] IDs of extra artists, separated by .
29013 new Boolean Whether the NEW icon shows up or not
29114 newType Integer Type of NEW icon. 0 for Yellow, 1 for Blue
29215 extraArtistNames Array Artist names in this format: {id},{name},{id},{name}
293
294*/
295
296repr_t!(
297 strict TextureQuality: i32 {
298 Auto = 0,
299 Low = 1,
300 Medium = 2,
301 High = 3,
302 } default Auto
303);
304
305repr_t!(
306 strict Resolution: i32 {
307 R640x480 = 1, // 4:3
308 R720x480 = 2, // 3:2
309 R720x576 = 3, // 5:4
310 R800x600 = 4, // 4:3
311 R1024x768 = 5, // 4:3
312 R1152x864 = 6, // 4:3
313 R1176x664 = 7, // 147:83
314 R1280x720 = 8, // 16:9
315 R1280x768 = 9, // 5:3
316 R1280x800 = 10, // 16:10
317 R1280x960 = 11, // 4:3
318 R1280x1024 = 12, // 5:4
319 R1360x768 = 13, // 85:48
320 R1366x768 = 14, // 683:384
321 R1440x900 = 15, // 16:10
322 R1600x900 = 16, // 16:9
323 R1600x1024 = 17, // 25:16
324 R1600x1200 = 18, // 4:3
325 R1680x1050 = 19, // 16:10
326 R1768x992 = 20, // 221:124
327 R1920x1080 = 21, // 16:9
328 R1920x1200 = 22, // 16:10
329 R1920x1440 = 23, // 4:3
330 R2048x1536 = 24, // 4:3
331 R2560x1440 = 25, // 16:9
332 R2560x1600 = 26, // 16:10
333 R3840x2160 = 27, // 16:9
334 } default R1920x1080
335);
336
337#[derive(Debug, Default, Clone)]
338/// Platformer controls UI config
339pub struct GDPlatformerUI {
340 /// The width of the button hitbox
341 pub width: i32,
342 /// The height of the button hitbox
343 pub height: i32,
344 /// The scale of the buttons
345 pub scale: f32,
346 /// The button opacity (from 0 to 255)
347 pub opacity: i32,
348 /// The position of the buttons
349 pub pos: (f32, f32),
350 /// The ModeB checkbox
351 pub mode_b: bool,
352 /// The deadzone between the buttons
353 pub deadzone: f32,
354 /// The distance between the buttons
355 pub radius: f32,
356 /// The Snap checkbox
357 pub snap: bool,
358 /// The Split checkbox
359 pub split: bool,
360}
361
362impl GDPlatformerUI {
363 /// Parses a comma-separated list of values to this object
364 pub fn from_str(s: &str) -> Self {
365 let mut this = Self::default();
366 let fns = &[
367 Self::parse_width,
368 Self::parse_height,
369 Self::parse_scale,
370 Self::parse_opacity,
371 Self::parse_pos_x,
372 Self::parse_pos_y,
373 Self::parse_mode_b,
374 Self::parse_deadzone,
375 Self::parse_radius,
376 Self::parse_snap,
377 Self::parse_split,
378 ];
379 s.split(",")
380 .into_iter()
381 .enumerate()
382 .for_each(|(idx, s)| (fns[idx])(&mut this, s));
383
384 this
385 }
386
387 fn parse_width(&mut self, s: &str) {
388 self.width = s.parse::<i32>().unwrap();
389 }
390 fn parse_height(&mut self, s: &str) {
391 self.height = s.parse::<i32>().unwrap();
392 }
393 fn parse_scale(&mut self, s: &str) {
394 self.scale = s.parse::<f32>().unwrap();
395 }
396 fn parse_opacity(&mut self, s: &str) {
397 self.opacity = s.parse::<i32>().unwrap();
398 }
399 fn parse_pos_x(&mut self, s: &str) {
400 self.pos.0 = s.parse::<f32>().unwrap();
401 }
402 fn parse_pos_y(&mut self, s: &str) {
403 self.pos.1 = s.parse::<f32>().unwrap();
404 }
405 fn parse_mode_b(&mut self, s: &str) {
406 self.mode_b = s.parse::<i32>().unwrap() == 1;
407 }
408 fn parse_deadzone(&mut self, s: &str) {
409 self.deadzone = s.parse::<f32>().unwrap();
410 }
411 fn parse_radius(&mut self, s: &str) {
412 self.radius = s.parse::<f32>().unwrap();
413 }
414 fn parse_snap(&mut self, s: &str) {
415 self.snap = s.parse::<i32>().unwrap() == 1;
416 }
417 fn parse_split(&mut self, s: &str) {
418 self.split = s.parse::<i32>().unwrap() == 1;
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Default)]
423/// Search filter state for online levels
424pub struct GDSearchFilters {
425 /// The majority of search filters. Contains all search boolean flags by which online levels may be searched.
426 pub boolean_flags: GDSearchFilterFlags,
427 /// Optionally enabled song ID filter. Filters out all levels that do not have this song ID if given.
428 pub song: Option<i32>,
429 /// Values that are unaccounted for. This dictionary should be empty when parsing a vanilla savefile.
430 pub other: HashMap<String, Value>,
431}
432
433bitflags! {
434 #[derive(Debug, Copy, Clone, PartialEq, Default, Eq, Hash)]
435 #[must_use]
436 /// Filters for searching online levels
437 pub struct GDSearchFilterFlags: u32 {
438 /// Internal key: `Diff0`
439 const DifficultyNA = 1;
440 /// Internal key: `Diff1`
441 const DifficultyAuto = 1 << 1;
442 /// Internal key: `Diff2`
443 const DifficultyEasy = 1 << 2;
444 /// Internal key: `Diff3`
445 const DifficultyNormal = 1 << 3;
446 /// Internal key: `Diff4`
447 const DifficultyHard = 1 << 4;
448 /// Internal key: `Diff5`
449 const DifficultyHarder = 1 << 5;
450 /// Internal key: `Diff6`
451 const DifficultyInsane = 1 << 6;
452 /// Internal key: `Diff7`
453 const DifficultyDemon = 1 << 7;
454 /// Tiny levels
455 ///
456 /// Internal key: `Len0`
457 const LengthTiny = 1 << 8;
458 /// Small levels
459 ///
460 /// Internal key: `Len1`
461 const LengthSmall = 1 << 9;
462 /// Medium levels
463 ///
464 /// Internal key: `Len2`
465 const LengthMedium = 1 << 10;
466 /// Long levels
467 ///
468 /// Internal key: `Len3`
469 const LengthLong = 1 << 11;
470 /// XL levels
471 ///
472 /// Internal key: `Len4`
473 const LengthXL = 1 << 12;
474 /// Internal key: `demon_filter`
475 const DemonFilter = 1 << 13;
476 /// Platformer levels
477 ///
478 /// Internal key: `Len5`
479 const Platformer = 1 << 14;
480 /// Star-rated levels
481 ///
482 /// Internal key: `star_filter`
483 const HasStars = 1 << 15;
484 /// Internal key: `customsong_filter`
485 const CustomSongFilter = 1 << 16;
486 /// Internal key: `mythic_filter`
487 const IsMythic = 1 << 18;
488 /// Internal key: `enable_songFilter`
489 const SongFilterEnabled = 1 << 19;
490 /// Works only for rated levels.
491 ///
492 /// Internal key: `uncompleted_filter`
493 const IsUncompleted = 1 << 20;
494 /// Internal key: `completed_filter`
495 const IsCompleted = 1 << 21;
496 /// Internal key: `featured_filter`
497 const IsFeatured = 1 << 22;
498 /// Internal key: `original_filter`
499 const IsOriginal = 1 << 23;
500 /// Internal key: `twoP_filter`
501 const TwoPlayer = 1 << 24;
502 /// Internal key: `nostar_filter`
503 const NoStars = 1 << 25;
504 /// Internal key: `coin_filter`
505 const HasCoins = 1 << 26;
506 /// Levels from creators that the player follows.
507 ///
508 /// Internal key: `follow_filter`
509 const FollowedCreator = 1 << 27;
510 /// Levels from friends of this player.
511 ///
512 /// Internal key: `friend_filter`
513 const Friends = 1 << 28;
514 /// Levels with a rating of epic.
515 ///
516 /// Internal key: `epic_filter`
517 const RatingEpic = 1 << 29;
518 /// Internal key: `legendary_filter`
519 const RatingLegendary = 1 << 30;
520 }
521}
522
523impl GDSearchFilters {
524 /// Parses a dictionary from CCGameManager to search filters. This function will return `None` if the filter
525 /// dictionary is malformed, which can be due to of the following:
526 /// * Any value is not stored as a string
527 /// * The value for the `song_filter` key is not a numerical string
528 ///
529 /// Values not accounted for are still parsed, however they are left as-is and stored in `self.other`.
530 pub fn from_dict(d: &Dictionary) -> Option<Self> {
531 let mut this = Self::default();
532 for (k, v) in d.iter() {
533 let bool_value = v.as_string()? == "1";
534 match k.as_str() {
535 "Diff0" => this
536 .boolean_flags
537 .set(GDSearchFilterFlags::DifficultyNA, bool_value),
538 "Diff1" => this
539 .boolean_flags
540 .set(GDSearchFilterFlags::DifficultyAuto, bool_value),
541 "Diff2" => this
542 .boolean_flags
543 .set(GDSearchFilterFlags::DifficultyEasy, bool_value),
544 "Diff3" => this
545 .boolean_flags
546 .set(GDSearchFilterFlags::DifficultyNormal, bool_value),
547 "Diff4" => this
548 .boolean_flags
549 .set(GDSearchFilterFlags::DifficultyHard, bool_value),
550 "Diff5" => this
551 .boolean_flags
552 .set(GDSearchFilterFlags::DifficultyHarder, bool_value),
553 "Diff6" => this
554 .boolean_flags
555 .set(GDSearchFilterFlags::DifficultyInsane, bool_value),
556 "Diff7" => this
557 .boolean_flags
558 .set(GDSearchFilterFlags::DifficultyDemon, bool_value),
559 "Len0" => this
560 .boolean_flags
561 .set(GDSearchFilterFlags::LengthTiny, bool_value),
562 "Len1" => this
563 .boolean_flags
564 .set(GDSearchFilterFlags::LengthSmall, bool_value),
565 "Len2" => this
566 .boolean_flags
567 .set(GDSearchFilterFlags::LengthMedium, bool_value),
568 "Len3" => this
569 .boolean_flags
570 .set(GDSearchFilterFlags::LengthLong, bool_value),
571 "Len4" => this
572 .boolean_flags
573 .set(GDSearchFilterFlags::LengthXL, bool_value),
574 "demon_filter" => this
575 .boolean_flags
576 .set(GDSearchFilterFlags::DemonFilter, bool_value),
577 "Len5" => this
578 .boolean_flags
579 .set(GDSearchFilterFlags::Platformer, bool_value),
580 "star_filter" => this
581 .boolean_flags
582 .set(GDSearchFilterFlags::HasStars, bool_value),
583 "customsong_filter" => this
584 .boolean_flags
585 .set(GDSearchFilterFlags::CustomSongFilter, bool_value),
586 "mythic_filter" => this
587 .boolean_flags
588 .set(GDSearchFilterFlags::IsMythic, bool_value),
589 "enable_songFilter" => this
590 .boolean_flags
591 .set(GDSearchFilterFlags::SongFilterEnabled, bool_value),
592 "uncompleted_filter" => this
593 .boolean_flags
594 .set(GDSearchFilterFlags::IsUncompleted, bool_value),
595 "completed_filter" => this
596 .boolean_flags
597 .set(GDSearchFilterFlags::IsCompleted, bool_value),
598 "featured_filter" => this
599 .boolean_flags
600 .set(GDSearchFilterFlags::IsFeatured, bool_value),
601 "original_filter" => this
602 .boolean_flags
603 .set(GDSearchFilterFlags::IsOriginal, bool_value),
604 "twoP_filter" => this
605 .boolean_flags
606 .set(GDSearchFilterFlags::TwoPlayer, bool_value),
607 "nostar_filter" => this
608 .boolean_flags
609 .set(GDSearchFilterFlags::NoStars, bool_value),
610 "coin_filter" => this
611 .boolean_flags
612 .set(GDSearchFilterFlags::HasCoins, bool_value),
613 "follow_filter" => this
614 .boolean_flags
615 .set(GDSearchFilterFlags::FollowedCreator, bool_value),
616 "friend_filter" => this
617 .boolean_flags
618 .set(GDSearchFilterFlags::Friends, bool_value),
619 "epic_filter" => this
620 .boolean_flags
621 .set(GDSearchFilterFlags::RatingEpic, bool_value),
622 "legendary_filter" => this
623 .boolean_flags
624 .set(GDSearchFilterFlags::RatingLegendary, bool_value),
625 "song_filter" => this.song = Some(v.as_string()?.parse::<i32>().ok()?),
626 _ => {
627 this.other.insert(k.clone(), v.clone());
628 }
629 }
630 }
631
632 Some(this)
633 }
634}
635
636impl GDStatistics {
637 /// Returns the reported progress value on this specific vanilla achievement. See [`AchievementProgress`].
638 #[inline]
639 pub fn get_vanilla_achievement(&self, a: GDAchievement) -> AchievementProgress {
640 // subtract one from index because of the way that achievement repr was created
641 self.achievements[a as i32 as usize - 1]
642 }
643
644 /// Returns the reported progress value on this specific achievement. The given identifier is first parsed
645 /// as a vanilla achievement. If the achievement is a vanilla achievement, [`Self::get_vanilla_achievement`]
646 /// is used to retrieve its value. Otherwise, it is looked up in `self.custom_achievements`.
647 /// See [`AchievementProgress`].
648 #[inline]
649 pub fn get_achievement_by_ident(&self, ident: &str) -> Option<AchievementProgress> {
650 match GDAchievement::parse_str(ident) {
651 Some(a) => Some(self.get_vanilla_achievement(a)),
652 None => {
653 // this is not a vanilla achievement; lookup in custom achievements
654 self.custom_achievements.get(ident).copied()
655 }
656 }
657 }
658
659 /// Set an achievement's progress value. See [`AchievementProgress`].
660 #[inline]
661 pub fn set_vanilla_achievement(&mut self, a: GDAchievement, value: AchievementProgress) {
662 self.achievements[a as i32 as usize - 1] = value;
663 }
664
665 /// Set an achievement's progress value. The given identifier is first parsed
666 /// as a vanilla achievement. If the achievement is a vanilla achievement, [`Self::get_vanilla_achievement`]
667 /// is used to set its value. Otherwise, it is looked up in `self.custom_achievements`.
668 /// See [`AchievementProgress`].
669 #[inline]
670 pub fn set_achievement_by_ident(&mut self, ident: &str, value: AchievementProgress) {
671 match GDAchievement::parse_str(ident) {
672 Some(a) => self.set_vanilla_achievement(a, value),
673 None => {
674 // this is not a vanilla achievement; lookup in custom achievements
675 let _ = self.custom_achievements.insert(ident.to_string(), value);
676 }
677 }
678 }
679}