use base64::engine::{general_purpose::URL_SAFE, Engine}; use eframe::egui::TextFormat; use eframe::epaint::text::LayoutJob; use flate2::read::GzDecoder; use gd_plist::Value; use reqwest::blocking as req; use std::fs::File; use std::io::{Cursor, Read}; use std::num::ParseIntError; use std::path::PathBuf; use thiserror::Error; struct User { name: String, id: Option, } pub enum Song { Official { id: i32 /*k8*/ }, Newgrounds { id: i32 /*k45*/ }, Unknown, } #[derive(Clone)] pub struct SongResponse([Option; 9]); struct Level { outer: OuterLevel, song: Song, } #[derive(Debug)] pub struct OuterLevel { name: String, // k2 revision: Option, // k46 } #[derive(Debug, Error, Clone)] pub enum SongRequestError { #[error("Request failed")] ConnectionFailure, #[error("Index is not an int?????")] ParseFailure(#[from] ParseIntError), #[error("Not a Newgrounds song")] NotNewgrounds, } impl From for SongRequestError { fn from(_: reqwest::Error) -> Self {Self::ConnectionFailure} } impl Song { pub fn get_response(&self) -> Result { match self { Self::Newgrounds { id } => { let mut out = SongResponse(Default::default()); req::Client::new() .post("http://boomlings.com/database/getGJSongInfo.php") .body(format!( r#" {{ "secret": "Wmfd2893gb7", "songID": {} }} "#, id )) .send()? .text()? .split("~|~") .array_chunks() .try_for_each(|[id, value]| -> Result<(), SongRequestError> { out.0[id.parse::()? - 1] = Some(value.into()); Ok(()) }) .map(|_| out) } _ => Err(SongRequestError::NotNewgrounds), } } } impl SongResponse { pub fn id(&self) -> Option { self.0[0].and_then(|s| s.parse().ok()) } pub fn name(&self) -> Option { self.0[1] } pub fn artist_id(&self) -> Option { self.0[2].and_then(|s| s.parse().ok()) } pub fn artist_name(&self) -> Option { self.0[3] } pub fn size(&self) -> Option { self.0[4].and_then(|s| s.parse().ok()) } pub fn video_id(&self) -> Option { self.0[5] } pub fn youtube_url(&self) -> Option { self.0[6] } pub fn song_priority(&self) -> Option { self.0[8].and_then(|s| s.parse().ok()) } pub fn download_link(&self) -> Option { self.0[9].and_then(|url| urlencoding::decode(&url).ok().map(|url| url.into_owned())) } } impl OuterLevel { pub fn load_all() -> Vec { let plist = get_local_level_plist(); let levels: Vec = plist .as_dictionary() .and_then(|dict| dict.get("LLM_01")) .unwrap() .as_dictionary() .unwrap() .into_iter() .filter(|(key, _)| key.as_str() != "_isArr") .map(|(_, val)| { let mut builder = LevelBuilder::new(); let props = val.as_dictionary().unwrap(); if let Some(title) = props.get("k2") { builder.with_name(title.as_string().unwrap().into()); } if let Some(rev) = props.get("k46") { builder.with_revision(rev.as_signed_integer().unwrap().into()); } builder.build_outer_level().unwrap() }) .collect(); levels } pub fn display_name(&self) -> LayoutJob { match self.revision { Some(rev) => { let mut job = LayoutJob::default(); job.append(&format!("{} ", self.name), 0f32, TextFormat::default()); job.append( &format!("(rev {})", rev), 0f32, TextFormat { italics: true, ..Default::default() }, ); job } None => { let mut job = LayoutJob::default(); job.append(&self.name, 0f32, TextFormat::default()); job } } } } pub fn gd_path() -> PathBuf { let mut path_buf = home::home_dir().unwrap(); #[cfg(unix)] path_buf.extend( [ ".local", "share", "Steam", "steamapps", "compatdata", "322170", "pfx", "drive_c", "users", "steamuser", "AppData", "Local", "GeometryDash", ] .iter(), ); #[cfg(windows)] path_buf.extend(["AppData", "Local", "GeometryDash"].iter()); path_buf } struct LevelBuilder { name: Option, song: Option, revision: Option, } impl Default for LevelBuilder { fn default() -> Self { Self { name: None, song: None, revision: None, } } } impl LevelBuilder { fn new() -> Self { Self::default() } fn with_name(&mut self, name: String) { self.name = Some(name); } fn with_song(&mut self, song: Song) { self.song = Some(song); } fn with_revision(&mut self, revision: i64) { self.revision = Some(revision); } fn build_level(self) -> Option { match self { Self { name: Some(name), song: Some(song), revision, } => Some(Level { song, outer: OuterLevel { name, revision }, }), _ => None, } } fn build_outer_level(self) -> Option { match self { Self { name: Some(name), revision, .. } => Some(OuterLevel { name, revision }), _ => None, } } } fn get_local_level_plist() -> Value { let raw_save_data = { let mut save_file = File::open(gd_path().join("CCLocalLevels.dat")).expect("No save file found!"); let mut sd = Vec::new(); save_file.read_to_end(&mut sd).unwrap(); sd }; let data_post_xor: Vec = raw_save_data .iter() .map(|b| b ^ 11) .filter(|&b| b != 0u8) .collect(); let data_post_b64 = URL_SAFE.decode(data_post_xor).unwrap(); let mut decoder = GzDecoder::<&[u8]>::new(data_post_b64.as_ref()); let mut plist = String::new(); if let Err(_) = decoder.read_to_string(&mut plist) { println!("Warning: Game save likely corrupted (gzip decode failed)"); } Value::from_reader(Cursor::new(plist)).unwrap() }