//! This file contains the necessary structs for interfacing with the level(s) themselves
use std::{
collections::{BTreeSet, HashMap, HashSet},
fmt::Display,
fs::{self, read, write},
io::Cursor,
path::PathBuf,
};
use crate::{core::GDError, gdobj::Group};
use plist::{Dictionary, Value};
use crate::{
core::{b64_decode, b64_encode, get_local_levels_path, proper_plist_tags},
deserialiser::{decode_levels_to_string, decompress},
gdobj::{GDObjPropType, GDObject, lookup::PROPERTY_TABLE},
serialiser::{encrypt_level_str, encrypt_savefile_str, stringify_xml},
};
/// 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
#[derive(Debug, Clone, PartialEq)]
#[allow(missing_docs)]
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
#[derive(Debug, Clone, PartialEq)]
pub struct Levels {
/// All levels in the savefile
pub levels: Vec<Level>,
/// Headers of the level file
pub headers: LevelsFileHeaders,
}
/// This struct contains level data that has not yet been decrypted
#[derive(Clone, Debug, PartialEq)]
pub struct EncryptedLevelData {
/// Raw level data
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, PartialEq)]
pub struct LevelData {
/// Level header string
pub headers: String,
/// Level objects
pub objects: Vec<GDObject>,
}
/// Enum that contains either a raw encrypted level string or decrypted level object
#[derive(Clone, Debug, PartialEq)]
pub enum LevelState {
/// Raw encrypted data
Encrypted(EncryptedLevelData),
/// Parsed, structured data
Decrypted(LevelData),
}
impl Levels {
/// Returns the levels in CCLocalLevels.dat if retrievable
#[inline(always)]
pub fn from_local() -> Result<Self, GDError> {
Levels::from_decrypted(decode_levels_to_string()?)
}
/// Parses raw savefile string into this struct
pub fn from_decrypted(s: String) -> Result<Self, GDError> {
let xmltree = match Value::from_reader_xml(Cursor::new(proper_plist_tags(s).as_bytes())) {
Ok(v) => v.into_dictionary().unwrap(),
Err(e) => return Err(GDError::BadPlist(e)),
};
let levels_dict = xmltree
.get("LLM_01")
.unwrap()
.to_owned()
.into_dictionary()
.unwrap();
let llm_02 = xmltree.get("LLM_02").unwrap().to_owned();
let llm_03 = xmltree.get("LLM_03").unwrap().to_owned();
// 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);
}
/// 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());
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<(), GDError> {
let savefile = get_local_levels_path().unwrap();
let export_str = encrypt_savefile_str(self.export_to_string());
write(savefile, export_str)?;
Ok(())
}
/// Exports this struct as encrypted XML to a given file
pub fn export_to_file(&mut self, file: PathBuf) -> Result<(), GDError> {
let export_str = encrypt_savefile_str(self.export_to_string());
write(file, 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<(), GDError> {
let savefile = get_local_levels_path().unwrap();
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(())
}
}
#[inline(always)]
fn vec_as_str(data: &[u8]) -> String {
String::from_utf8(data.to_vec()).unwrap()
}
/// 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, PartialEq)]
pub struct Level {
/// Title of the level
pub title: Option<String>,
/// Author of the level
pub author: Option<String>,
/// Level description, which is a base64-encoded string
pub description: Option<String>,
/// Level data as a [`LevelState`]
pub data: Option<LevelState>,
/// Song used in the level
pub song: Option<i64>,
/// Level properties
pub properties: HashMap<String, Value>,
}
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: Level::default_properties(),
}
}
/// Generates a hashmap with default level perties
pub 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
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>>()
}
/// Parses a .gmd file to a `Level` object
pub fn from_gmd<T: Into<PathBuf>>(path: T) -> Result<Self, GDError> {
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();
Ok(Level::from_dict(xmltree))
}
/// Exports the level to a .gmd file
pub fn export_to_gmd<T: Into<PathBuf>>(&self, path: T) -> Result<(), GDError> {
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(crate) 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
};
self.data = Some(LevelState::Decrypted(LevelData::parse(raw_data)));
}
/// Returns the decrypted level data as a `LevelData` object if there is data.
pub fn get_decrypted_data(&self) -> Option<LevelData> {
let raw_data = match self.data.clone() {
Some(data) => match data {
LevelState::Encrypted(encrypted) => encrypted.data.clone(),
LevelState::Decrypted(d) => return Some(d), // already decrypted
},
None => return None, // no level data
};
Some(LevelData::parse(raw_data))
}
/// Returns the decrypted level data as a `LevelData` object if there is data.
pub fn get_decrypted_data_ref(&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 => 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.serialise_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);
}
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(_) => (),
};
}
}
}
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 {
/// Serialises this object to a string by serialising each subsequent component.
pub fn serialise_to_string(&self) -> String {
let objstr = self
.objects
.iter()
.map(|obj| obj.serialise_to_string())
.collect::<Vec<String>>()
.join("");
let unencrypted = format!("{};{objstr}", self.headers.clone());
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<Group> {
if self.objects.is_empty() {
return vec![];
}
let mut groups = HashSet::new();
for object in self.objects.iter() {
groups.extend(object.config.groups.iter());
}
let mut arr: Vec<Group> = groups.into_iter().collect();
arr.sort();
arr
}
/// Returns a list of all the groups that do not contain any objects
pub fn get_unused_groups(&self) -> Vec<Group> {
let all: BTreeSet<Group> = (1..10000).map(Group::Regular).collect();
let used: BTreeSet<Group> = self.get_used_groups().into_iter().collect();
all.difference(&used).cloned().collect::<Vec<Group>>()
}
/// Returns a list of all groups used as arguments in triggers
pub fn get_argument_groups(&self) -> Vec<i16> {
if self.objects.is_empty() {
return vec![];
}
// this should really be a const map, but that is impossible in the current version of rust.
// however, the performance cost is negligible since we only generate this list once per search.
let group_properties = PROPERTY_TABLE
.entries()
.filter_map(|(id, info)| {
if info.1 == GDObjPropType::Group {
Some(*id)
} else {
None
}
})
.collect::<Vec<u16>>();
let mut groups = Vec::with_capacity(self.objects.len());
for object in self.objects.iter() {
for p in group_properties.iter() {
if let Some(val) = object.get_property(*p) {
match val {
crate::gdobj::GDValue::Group(g) => groups.push(g),
crate::gdobj::GDValue::GroupList(gs) => groups.extend(gs.to_vec()),
_ => {}
}
}
}
}
groups.sort();
groups.dedup();
groups
}
/// Parse raw level data to this struct
pub fn parse(raw_data: String) -> Self {
// parse level data
let raw_data = decompress(raw_data.as_bytes().to_vec()).unwrap();
let decrypted = std::str::from_utf8(&raw_data[..]).unwrap();
let split = decrypted.split(";").collect::<Vec<&str>>();
let headers = split[0].to_string();
let mut objects = Vec::with_capacity(split.len() - 1);
for object in &split[1..] {
if object.len() > 1 {
objects.push(GDObject::parse_str(object));
}
}
LevelData { headers, objects }
}
}