gdlib 0.2.2-patch

Rust library for editing Geometry Dash savefiles
Documentation
//! This file contains the necessary structs for interfacing with the level(s) themselves
use std::{collections::{BTreeSet, HashMap, HashSet}, error::Error, fmt::Display, fs::{self, read, write}, io::Cursor, path::PathBuf};

use plist::{Dictionary, Value};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};

use crate::{deserialiser::{decode_levels_to_string, decompress}, gdobj::GDObject, serialiser::{encrypt_level_str, encrypt_savefile_str, stringify_xml}, utils::{b64_decode, b64_encode, get_local_levels_path, proper_plist_tags, vec_as_str}};

/// This is the default level header 
pub const DEFAULT_LEVEL_HEADERS: &str = "kS38,1_40_2_125_3_255_11_255_12_255_13_255_4_-1_6_1000_7_1_15_1_18_0_8_1|1_0_2_102_3_255_11_255_12_255_13_255_4_-1_6_1001_7_1_15_1_18_0_8_1|1_0_2_102_3_255_11_255_12_255_13_255_4_-1_6_1009_7_1_15_1_18_0_8_1|1_255_2_255_3_255_11_255_12_255_13_255_4_-1_6_1002_5_1_7_1_15_1_18_0_8_1|1_40_2_125_3_255_11_255_12_255_13_255_4_-1_6_1013_7_1_15_1_18_0_8_1|1_40_2_125_3_255_11_255_12_255_13_255_4_-1_6_1014_7_1_15_1_18_0_8_1|1_0_2_125_3_255_11_255_12_255_13_255_4_-1_6_1005_5_1_7_1_15_1_18_0_8_1|1_0_2_200_3_255_11_255_12_255_13_255_4_-1_6_1006_5_1_7_1_15_1_18_0_8_1|,kA13,0,kA15,0,kA16,0,kA14,,kA6,0,kA7,0,kA25,0,kA17,0,kA18,0,kS39,0,kA2,0,kA3,0,kA8,0,kA4,0,kA9,0,kA10,0,kA22,0,kA23,0,kA24,0,kA27,1,kA40,1,kA41,1,kA42,1,kA28,0,kA29,0,kA31,1,kA32,1,kA36,0,kA43,0,kA44,0,kA45,1,kA46,0,kA33,1,kA34,1,kA35,0,kA37,1,kA38,1,kA39,1,kA19,0,kA26,0,kA20,0,kA21,0,kA11,0;";

/// This struct contains other values found in the levels savefile that aren't of any particular use
pub struct LevelsFileHeaders {
    pub llm02: Value,
    pub llm03: Value
}

/// This struct contains all the levels of the savefile
/// # Fields:
/// * `levels`: The levels. Ones at the beginning are the most recently created.
/// * `headers`: other information necessary for re-encoding
pub struct Levels {
    pub levels: Vec<Level>,
    pub headers: LevelsFileHeaders
}

/// This struct contains level data that has not yet been decrypted 
#[derive(Clone, Debug)]
pub struct EncryptedLevelData {
    pub data: String
}

/// This struct contains the objects of a level and its headers
/// # Fields:
/// * `objects`: Array of objects
/// * `headers`: Other important information about the level 
#[derive(Clone, Debug)]
pub struct LevelData {
    pub headers: String,
    pub objects: Vec<GDObject>
}

/// Enum that contains either a encrypted level string or decrypted level object 
#[derive(Clone, Debug)]
pub enum LevelState {
    Encrypted(EncryptedLevelData),
    Decrypted(LevelData)
}

/// This struct contains level-specific information
/// # Fields:
/// * `title`: Title of the level
/// * `author`: Author of the level
/// * `description`: Author of the description
/// * `data`: Encrypted or decrypted level data
/// * `properties`: Other unspecified properties of this level
#[derive(Debug, Clone)]
pub struct Level {
    pub title: Option<String>,
    pub author: Option<String>,
    pub description: Option<String>,
    pub data: Option<LevelState>,
    pub song: Option<i64>,
    pub properties: HashMap<String, Value>
}

impl Levels {
    /// Returns the levels in CCLocalLevels.dat if retrievable
    pub fn from_local() -> Result<Self, Box<dyn Error>> {
        match decode_levels_to_string() {
            Ok(v) => Levels::from_decrypted(v),
            Err(e) => Err(e)
        }
    }

    /// Parses raw savefile string into this struct
    pub fn from_decrypted(s: String) -> Result<Self, Box<dyn Error>> {
        let xmltree = Value::from_reader_xml(
            Cursor::new(proper_plist_tags(s).as_bytes())
        )?.as_dictionary_mut().unwrap().clone();
        

        let levels_dict = xmltree.get("LLM_01").unwrap().clone().as_dictionary().unwrap().clone();
        let llm_02 = xmltree.get("LLM_02").unwrap().clone();
        let llm_03 = xmltree.get("LLM_03").unwrap().clone();

        // these are stored as "k_0": <level>, "k_1": <level>, etc. in the savefile, 
        // the vec prserves that order.
        let levels_parsed = levels_dict.iter().filter_map(|(k, v)| {
            match k.as_str() {
                "_isArr" => None,
                _ => Some(Level::from_dict(v.as_dictionary().unwrap().clone()))
            } 
        }).collect::<Vec<Level>>();


        let levels = Levels { 
            levels: levels_parsed,  // one of these might be for lists. will consider that later
            headers: LevelsFileHeaders { llm02: llm_02, llm03: llm_03 }
        };

        Ok(levels)
    }

    /// Adds a level to the beginning of `self.levels`
    pub fn add_level(&mut self, level: Level) {
        self.levels.insert(0, level);
    }

    fn default_properties() -> HashMap<String, Value> {
        let mut ki6_dict = Dictionary::new();
        for i in 0..15 {
            ki6_dict.insert(format!("{i}"), Value::from("0"));
        }

        // genuienly have no clue wht any of these are
        let properties = vec![
            ("kCEK", Value::from(4)),
            ("k18", Value::from(1)),
            ("k101", Value::from("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")),
            ("k11", Value::from(4598)),
            ("k13", Value::from(true)),
            ("k21", Value::from(2)),
            ("k16", Value::from(1)),
            ("k27", Value::from(4598)),
            ("k50", Value::from(45)),
            ("k47", Value::from(true)),
            ("kI1", Value::from(100.0)),
            ("kI2", Value::from(100.0)),
            ("kI3", Value::from(1.0)),
            ("kI6", Value::from(ki6_dict))
        ].into_iter().map(|(k, v)| (k.to_string(), v)).collect::<HashMap<String, Value>>();

        return properties
    }

    /// Exports this struct as XML to a String
    pub fn export_to_string(&mut self) -> String {
        let mut dict = Dictionary::new();

        let mut levels_dict = Dictionary::new();
        levels_dict.insert("_isArr".to_string(), Value::from(true));
        for (idx, level) in self.levels.iter().enumerate() {
            levels_dict.insert(format!("k_{idx}"), Value::from(level.to_dict()));
        }

        dict.insert("LLM_01".to_string(), plist::Value::Dictionary(levels_dict));
        dict.insert("LLM_02".to_string(), self.headers.llm02.clone());
        dict.insert("LLM_03".to_string(), self.headers.llm03.clone());
        
        return format!("<?xml version=\"1.0\"?><plist version=\"1.0\" gjver=\"2.0\">{}</plist>", stringify_xml(&dict, true));
    }

    /// Exports this struct as encrypted XML to CCLocalLevels.dat
    pub fn export_to_savefile(&mut self) -> Result<(), Box<dyn Error>>{
        let savefile = get_local_levels_path()?;
        let export_str = encrypt_savefile_str(self.export_to_string());
        write(savefile, export_str)?;
        Ok(())
    }

    /// Exports this struct as encrypted XML to CCLocalLevels.dat and creates a backup, CCLocalLevels.dat.bak
    pub fn export_to_savefile_with_backup(&mut self) -> Result<(), Box<dyn Error>>{
        let savefile = get_local_levels_path()?;
        let backup_path = format!("{}.bak", savefile.to_string_lossy());
        write(backup_path, read(&savefile)?)?;

        let export_str = encrypt_savefile_str(self.export_to_string());
        write(savefile, export_str)?;
        Ok(())
    }
}

impl Level {
    /// Default constructor
    /// # Arguments:
    /// * `title`: Title of the level
    /// * `author`: Who made the level
    /// * `desciption`: (Optional) description of the level
    /// * `song`: (Optional) Song of the level, defaults to stereo madness
    pub fn new<T: Into<String>>(title: T, author: T, description: Option<T>, song: Option<i64>) -> Self {
        Level { 
            title: Some(title.into()), 
            author: Some(author.into()), 
            description: description.map(|desc| b64_encode(desc.into().as_bytes().to_vec())), 
            data: Some(LevelState::Decrypted(LevelData { headers: DEFAULT_LEVEL_HEADERS.to_string(), objects: vec![] })), 
            song, 
            properties: Levels::default_properties()
        }
    }

    /// Parses a .gmd file to a `Level` object
    pub fn from_gmd<T: Into<PathBuf>>(path: T) -> Result<Self, Box<dyn Error>> {
        let file = proper_plist_tags(vec_as_str(&fs::read(path.into())?));
        let xmltree = Value::from_reader_xml(
            Cursor::new(file.as_bytes())
        )?.as_dictionary_mut().unwrap().clone();

        return Ok(Level::from_dict(xmltree));
    }

    pub fn export_to_gmd<T: Into<PathBuf>>(&self, path: T) -> Result<(), Box<dyn Error>> {
        let export_str =  format!(
            "<?xml version=\"1.0\"?><plist version=\"1.0\" gjver=\"2.0\">{}</plist>", 
            stringify_xml(&self.to_dict(), true)
        );

        fs::write(path.into(), export_str)?;
        Ok(())
    }

    /// Parses a `plist::Dictionary` into a Level object
    pub fn from_dict(d: Dictionary) -> Self {
        // level data kv pairs
        // k2: level name
        // k3: description
        // k4: level str (encrypted) 
        // k5: author
        // k45: song

        let mut song = None;
        let mut author = None;
        let mut description = None;
        let mut title = None;
        let mut data = None;

        // residual properties
        let mut properties: HashMap<String, Value> = HashMap::new();

        for (property, value) in d.into_iter() {
            match property.as_str() {
                "k2" => title = Some(value.as_string().unwrap().to_owned()),
                "k3" => description = Some(value.as_string().unwrap().to_owned()),
                "k4" => data = Some(value.as_string().unwrap().to_owned()),
                "k5" => author = Some(value.as_string().unwrap().to_owned()),
                "k45" => song = Some(value.as_signed_integer().unwrap()), 
                _ => {
                    properties.insert(property, value);
                }
            }
        }

        let mut level_data: Option<LevelState> = None;
        if let Some(d) = data {
            level_data = Some(LevelState::Encrypted(EncryptedLevelData { data: d }))
        }
       
        Level {
            title,
            author,
            description,
            data: level_data,
            song,
            properties
        }
    }

    /// Returns the level data as unencrypted. 
    /// Level data is left unencrypted when parsing the level as it is slow.
    pub fn decrypt_level_data(&mut self) {
        let raw_data = match &self.data {
            Some(data) => match data {
                LevelState::Encrypted(encrypted) => {
                    encrypted.data.clone()
                },
                LevelState::Decrypted(_) => return // already decrypted
            },
            None => return // no level data
        };

        // parse level data
        let decrypted = vec_as_str(&decompress(raw_data.as_bytes().to_vec()).unwrap());

        let split = decrypted.split(";").collect::<Vec<&str>>();

        let headers = split.first().unwrap().to_owned().to_owned();
        let objects = split[1..].par_iter().filter_map(|objstr| {
            match objstr.len() > 1 {
                true => Some(GDObject::parse_str(*objstr)),
                false => None
            }
        }).collect::<Vec<GDObject>>();
        self.data = Some(LevelState::Decrypted(LevelData { headers, objects }));
    }

    /// Returns the decrypted level data as a `LevelData` object if there is data. 
    pub fn get_decrypted_data(&mut self) -> Option<&mut LevelData> {
        self.decrypt_level_data();
        match &mut self.data {
            Some(d) => {
                if let LevelState::Decrypted(data) = d {
                    Some(data)
                } else {
                    None
                }
            },
            None => return None
        }
    }

    /// Returns this object as a `plist::Dictionary`
    pub fn to_dict(&self) -> Dictionary {
        let mut properties = Dictionary::new();
        if let Some(v) = self.title.clone() {
            properties.insert("k2".to_string(), Value::from(v));
        };
        if let Some(v) = self.description.clone() {
            properties.insert("k3".to_string(), Value::from(v));
        };
        if let Some(v) = self.data.clone() {
            let str = match v {
                LevelState::Decrypted(data) => data.to_string(),
                LevelState::Encrypted(data) => data.data
            };
            properties.insert("k4".to_string(), Value::from(str));
        };
        if let Some(v) = self.author.clone() {
            properties.insert("k5".to_string(), Value::from(v));
        };
        if let Some(v) = self.song {
            properties.insert("k45".to_string(), Value::from(v));
        };

        for (p, val) in self.properties.clone().into_iter() {
            properties.insert(p, val);
        }

        return properties;
    }

    /// Adds a GDObject to `self.objects`
    pub fn add_object(&mut self, object: GDObject) {
        if let Some(data) = &mut self.data {
            match data {
                LevelState::Decrypted(state) => {
                    state.objects.push(object);
                },
                LevelState::Encrypted(_) => return
            };
        }
    }
}

impl Display for Level {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let info_str = match &self.data {
            Some(d) => match d {
                LevelState::Encrypted(enc) => {
                    &format!("{} Bytes", enc.data.len())
                },
                LevelState::Decrypted(dec) => {
                    &format!("{} Objects", dec.objects.len())
                }
            },
            None => "Empty"
        };

        write!(
            f, 
            "\"{}\" ({}) by {} using song {}; {info_str}", 
            self.title.clone().unwrap_or("<No title>".to_string()), 
            vec_as_str(&b64_decode(
                self.description.clone().unwrap_or("PE5vIGRlc2NyaXB0aW9uPg==".to_string())
            .as_bytes().to_vec())),
            self.author.clone().unwrap_or("<Unknown author>".to_string()),
            self.song.unwrap_or(0)
        )
    }
}

impl LevelData {
    pub fn to_string(&self) -> String {
        let objstr = self.objects.iter()
            .map(|obj| obj.to_string()).collect::<Vec<String>>().join("");
        let unencrypted = format!("{};{objstr}", self.headers.clone());
        return vec_as_str(&encrypt_level_str(unencrypted))
    }

    /// Returns a list of all the groups that contain at least one object
    pub fn get_used_groups(&self) -> Vec<u16> {
        if self.objects.len() == 0 {
            return vec![];
        }

        let mut groups = HashSet::new();
        
        for object in self.objects.iter() {
            groups.extend(object.config.groups.iter());
        };
        let mut arr: Vec<u16> = groups.into_iter().collect();
        arr.sort();
        return arr
    }

    /// Returns a list of all the groups that do not contain any objects
    pub fn get_unused_groups(&self) -> Vec<u16> {
        let all: BTreeSet<u16> = (0..10000).collect();
        let used: BTreeSet<u16> = self.get_used_groups().into_iter().collect();

        all.difference(&used).cloned().collect::<Vec<u16>>()
    }
}