use std::{array, collections::HashMap, hash::BuildHasherDefault, io::Cursor};
use nohash_hasher::NoHashHasher;
use plist::{Dictionary, Value};
use crate::{
cclocallevels::gdlevel::{GDLevel, PLIST_HEADER},
core::{GDError, get_ccgamemanager_path, io::decrypt_file, proper_plist_tags},
repr_t,
};
type IntMap<V> = HashMap<i32, V, BuildHasherDefault<NoHashHasher<i32>>>;
#[derive(Debug, Default, Clone)]
pub struct CCGameManager {
pub player_info: GDPlayerInfo,
pub stats: GDStatistics,
pub config: GDConfig,
pub account: GDAccount,
pub keybinds: (Dictionary, Dictionary),
pub temp_state: GDCurrentValues,
pub other_properties: HashMap<String, Value>,
}
impl CCGameManager {
pub fn from_local() -> Result<Self, GDError> {
let path = get_ccgamemanager_path().ok_or(GDError::MissingSavefile)?;
Self::from_raw_string(decrypt_file(path).unwrap())
}
pub fn from_raw_string(s: String) -> Result<Self, GDError> {
if !s.starts_with(PLIST_HEADER) {
return Err(GDError::CorruptedSavefile("Savefile header does not match the expected header. This may be due to a corrupted savefile or a savefile from a previous version of GD.".into()));
};
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 mut this = Self::default();
if let None = this.parse_dict(xmltree) {
return Err(GDError::CorruptedSavefile(
"Unable to parse corrupted savefile.".into(),
));
}
Ok(this)
}
fn parse_dict(&mut self, dict: Dictionary) -> Option<()> {
let mut d = dict;
parse_values(
&mut d,
&mut [
("playerUDID", &mut self.player_info.udid),
("playerName", &mut self.player_info.username),
("GJA_001", &mut self.account.username),
],
|v| v.as_string().map(|v| v.to_string()),
)?;
parse_values(
&mut d,
&mut [
("GJA_002", &mut self.account.plaintext_password),
("GJA_004", &mut self.account.session_id),
("GJA_005", &mut self.account.hashed_password),
],
|v| v.as_string().map(|v| Some(v.to_string())),
)?;
parse_values(
&mut d,
&mut [
("playerUserID", &mut self.player_info.user_id),
("playerFrame", &mut self.player_info.icon_cube),
("playerShip", &mut self.player_info.icon_ship),
("playerBall", &mut self.player_info.icon_ball),
("playerBird", &mut self.player_info.icon_ufo),
("playerDart", &mut self.player_info.icon_wave),
("playerRobot", &mut self.player_info.icon_robot),
("playerSpider", &mut self.player_info.icon_spider),
("playerSwing", &mut self.player_info.icon_swing),
("playerColor", &mut self.player_info.player_col1),
("playerColor2", &mut self.player_info.player_col2),
("playerColor3", &mut self.player_info.player_col_glow),
("playerStreak", &mut self.player_info.icon_streak),
("playerShipStreak", &mut self.player_info.ship_streak),
("playerDeathEffect", &mut self.player_info.death_effect),
("playerJetpack", &mut self.player_info.icon_jetpack),
("playerIconType", &mut self.player_info.icon_type),
("bootups", &mut self.stats.bootups),
("binaryVersion", &mut self.config.binary_version),
("timeOffset", &mut self.config.music_offset),
("GJA_003", &mut self.account.account_id),
("GS_20", &mut self.stats.demon_keys),
("GLM_11", &mut self.temp_state.current_daily_level),
("GLM_17", &mut self.temp_state.current_weekly_level),
],
|v| v.as_signed_integer().map(|v| v as i32),
)?;
parse_values(
&mut d,
&mut [
("playerGlow", &mut self.player_info.using_glow),
("hasRP", &mut self.player_info.is_moderator),
("showSongMarkers", &mut self.config.show_song_markers),
("showProgressBar", &mut self.config.show_progress_bar),
("clickedGarage", &mut self.config.has_clicked_garage),
("clickedEditor", &mut self.config.has_clicked_editor),
("clickedPractice", &mut self.config.has_clicked_practice),
("showedEditorGuide", &mut self.config.seen_editor_guide),
("showedLowDetailDialog", &mut self.config.seen_ldm_dialog),
(
"showedRateStarDialog",
&mut self.config.seen_rate_star_dialog,
),
("hasRatedGame", &mut self.config.has_rated_game),
],
|v| v.as_boolean(),
)?;
parse_values(
&mut d,
&mut [
("bgVolume", &mut self.config.bgm_volume),
("sfxVolume", &mut self.config.sfx_volume),
("practicePosX", &mut self.config.practice_ui_pos.0),
("practicePosY", &mut self.config.practice_ui_pos.1),
("practiceOpacity", &mut self.config.practice_ui_opacity),
("customFPSTarget", &mut self.config.fps_target),
],
|v| v.as_real().map(|v| v as f32),
)?;
for i in 0..5 {
parse_val(&mut d, &format!("dpad0{}", i + 1), |v| {
self.config.dpads[i] = GDPlatformerUI::from_str(v.as_string().unwrap());
Some(())
})?;
}
self.config.dpad_layout = d
.get("dpad_layout")
.map(|v| GDPlatformerUI::from_str(v.as_string().unwrap()));
parse_val(&mut d, "resolution", |v| {
self.config.resolution = Resolution::try_from(v.as_signed_integer()? as i32).ok()?;
Some(())
})?;
parse_val(&mut d, "texQuality", |v| {
self.config.text_quality =
TextureQuality::try_from(v.as_signed_integer()? as i32).ok()?;
Some(())
})?;
parse_val(&mut d, "KBM_001", |v| {
self.keybinds.0 = v.as_dictionary()?.clone();
Some(())
})?;
parse_val(&mut d, "KBM_002", |v| {
self.keybinds.1 = v.as_dictionary()?.clone();
Some(())
})?;
parse_values(
&mut d,
&mut [
("GLM_01", &mut self.stats.official_level_progresses),
("GLM_03", &mut self.stats.online_levels_played),
("GLM_16", &mut self.stats.gauntlet_levels_played),
],
|v| parse_level_dict(&v),
)?;
parse_values(
&mut d,
&mut [
("GLM_06", &mut self.account.following_creators),
("GLM_07", &mut self.temp_state.last_played_levels),
("GLM_13", &mut self.stats.submitted_ratings),
("GLM_14", &mut self.account.reported_levels),
("GLM_15", &mut self.stats.submitted_ratings_demons),
],
|v| {
Some(
v.as_dictionary()?
.iter()
.map(|(k, _)| k.parse::<i32>().unwrap())
.collect::<Vec<_>>(),
)
},
)?;
parse_values(
&mut d,
&mut [
("GLM_18", &mut self.config.saved_levels_foldernames),
("GLM_19", &mut self.config.local_levels_foldernames),
],
parse_foldernames,
)?;
parse_val(
&mut d,
"GLM_12",
|v| {
self.config.glm12_unknown = v
.as_dictionary()?
.iter()
.map(|(k, _)| {
let mut split = k.split("_").into_iter();
let _ = split.next(); let keys =
array::from_fn(|_| split.next().unwrap().parse::<i32>().unwrap());
keys
})
.collect::<Vec<_>>();
Some(())
},
)?;
parse_val(&mut d, "GLM_10", |v| {
self.stats.completed_dailies = v
.as_dictionary()?
.iter()
.map(|(k, v)| {
(
k.parse::<i32>().unwrap(),
GDLevel::from_dict(v.as_dictionary().unwrap()).unwrap(),
)
})
.collect();
Some(())
})?;
self.other_properties = d.into_iter().collect();
Some(())
}
}
#[must_use]
fn parse_values<F: Fn(Value) -> Option<R>, R>(
d: &mut Dictionary,
fields: &mut [(&str, &mut R)],
parser: F,
) -> Option<()> {
for (k, f) in fields {
if let Some(v) = d.remove(k) {
**f = parser(v)?;
}
}
Some(())
}
#[must_use]
fn parse_val<F: FnMut(Value) -> Option<()>>(
d: &mut Dictionary,
key: &str,
mut parser: F,
) -> Option<()> {
if let Some(v) = d.remove(key) {
parser(v)
} else {
Some(())
}
}
fn parse_level_dict(v: &Value) -> Option<Vec<GDLevel>> {
v.as_dictionary()?
.iter()
.map(|(_id, level_dict)| GDLevel::from_dict(level_dict.as_dictionary().unwrap()))
.collect::<Option<Vec<GDLevel>>>()
}
fn parse_foldernames(v: Value) -> Option<Vec<(i32, String)>> {
if v.as_dictionary()?.is_empty() {
return Some(vec![]);
}
let mut raw_folders = v
.as_dictionary()?
.iter()
.map(|(idx, name)| {
(
idx.parse::<i32>().unwrap(),
name.as_string().unwrap().to_string(),
)
})
.collect::<Vec<_>>();
raw_folders.sort_by(|(a, _), (b, _)| a.cmp(b));
Some(raw_folders)
}
#[derive(Debug, Default, Clone)]
#[allow(missing_docs)]
pub struct GDPlayerInfo {
pub username: String,
pub udid: String,
pub user_id: i32,
pub icon_cube: i32,
pub icon_ship: i32,
pub icon_ball: i32,
pub icon_ufo: i32,
pub icon_wave: i32,
pub icon_robot: i32,
pub icon_spider: i32,
pub icon_swing: i32,
pub player_col1: i32,
pub player_col2: i32,
pub player_col_glow: i32,
pub icon_streak: i32,
pub ship_streak: i32,
pub death_effect: i32,
pub icon_jetpack: i32,
pub icon_type: i32,
pub using_glow: bool,
pub is_moderator: bool,
}
#[derive(Debug, Default, Clone)]
#[allow(missing_docs)]
pub struct GDStatistics {
pub bootups: i32,
pub official_level_progresses: Vec<GDLevel>,
pub online_levels_played: Vec<GDLevel>,
pub demon_keys: i32,
pub submitted_ratings: Vec<i32>,
pub submitted_ratings_demons: Vec<i32>,
pub gauntlet_levels_played: Vec<GDLevel>,
pub completed_dailies: IntMap<GDLevel>,
}
#[derive(Debug, Default, Clone)]
#[allow(missing_docs)]
pub struct GDConfig {
pub bgm_volume: f32,
pub sfx_volume: f32,
pub text_quality: TextureQuality,
pub resolution: Resolution,
pub show_song_markers: bool,
pub show_progress_bar: bool,
pub has_clicked_garage: bool,
pub has_clicked_editor: bool,
pub has_clicked_practice: bool,
pub seen_editor_guide: bool,
pub seen_ldm_dialog: bool,
pub seen_rate_star_dialog: bool,
pub has_rated_game: bool,
pub binary_version: i32,
pub practice_ui_pos: (f32, f32),
pub practice_ui_opacity: f32,
pub fps_target: f32,
pub music_offset: i32,
pub dpads: [GDPlatformerUI; 5],
pub dpad_layout: Option<GDPlatformerUI>,
pub saved_levels_foldernames: Vec<(i32, String)>,
pub local_levels_foldernames: Vec<(i32, String)>,
pub glm12_unknown: Vec<[i32; 4]>,
}
repr_t!(
strict TextureQuality: i32 {
Auto = 0,
Low = 1,
Medium = 2,
High = 3,
} default Auto
);
repr_t!(
strict Resolution: i32 {
R640x480 = 1, R720x480 = 2, R720x576 = 3, R800x600 = 4, R1024x768 = 5, R1152x864 = 6, R1176x664 = 7, R1280x720 = 8, R1280x768 = 9, R1280x800 = 10, R1280x960 = 11, R1280x1024 = 12, R1360x768 = 13, R1366x768 = 14, R1440x900 = 15, R1600x900 = 16, R1600x1024 = 17, R1600x1200 = 18, R1680x1050 = 19, R1768x992 = 20, R1920x1080 = 21, R1920x1200 = 22, R1920x1440 = 23, R2048x1536 = 24, R2560x1440 = 25, R2560x1600 = 26, R3840x2160 = 27, } default R1920x1080
);
#[derive(Debug, Default, Clone)]
#[allow(missing_docs)]
pub struct GDPlatformerUI {
pub width: i32, pub height: i32, pub scale: f32, pub opacity: i32, pub pos: (f32, f32), pub mode_b: bool, pub deadzone: f32, pub radius: f32, pub snap: bool, pub split: bool, }
impl GDPlatformerUI {
pub fn from_str(s: &str) -> Self {
let mut this = Self::default();
let fns = &[
Self::parse_width,
Self::parse_height,
Self::parse_scale,
Self::parse_opacity,
Self::parse_pos_x,
Self::parse_pos_y,
Self::parse_mode_b,
Self::parse_deadzone,
Self::parse_radius,
Self::parse_snap,
Self::parse_split,
];
s.split(",")
.into_iter()
.enumerate()
.for_each(|(idx, s)| (fns[idx])(&mut this, s));
this
}
fn parse_width(&mut self, s: &str) {
self.width = s.parse::<i32>().unwrap();
}
fn parse_height(&mut self, s: &str) {
self.height = s.parse::<i32>().unwrap();
}
fn parse_scale(&mut self, s: &str) {
self.scale = s.parse::<f32>().unwrap();
}
fn parse_opacity(&mut self, s: &str) {
self.opacity = s.parse::<i32>().unwrap();
}
fn parse_pos_x(&mut self, s: &str) {
self.pos.0 = s.parse::<f32>().unwrap();
}
fn parse_pos_y(&mut self, s: &str) {
self.pos.1 = s.parse::<f32>().unwrap();
}
fn parse_mode_b(&mut self, s: &str) {
self.mode_b = s.parse::<i32>().unwrap() == 1;
}
fn parse_deadzone(&mut self, s: &str) {
self.deadzone = s.parse::<f32>().unwrap();
}
fn parse_radius(&mut self, s: &str) {
self.radius = s.parse::<f32>().unwrap();
}
fn parse_snap(&mut self, s: &str) {
self.snap = s.parse::<i32>().unwrap() == 1;
}
fn parse_split(&mut self, s: &str) {
self.split = s.parse::<i32>().unwrap() == 1;
}
}
#[derive(Debug, Default, Clone)]
#[allow(missing_docs)]
pub struct GDAccount {
pub username: String,
pub plaintext_password: Option<String>,
pub account_id: i32,
pub session_id: Option<String>,
pub hashed_password: Option<String>,
pub following_creators: Vec<i32>,
pub reported_levels: Vec<i32>,
}
#[allow(missing_docs)]
#[derive(Debug, Default, Clone)]
pub struct GDCurrentValues {
pub last_played_levels: Vec<i32>,
pub current_daily_level: i32,
pub current_weekly_level: i32,
}