#![allow(dead_code)]
mod data;
mod util;
use base64::{engine::general_purpose, Engine as _};
use data::*;
use directories::BaseDirs;
use libflate::gzip::Decoder;
use plist;
use serde_derive::Deserialize;
use std::{collections::HashMap, fs};
use util::*;
pub enum GDObjectPropertyType {
Int,
Float,
Bool,
}
pub struct GDObjectProperty {
kind: GDObjectPropertyType,
id: u32,
name: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub enum GDObjectHitbox {
Rect(f32, f32),
Circle(f32),
Triangle(f32, f32),
}
impl Default for GDObjectHitbox {
fn default() -> Self {
GDObjectHitbox::Rect(1.0, 1.0)
}
}
#[derive(Debug, Clone, Default)]
pub struct GDObject {
pub id: u16,
pub x: f32,
pub y: f32,
pub rotation: f32,
pub scalex: f32,
pub scaley: f32,
}
#[derive(Default, Debug, Clone)]
pub struct GDLocalLevel {
pub objects: Vec<GDObject>,
pub mode: GDLevelMode,
pub speed: u16,
pub mini_mode: bool,
pub dual_mode: bool,
pub player_mode2: bool,
pub flip_gravity: bool,
pub platformer_mode: bool,
}
#[derive(Debug, Default, Clone, Copy)]
pub enum GDLevelMode {
#[default]
Cube,
Ship,
Ball,
UFO,
Wave,
Robot,
Swing,
Spider,
}
impl From<u16> for GDLevelMode {
fn from(value: u16) -> Self {
match value {
1 => GDLevelMode::Ship,
2 => GDLevelMode::Ball,
3 => GDLevelMode::UFO,
4 => GDLevelMode::Wave,
5 => GDLevelMode::Robot,
6 => GDLevelMode::Spider,
7 => GDLevelMode::Swing,
_ => GDLevelMode::Cube,
}
}
}
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Debug, Deserialize, Default, Clone)]
pub struct GDRawLocalLevel {
pub attempts: Option<i32>,
pub jumps: Option<i32>,
pub kCEK: Option<i32>,
pub clicks: Option<i32>,
pub levelseed: Option<i32>,
pub levelprogress: Option<String>,
pub normalmodepercentage: Option<i32>,
pub manaorbpercentage: Option<i32>,
pub leaderboardpercentage: Option<i32>,
pub levelname: String,
pub innerlevelstring: String,
pub creator: String,
pub k95: Option<i32>,
pub k101: Option<String>,
pub iseditable: String,
pub leveltype: usize,
pub levelversion: i32,
pub secondsspentediting: Option<i32>,
pub binaryversion: i32,
pub hasbeenmodified: Option<String>,
pub objectcount: Option<u32>,
pub kI1: Option<f32>,
pub kI2: Option<f32>,
pub kI3: Option<f32>,
pub kI6: Option<std::collections::HashMap<String, String>>,
}
impl GDRawLocalLevel {
fn into(self) -> Result<GDLocalLevel, Box<dyn std::error::Error>> {
let b64_dec = general_purpose::URL_SAFE.decode(self.innerlevelstring)?;
let mut decoder = Decoder::new(&b64_dec[..])?;
let mut unzipped = Vec::new();
std::io::copy(&mut decoder, &mut unzipped)?;
let content = &String::from_utf8(unzipped.clone())?;
let split = content.split(";").collect::<Vec<&str>>();
let mut level_start = split[0].to_string();
let object_str = split.get(1..split.len() - 1).unwrap();
tags_replace(&mut level_start, GD_LEVEL_START_KEY_FORMAT.as_slice(), ",");
let ls_map = parser(level_start, ',');
let os_map = object_str
.iter()
.map(|x| parser(x.to_string(), ','))
.collect::<Vec<HashMap<String, String>>>();
let mut this = GDLocalLevel::default();
println!("{ls_map:?}");
this.speed = ls_map.get("speed").unwrap().parse::<u16>().unwrap() + 1;
this.mini_mode = bool_from_str(ls_map.get("mini mode").unwrap());
this.player_mode2 = bool_from_str(ls_map.get("2-player mode").unwrap());
this.mode = GDLevelMode::from(ls_map.get("gamemode").unwrap().parse::<u16>().unwrap());
this.platformer_mode = bool_from_str(ls_map.get("platformer mode").unwrap());
println!("{this:?}");
for objhash in os_map {
let obj_id = objhash.get("1").unwrap();
let mut obj = GDObject::default();
obj.id = obj_id.parse::<u16>()?;
obj.x = objhash.get("2").unwrap().parse::<f32>()? / 30.0;
obj.y = objhash.get("3").unwrap().parse::<f32>()? / 30.0;
obj.rotation = objhash
.get("6")
.unwrap_or(&"0".to_string())
.parse::<f32>()?;
this.objects.push(obj);
}
Ok(this)
}
}
#[derive(Debug, Default)]
pub struct GDCCLocalLevels {
pub raw_levels: Vec<GDRawLocalLevel>,
pub levels: Vec<GDLocalLevel>,
}
impl GDCCLocalLevels {
pub fn read_raw(buf: &[u8]) -> Result<GDCCLocalLevels, Box<dyn std::error::Error>> {
let mut xorred = xor(&buf.to_vec(), 11);
xorred.retain(|b| *b != b"\0"[0]);
let len = xorred.len() - 1;
if xorred[len] != '=' as u8 && xorred[len - 1] == '=' as u8 {
xorred.pop();
}
let b64_dec = general_purpose::URL_SAFE.decode(&xorred)?;
let mut decoder = Decoder::new(&b64_dec[..])?;
let mut unzipped = Vec::new();
std::io::copy(&mut decoder, &mut unzipped)?;
let mut content = String::from_utf8(unzipped.clone())?;
for [f, t] in GD_LEVEL_KEY_FORMAT {
content = content.replacen(
format!("<k>{f}</k>").as_str(),
format!("<k>{t}</k>").as_str(),
99999,
);
}
for [f, t] in GD_PLIST_TAGS_FORMAT {
content = content
.replacen(format!("<{f}>").as_str(), format!("<{t}>").as_str(), 99999)
.replacen(
format!("</{f}>").as_str(),
format!("</{t}>").as_str(),
99999,
)
.replacen(
format!("<{f} />").as_str(),
format!("<{t} />").as_str(),
99999,
);
}
let mut this = GDCCLocalLevels::default();
let parsed: plist::Value = plist::from_bytes(content.as_bytes())?;
if let Some(raw_levels) = parsed
.as_dictionary()
.and_then(|dict| dict.get("LLM_01"))
.and_then(|v| v.as_dictionary())
{
for key in raw_levels.keys() {
if key.get(0..1).unwrap() == "k" {
let data = raw_levels.get(key).unwrap();
let parse_res = plist::from_value::<GDRawLocalLevel>(data);
if let Ok(rllevel) = parse_res {
this.raw_levels.push(rllevel.clone());
this.levels.push(rllevel.clone().into()?);
} else if let Err(e) = parse_res {
let creator = data
.as_dictionary()
.and_then(|dict| dict.get("creator").unwrap().as_string())
.unwrap();
let name = data
.as_dictionary()
.and_then(|dict| dict.get("levelname").unwrap().as_string())
.unwrap();
eprintln!("Couldnt parse level `{name}` by `{creator}` {e:?}",);
}
}
}
} else {
panic!("Error getting \"LLM_01\" (local levels) --> save file might be corrupted")
}
Ok(this)
}
pub fn get_savefile_raw() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
if let Some(base_dirs) = BaseDirs::new() {
let read_path = base_dirs.data_local_dir().to_string_lossy().to_string()
+ "\\GeometryDash\\CCLocalLevels.dat";
return Ok(fs::read(read_path)?);
}
panic!("BaseDirs::new() got None expected Some KnownFolder might be corrupted");
}
}
#[cfg(tests)]
mod tests {}