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 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 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 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 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 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 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#[derive(Debug, Clone, Default, PartialEq)]
114pub struct GDLocalLevel {
115 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 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())); props.push(format!("kA3,{}", self.mini_mode.as_gd_string()));
152 props.push(format!("kA4,{}", self.speed.to_string())); 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, };
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 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 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 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 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 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 } else {
516 panic!("Error getting \"LLM_01\" (local levels) --> save file might be corrupted")
517 }
518
519 Ok(this)
521 }
522
523 pub fn new() -> Result<GDCCLocalLevels, BoxERR> {
525 GDCCLocalLevels::read_raw(&GDCCLocalLevels::get_savefile_raw()?)
526 }
527
528 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}