Skip to main content

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}