gd_rs/
prelude.rs

1use crate::consts::*;
2use crate::traits::*;
3use crate::util::*;
4use base64::{engine::general_purpose, Engine as _};
5use directories::BaseDirs;
6use libflate::gzip::Decoder;
7use plist;
8use serde_derive::Deserialize;
9use std::{collections::HashMap, fs};
10
11type BoxERR = Box<dyn std::error::Error>;
12
13#[derive(Debug, Clone, Copy)]
14pub enum GDObjectHitbox {
15    Rect(f32, f32),
16    Circle(f32),
17    Triangle(f32, f32)
18}
19
20
21
22#[derive(Debug, Clone, Default, PartialEq)]
23pub struct GDObject {
24    // id of an object
25    pub id: u16,
26    pub x: f32,
27    pub y: f32,
28    pub group_id: Vec<u16>,
29    pub rotation: f32,
30    pub scalex: f32,
31    pub scaley: f32,
32    // Hashmap for reading raw object data
33    pub props: HashMap<String, String>,
34}
35
36#[allow(dead_code)]
37impl GDObject {
38    pub fn as_string(&self) -> String {
39        let mut props = self.props.clone();
40        props.insert("1".to_string(), self.id.to_string());
41        props.insert("2".to_string(), self.x.to_string());
42        props.insert("3".to_string(), self.y.to_string());
43        if self.group_id.len() > 1 {
44            let mut arr_str = Vec::new();
45            for n in &self.group_id {
46                arr_str.push(n.to_string());
47            }
48            props.insert("57".to_string(), arr_str.join("."));
49        } else if self.group_id.len() == 1 {
50            props.insert("33".to_string(), self.group_id.get(0).unwrap().to_string());
51        }
52
53
54        let mut out = String::new();
55        for (key, value) in props {
56            out.push_str(format!("{},{},", key, value).as_str());
57        }
58        return out;
59    }
60
61    pub fn partial_get_prop<V: GDProperty>(&self, key: &str) -> Option<V> {
62        for (ik, nk) in GD_PROPS_MAP {
63            if nk == key {
64                if let Some(val) = self.props.get(ik) {
65                    return Some(V::from_gd_string(val));
66                }
67                return None
68            }
69        }
70        None
71    }
72
73    // Sets the property `key` to `value` that has trait GDProperty(for <V>.as_gd_string fn) panics
74    // if key doesnt exist
75    pub fn set_prop<V>(&mut self, key: &str, value: V) -> Option<String> where V: GDProperty
76    {
77        for (ik, nk) in GD_PROPS_MAP {
78            if nk == key {
79                return self.props.insert(ik.to_string(), value.as_gd_string());
80            }
81        }
82        panic!("Invalid key {key} with {}", value.as_gd_string());
83    }
84
85    // Gets the property `key` and returns it or `def` panics if the key doesnt exist
86    pub fn get_prop<V: GDProperty>(&self, key: &str, def: &str) -> V {
87        for (ik, nk) in GD_PROPS_MAP {
88            if nk == key {
89                let value = self.partial_get_prop::<V>(key);
90                if let Some(v) = value {
91                    return v;
92                }
93                return V::from_gd_string(&def.to_string());
94            }
95        }
96        panic!("Invalid key {key}");
97    }
98
99    // Its just self.props.get that retursn `value` or `def`
100    pub fn get_raw_prop(&self, key: &str, def: &str) -> String {
101        self.props.get(key).unwrap_or(&def.to_string()).clone()
102    }
103
104    // Its just self.props.insert
105    pub fn set_raw_prop(&mut self, k: &str, v: String) -> Option<String> {
106        self.props.insert(k.to_string(), v)
107    }
108}
109
110// GDLocalLevel is just InnerLevelString into a struct
111// You can convert GDLocalLevel to GDRawLocalLevel 
112//
113#[derive(Debug, Clone, Default, PartialEq)]
114pub struct GDLocalLevel {
115    // INNER LEVEL STRING START
116    pub mode: GDLevelMode,
117    pub speed: u16,
118    pub mini_mode: bool,
119    pub dual_mode: bool,
120    pub player_mode2: bool,
121    pub flip_gravity: bool,
122    pub platformer_mode: bool,
123    // INNER LEVEL STRING END
124    pub objects: Vec<GDObject>,
125    pub name: String,
126    pub raw: GDRawLocalLevel 
127}
128
129impl Into<GDRawLocalLevel> for GDLocalLevel {
130    fn into(self) -> GDRawLocalLevel {
131        let mut raw = self.raw.clone();
132        raw.inner_level_string = self.get_inner_level_string();
133        raw.level_name = self.name;
134        
135        return raw;        
136    }
137}
138
139impl GDLocalLevel {
140    pub fn get_inner_level_string(&self) -> String {
141        let mut lsos = self.get_level_start();
142        lsos.push_str(&self.get_object_string());
143
144        return general_purpose::URL_SAFE.encode(lsos);
145    }
146
147    pub fn get_level_start(&self) -> String {
148        let mut props = Vec::new();
149        
150        props.push(format!("kA2,{}", self.mode.as_string())); // gamemode
151        props.push(format!("kA3,{}", self.mini_mode.as_gd_string()));
152        props.push(format!("kA4,{}", self.speed.to_string())); // speed
153        props.push(format!("kA8,{}", self.dual_mode.to_string()));
154        props.push(format!("kA10,{}",self.player_mode2.as_gd_string()));
155        props.push(format!("kA11,{}", self.flip_gravity.as_gd_string()));
156        props.push(format!("kA22,{}", self.platformer_mode.as_gd_string()));
157
158        return props.join(",");
159    }
160
161    pub fn get_object_string(&self) -> String {
162        let mut os = String::new();
163        for obj in &self.objects {
164            os.push_str(obj.as_string().as_str());
165        }
166        return os;
167    }
168    
169    pub fn object_by_gid(&self, gid: u16) -> Vec<usize> {
170        let mut objs = Vec::new();
171
172        for (i, obj) in self.objects.iter().enumerate() {
173            if obj.group_id.contains(&gid) {
174                objs.push(i);
175            }
176        }
177
178        objs
179    }
180}
181
182#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
183pub enum GDLevelMode {
184    #[default]
185    Cube,
186    Ship,
187    Ball,
188    UFO,
189    Wave,
190    Robot,
191    Swing,
192    Spider,
193}
194
195impl From<u16> for GDLevelMode {
196    fn from(value: u16) -> Self {
197        match value {
198            1 => GDLevelMode::Ship,
199            2 => GDLevelMode::Ball,
200            3 => GDLevelMode::UFO,
201            4 => GDLevelMode::Wave,
202            5 => GDLevelMode::Robot,
203            6 => GDLevelMode::Spider,
204            7 => GDLevelMode::Swing,
205            _ => GDLevelMode::Cube,
206        }
207    }
208}
209
210impl GDLevelMode {
211    fn as_string(self) -> String {
212        let v = match self {
213            GDLevelMode::Ship => 1,
214            GDLevelMode::Ball => 2,
215            GDLevelMode::UFO => 3,
216            GDLevelMode::Wave => 4,
217            GDLevelMode::Robot => 5,
218            GDLevelMode::Spider => 6,
219            GDLevelMode::Swing => 7,
220            _ => 0, // Default value when mode is not matched
221        };
222
223        return v.to_string();
224    }
225}
226
227
228#[allow(non_snake_case)]
229#[derive(Debug, Deserialize, Default, Clone, PartialEq)]
230pub struct GDRawLocalLevel {
231        #[serde(rename = "k1")]
232        pub level_id: Option<u32>,
233        #[serde(rename = "k2")]
234        pub level_name: String,
235        #[serde(rename = "k3")]
236        pub description: Option<String>,
237        #[serde(rename = "k4")]
238        pub inner_level_string: String,
239        #[serde(rename = "k5")]
240        pub creator: String,
241        #[serde(rename = "k6")]
242        pub userid: Option<u32>,
243        #[serde(rename = "k7")]
244        pub level_difficulty: Option<u32>,
245        #[serde(rename = "k8")]
246        pub official_song_id: Option<u32>,
247        #[serde(rename = "k9")]
248        pub rating: Option<u32>,
249        #[serde(rename = "k10")]
250        pub ratingsum: Option<u32>,
251        #[serde(rename = "k11")]
252        pub downloads: Option<u32>,
253        #[serde(rename = "k12")]
254        pub setcompletes: Option<u32>,
255        #[serde(rename = "k13")]
256        pub iseditable: Option<String>,
257        #[serde(rename = "k14")]
258        pub verified: Option<String>,
259        #[serde(rename = "k15")]
260        pub uploaded: Option<String>,
261        #[serde(rename = "k16")]
262        pub level_version: Option<u32>,
263        #[serde(rename = "k18")]
264        pub game_version: Option<u32>,
265        #[serde(rename = "k18")]
266        pub attempts: Option<u32>,
267        #[serde(rename = "k19")]
268        pub normal_mode_percentage: Option<u32>,
269        #[serde(rename = "k20")]
270        pub practice_mode_percentage: Option<u32>,
271        #[serde(rename = "k21")]
272        pub leveltype: Option<u32>,
273        #[serde(rename = "k22")]
274        pub like_rating: Option<u32>,
275        #[serde(rename = "k23")]
276        pub length: Option<u64>,
277        #[serde(rename = "k24")]
278        pub dislikes: Option<u32>,
279        #[serde(rename = "k25")]
280        pub isdemon: Option<String>,
281        #[serde(rename = "k26")]
282        pub stars: Option<u32>,
283        #[serde(rename = "k27")]
284        pub featurescore: Option<u32>,
285        #[serde(rename = "k33")]
286        pub auto: Option<String>,
287        #[serde(rename = "k34")]
288        pub replay_data: Option<String>,
289        #[serde(rename = "k35")]
290        pub isplayable: Option<String>,
291        #[serde(rename = "k36")]
292        pub jumps: Option<u32>,
293        #[serde(rename = "k37")]
294        pub required_coins: Option<u32>,
295        #[serde(rename = "k38")]
296        pub isunlocked: Option<String>,
297        #[serde(rename = "k39")]
298    // TODO try to parse the value with a generic to convert to
299        pub level_size: Option<u32>,
300        #[serde(rename = "k40")]
301        pub build_version: Option<u32>,
302        #[serde(rename = "k41")]
303        pub password: Option<u32>,
304        #[serde(rename = "k42")]
305        pub original: Option<u32>,
306        #[serde(rename = "k43")]
307        pub two_player_mode: Option<String>,
308        #[serde(rename = "k45")]
309        pub custom_song_id: Option<u32>,
310        #[serde(rename = "k46")]
311        pub level_revision: Option<u32>,
312        #[serde(rename = "k47")]
313        pub hasbeenmodified: Option<String>,
314        #[serde(rename = "k48")]
315        pub object_count: Option<u32>,
316        #[serde(rename = "k50")]
317        pub binary_version: Option<u32>,
318        #[serde(rename = "k51")]
319        pub capacity001: Option<u32>,
320        #[serde(rename = "k52")]
321        pub capacity002: Option<u32>,
322        #[serde(rename = "k53")]
323        pub capacity003: Option<u32>,
324        #[serde(rename = "k54")]
325        pub capacity004: Option<u32>,
326        #[serde(rename = "k60")]
327        pub accountid: Option<u32>,
328        #[serde(rename = "k61")]
329        pub first_coin_acquired: Option<String>,
330        #[serde(rename = "k62")]
331        pub second_coin_acquired: Option<String>,
332        #[serde(rename = "k63")]
333        pub third_coin_acquired: Option<String>,
334        #[serde(rename = "k64")]
335        pub total_coins: Option<u32>,
336        #[serde(rename = "k65")]
337        pub arecoinsverified: Option<String>,
338        #[serde(rename = "k66")]
339        pub requested_stars: Option<u32>,
340        #[serde(rename = "k67")]
341        pub capacity_string: Option<String>,
342        #[serde(rename = "k68")]
343        pub triggeredanticheat: Option<String>,
344        #[serde(rename = "k69")]
345        pub high_object_count: Option<String>,
346        #[serde(rename = "k71")]
347        pub mana_orb_percentage: Option<u32>,
348        #[serde(rename = "k72")]
349        pub haslowdetailmode: Option<String>,
350        #[serde(rename = "k73")]
351        pub toggleldm: Option<String>,
352        #[serde(rename = "k74")]
353        pub timelyid: Option<u32>,
354        #[serde(rename = "k75")]
355        pub isepic: Option<String>,
356        #[serde(rename = "k76")]
357        pub demon_type: Option<u32>,
358        #[serde(rename = "k77")]
359        pub isgauntlet: Option<String>,
360        #[serde(rename = "k78")]
361        pub isaltgame: Option<String>,
362        #[serde(rename = "k79")]
363        pub unlisted: Option<String>,
364        #[serde(rename = "k80")]
365        pub seconds_spent_editing: Option<u32>,
366        #[serde(rename = "k82")]
367        pub islevelfavourited: Option<String>,
368        #[serde(rename = "k83")]
369        pub levelorder: Option<u32>,
370        #[serde(rename = "k84")]
371        pub level_folder: Option<u32>,
372        #[serde(rename = "k85")]
373        pub clicks: Option<u32>,
374        #[serde(rename = "k86")]
375        pub player_time: Option<u32>,
376        #[serde(rename = "k87")]
377        pub level_seed: Option<u64>,
378        #[serde(rename = "k88")]
379        pub level_progress: Option<String>,
380        #[serde(rename = "k89")]
381        pub vfdchk: Option<String>,
382        #[serde(rename = "k90")]
383        pub leaderboard_percentage: Option<u32>,
384}
385impl GDRawLocalLevel {
386    fn into(self) -> Result<GDLocalLevel, BoxERR> {
387        let b64_dec = general_purpose::URL_SAFE.decode(self.inner_level_string.clone())?;
388        let mut decoder = Decoder::new(&b64_dec[..])?;
389        let mut unzipped = Vec::new();
390        std::io::copy(&mut decoder, &mut unzipped)?;
391        let content = &String::from_utf8(unzipped.clone())?;
392        let split = content.split(";").collect::<Vec<&str>>();
393
394        let mut level_start = split[0].to_string();
395        let object_str = split.get(1..split.len() - 1).unwrap();
396        tags_replace(&mut level_start, GD_LEVEL_START_KEY_FORMAT.as_slice(), ",");
397        let ls_map = parser(level_start, ',');
398        let os_map = object_str
399            .iter()
400            .map(|x| parser(x.to_string(), ','))
401            .collect::<Vec<HashMap<String, String>>>();
402        let mut this = GDLocalLevel::default();
403        // println!("{ls_map:?}");
404        this.speed = ls_map.get("speed").unwrap().parse::<u16>().unwrap() + 1;
405        this.mini_mode = bool::from_str(ls_map.get("mini mode").unwrap());
406        this.player_mode2 = bool::from_str(ls_map.get("2-player mode").unwrap());
407        this.mode = GDLevelMode::from(ls_map.get("gamemode").unwrap().parse::<u16>().unwrap());
408        this.platformer_mode = bool::from_str(ls_map.get("platformer mode").unwrap());
409        this.name = self.level_name.clone();
410
411        for objhash in os_map {
412            let mut obj = GDObject::default();
413            obj.props = objhash.clone();
414            if let Some(sgroup) = obj.partial_get_prop::<u16>("single_group") {
415                obj.group_id.push(sgroup);
416            } else if let Some(val) = obj.partial_get_prop::<String>("groups") {
417                let arr = val.split(".");
418                for value in arr {
419                    obj.group_id.push(value.parse::<u16>().unwrap());
420                }
421            }
422            obj.id = obj.get_prop::<u16>("id", "0");
423            obj.x = obj.get_prop::<f32>("x", "0");
424            obj.y = obj.get_prop::<f32>("y", "0");
425            obj.rotation = obj.get_prop::<f32>("rotation", "0");
426            this.objects.push(obj);
427        }
428
429        this.raw = self;
430        Ok(this)
431    }
432}
433
434#[derive(Debug, Default, PartialEq)]
435pub struct GDCCLocalLevels {
436    pub raw_levels: Vec<GDRawLocalLevel>,
437    pub levels: Vec<GDLocalLevel>,
438}
439
440impl GDCCLocalLevels {
441    // Tries to find level of name `level_name` 
442    pub fn get(&self, level_name: &String) -> Option<GDLocalLevel> {
443        for lvl in &self.levels {
444            if &lvl.name == level_name {
445                return Some(lvl.clone());
446            }
447        }
448        return None;
449    }
450    // Tries to find raw level of name `level_name`
451    pub fn get_raw(&self, level_name: &String) -> Option<GDRawLocalLevel> {
452        for lvl in &self.raw_levels {
453            if &lvl.level_name == level_name {
454                return Some(lvl.clone());
455            }
456        }
457        return None;
458    }
459
460    // Reads the buf from self.get_savefile_raw and turns into GDCCLocalLevels
461    pub fn read_raw(buf: &[u8]) -> Result<GDCCLocalLevels, BoxERR> {
462        let mut xorred = xor(&buf.to_vec(), 11);
463        xorred.retain(|b| *b != b"\0"[0]);
464        let len = xorred.len() - 1;
465        if xorred[len] != '=' as u8 && xorred[len - 1] == '=' as u8 {
466            xorred.pop();
467        }
468        let b64_dec = general_purpose::URL_SAFE.decode(&xorred)?;
469
470        let mut decoder = Decoder::new(&b64_dec[..])?;
471        let mut unzipped = Vec::new();
472        std::io::copy(&mut decoder, &mut unzipped)?;
473        let mut content = String::from_utf8(unzipped.clone())?;
474
475        for [f, t] in GD_PLIST_TAGS_FORMAT {
476            content = content
477                .replacen(format!("<{f}>").as_str(), format!("<{t}>").as_str(), 99999)
478                .replacen(
479                    format!("</{f}>").as_str(),
480                    format!("</{t}>").as_str(),
481                    99999,
482                )
483                .replacen(
484                    format!("<{f} />").as_str(),
485                    format!("<{t} />").as_str(),
486                    99999,
487                );
488        }
489
490        let mut this = GDCCLocalLevels::default();
491        let parsed: plist::Value = plist::from_bytes(content.as_bytes())?;
492        if let Some(raw_levels) = parsed
493            .as_dictionary()
494            .and_then(|dict| dict.get("LLM_01"))
495            .and_then(|v| v.as_dictionary())
496        {
497            for key in raw_levels.keys() {
498                if key.get(0..1).unwrap() == "k" {
499                    let data = raw_levels.get(key).unwrap();
500                    let parse_res = plist::from_value::<GDRawLocalLevel>(data);
501                    if let Ok(rllevel) = parse_res {
502                        this.raw_levels.push(rllevel.clone());
503                        this.levels.push(rllevel.into()?);
504                    } else if let Err(e) = parse_res {
505                       let name = data
506                            .as_dictionary()
507                            .and_then(|dict| dict.get("k2").unwrap().as_string())
508                            .unwrap();
509                        println!("{data:?}");
510                        panic!("Couldnt parse level `{name}` {e:?}",);
511                    }
512                }
513            }
514            // println!("Successfuly read {raw_levels:?}");
515        } else {
516            panic!("Error getting \"LLM_01\" (local levels) --> save file might be corrupted")
517        }
518
519        // println!("{this:?}");
520        Ok(this)
521    }
522
523    // Creates new instance of GDCCLocalLevels
524    pub fn new() -> Result<GDCCLocalLevels, BoxERR> {
525        GDCCLocalLevels::read_raw(&GDCCLocalLevels::get_savefile_raw()?)
526    }
527
528    // Reads the savefile and returns its content in Vec<u8>
529    pub fn get_savefile_raw() -> Result<Vec<u8>, BoxERR> {
530        if let Some(base_dirs) = BaseDirs::new() {
531            let read_path = base_dirs.data_local_dir().to_string_lossy().to_string()
532                + "\\GeometryDash\\CCLocalLevels.dat";
533            return Ok(fs::read(read_path)?);
534        }
535        panic!("BaseDirs::new() got None expected Some KnownFolder might be corrupted");
536    }
537}