JSON Translation
This commit is contained in:
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -548,6 +548,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"dirs",
|
"dirs",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"phf",
|
||||||
"quick-xml 0.37.0",
|
"quick-xml 0.37.0",
|
||||||
"rfd",
|
"rfd",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -972,6 +973,48 @@ version = "2.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -1239,6 +1282,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ dirs = "5.0.1"
|
|||||||
quick-xml = "0.37.0"
|
quick-xml = "0.37.0"
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
|
phf = { version = "0.11.2", features = ["macros"] }
|
||||||
|
|||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Geometry Dash Save Translator
|
||||||
|
|
||||||
|
Convert a `CCGameManager.dat` save file to a readable `CCGameManager.json` file for data viewing/parsing
|
||||||
661
src/constants.rs
661
src/constants.rs
@@ -1,2 +1,661 @@
|
|||||||
|
use phf::{phf_map, Map};
|
||||||
|
|
||||||
|
pub const TRUNCATE_LEVEL_DATA: bool = true;
|
||||||
|
|
||||||
pub const XOR_KEY: u8 = 0xB;
|
pub const XOR_KEY: u8 = 0xB;
|
||||||
pub const PASSWORD_SALT: &str = "mI29fmAnxgTs";
|
// pub const PASSWORD_SALT: &str = "mI29fmAnxgTs";
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum ArrayInnerType {
|
||||||
|
// ! String, Uncomment if needed
|
||||||
|
Number,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum ValueType {
|
||||||
|
Array(char, ArrayInnerType),
|
||||||
|
ChunkMap(char),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static KEY_VALUE_TYPES: Map<&'static str, ValueType> = phf_map! {
|
||||||
|
"extra_artists" => ValueType::Array('.', ArrayInnerType::Number),
|
||||||
|
"used_sfx" => ValueType::Array(',', ArrayInnerType::Number),
|
||||||
|
"used_songs" => ValueType::Array(',', ArrayInnerType::Number),
|
||||||
|
"capacity_string" => ValueType::Array(',', ArrayInnerType::Number),
|
||||||
|
"scores" => ValueType::Array(',', ArrayInnerType::Number),
|
||||||
|
"extra_artist_names" => ValueType::ChunkMap(','),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BOOLEAN_PARENT_KEYS: [&str; 8] = [
|
||||||
|
"likes",
|
||||||
|
"bronze_user_coins",
|
||||||
|
"completed_levels",
|
||||||
|
"wraith_rewards",
|
||||||
|
"user_coins",
|
||||||
|
"followed_accounts",
|
||||||
|
"enabled_search_filters",
|
||||||
|
"enabled_items",
|
||||||
|
];
|
||||||
|
pub const BOOLEAN_KEYS: [&str; 3] = ["level_page_coin", "glubfub_coin", "nong"];
|
||||||
|
|
||||||
|
pub static GAMESAVE_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"GS_value" => "stats",
|
||||||
|
"GS_completed" => "completed_levels",
|
||||||
|
"GS_3" => "user_coins",
|
||||||
|
"GS_4" => "bronze_user_coins",
|
||||||
|
"GS_5" => "map_pack_stars",
|
||||||
|
"GS_6" => "shop_purchases",
|
||||||
|
"GS_7" => "level_progress",
|
||||||
|
"GS_8" => "unknown_1",
|
||||||
|
"GS_9" => "level_stars",
|
||||||
|
"GS_10" => "official_level_progress",
|
||||||
|
"GS_11" => "daily_rewards",
|
||||||
|
"GS_12" => "quests",
|
||||||
|
"GS_13" => "unknown_2",
|
||||||
|
"GS_14" => "quest_rewards",
|
||||||
|
"GS_15" => "queued_quests",
|
||||||
|
"GS_16" => "daily_progress",
|
||||||
|
"GS_17" => "daily_stars",
|
||||||
|
"GS_18" => "gauntlet_progress",
|
||||||
|
"GS_19" => "treasure_room_rewards",
|
||||||
|
"GS_20" => "total_demon_keys",
|
||||||
|
"GS_21" => "rewards",
|
||||||
|
"GS_22" => "gd_world_rewards",
|
||||||
|
"GS_23" => "gauntlet_progress_2",
|
||||||
|
"GS_24" => "daily_progress_2",
|
||||||
|
"GS_25" => "weekly_rewards",
|
||||||
|
"GS_26" => "active_path",
|
||||||
|
"GS_27" => "completed_lists",
|
||||||
|
"GS_28" => "enabled_items",
|
||||||
|
"GS_30" => "wraith_rewards",
|
||||||
|
"GS_31" => "event_rewards",
|
||||||
|
"GLM_01" => "official_levels",
|
||||||
|
"GLM_02" => "uploaded_levels",
|
||||||
|
"GLM_03" => "online_levels",
|
||||||
|
"GLM_04" => "starred_levels",
|
||||||
|
"GLM_05" => "unknown_3",
|
||||||
|
"GLM_06" => "followed_accounts",
|
||||||
|
"GLM_07" => "recently_played",
|
||||||
|
"GLM_08" => "enabled_search_filters",
|
||||||
|
"GLM_09" => "available_search_filters",
|
||||||
|
"GLM_10" => "timely_levels",
|
||||||
|
"GLM_11" => "daily_id",
|
||||||
|
"GLM_12" => "likes",
|
||||||
|
"GLM_13" => "rated_levels",
|
||||||
|
"GLM_14" => "reported_levels",
|
||||||
|
"GLM_15" => "rated_demons",
|
||||||
|
"GLM_16" => "gauntlets",
|
||||||
|
"GLM_17" => "weekly_id",
|
||||||
|
"GLM_18" => "level_folders",
|
||||||
|
"GLM_19" => "created_level_folders",
|
||||||
|
"GLM_20" => "smart_templates",
|
||||||
|
"GLM_22" => "favourited_lists",
|
||||||
|
"GLM_23" => "event_id",
|
||||||
|
"GJA_001" => "username",
|
||||||
|
"GJA_002" => "password",
|
||||||
|
"GJA_003" => "account_id",
|
||||||
|
"GJA_004" => "session_id",
|
||||||
|
"GJA_005" => "hashed_password",
|
||||||
|
"LLM_01" => "local_levels",
|
||||||
|
"LLM_02" => "binary_version",
|
||||||
|
"MDLM_001" => "song_info",
|
||||||
|
"MDLM_002" => "song_priority",
|
||||||
|
"KBM_001" => "keybinds",
|
||||||
|
"KBM_002" => "keybinds2",
|
||||||
|
"texQuality" => "texture_quality",
|
||||||
|
"customObjectDict" => "custom_objects",
|
||||||
|
"playerUserID" => "player_id",
|
||||||
|
"reportedAchievements" => "achievements",
|
||||||
|
"secretNumber" => "cod3breaker_solution",
|
||||||
|
"clickedGarage" => "clicked_icon_kit",
|
||||||
|
"hasRP" => "is_mod",
|
||||||
|
"kCEK" => "type",
|
||||||
|
"valueKeeper" => "game_values",
|
||||||
|
"unlockValueKeeper" => "ingame_events",
|
||||||
|
"sfxVolume" => "sfx_volume",
|
||||||
|
"bgVolume" => "bg_volume",
|
||||||
|
"binaryVersion" => "binary_version",
|
||||||
|
"customFPSTarget" => "fps_target",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static DIFFICULTY: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"-1" => "n/a",
|
||||||
|
"0" => "auto",
|
||||||
|
"1" => "easy",
|
||||||
|
"2" => "normal",
|
||||||
|
"3" => "hard",
|
||||||
|
"4" => "harder",
|
||||||
|
"5" => "insane",
|
||||||
|
"6" => "hard_demon",
|
||||||
|
"7" => "easy_demon",
|
||||||
|
"8" => "medium_demon",
|
||||||
|
"9" => "insane_demon",
|
||||||
|
"10" => "extreme_demon"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const REWARD_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "item",
|
||||||
|
"2" => "icon_id",
|
||||||
|
"3" => "amount",
|
||||||
|
"4" => "icon_form"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const REWARD_DATA_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "item_type",
|
||||||
|
"2" => "custom_item_id",
|
||||||
|
"3" => "amount",
|
||||||
|
"4" => "item_unlock_value",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static LEVEL_TYPE: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "official",
|
||||||
|
"2" => "local",
|
||||||
|
"3" => "saved",
|
||||||
|
"4" => "online"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static LEVEL_LENGTH: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"0" => "tiny",
|
||||||
|
"1" => "short",
|
||||||
|
"2" => "medium",
|
||||||
|
"3" => "long",
|
||||||
|
"4" => "xl",
|
||||||
|
"5" => "platformer"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static EPIC_RATING: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"0" => "none",
|
||||||
|
"1" => "epic",
|
||||||
|
"2" => "legendary",
|
||||||
|
"3" => "mythic"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static LIST_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"k1" => "id",
|
||||||
|
"k2" => "name",
|
||||||
|
"k3" => "description",
|
||||||
|
"k5" => "level_creator",
|
||||||
|
"k7" => "difficulty",
|
||||||
|
"k11" => "downloads",
|
||||||
|
"k15" => "uploaded",
|
||||||
|
"k16" => "version",
|
||||||
|
"k21" => "list_type",
|
||||||
|
"k22" => "rating",
|
||||||
|
"k25" => "is_demon",
|
||||||
|
"k26" => "stars",
|
||||||
|
"k27" => "featured",
|
||||||
|
"k42" => "original_list_id",
|
||||||
|
"k46" => "version",
|
||||||
|
"k60" => "account_id",
|
||||||
|
"k79" => "unlisted",
|
||||||
|
"k82" => "favourited",
|
||||||
|
"k83" => "order",
|
||||||
|
"k94" => "unknown_1",
|
||||||
|
"k96" => "level_ids",
|
||||||
|
"k97" => "levels",
|
||||||
|
"k98" => "upload_date",
|
||||||
|
"k99" => "update_date",
|
||||||
|
"k100" => "unknown_2",
|
||||||
|
"k113" => "diamond_reward",
|
||||||
|
"k114" => "level_count_threshold",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static LEVEL_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"k1" => "id",
|
||||||
|
"k2" => "name",
|
||||||
|
"k3" => "description",
|
||||||
|
"k4" => "level_data",
|
||||||
|
"k5" => "author",
|
||||||
|
"k6" => "player_id",
|
||||||
|
"k7" => "difficulty",
|
||||||
|
"k8" => "official_song_id",
|
||||||
|
"k9" => "rating_score_1",
|
||||||
|
"k10" => "rating_score_2",
|
||||||
|
"k11" => "downloads",
|
||||||
|
"k12" => "completions",
|
||||||
|
"k13" => "editable",
|
||||||
|
"k14" => "verified",
|
||||||
|
"k15" => "uploaded",
|
||||||
|
"k16" => "version",
|
||||||
|
"k17" => "game_version",
|
||||||
|
"k18" => "attempts",
|
||||||
|
"k19" => "percentage",
|
||||||
|
"k20" => "practice_percentage",
|
||||||
|
"k21" => "level_type",
|
||||||
|
"k22" => "likes",
|
||||||
|
"k23" => "length",
|
||||||
|
"k24" => "dislikes",
|
||||||
|
"k25" => "demon",
|
||||||
|
"k26" => "stars",
|
||||||
|
"k27" => "featured_position",
|
||||||
|
"k33" => "auto",
|
||||||
|
"k34" => "replay_data",
|
||||||
|
"k35" => "playable",
|
||||||
|
"k36" => "jumps",
|
||||||
|
"k37" => "secret_coins_to_unlock",
|
||||||
|
"k38" => "level_unlocked",
|
||||||
|
"k39" => "level_size",
|
||||||
|
"k40" => "build_version",
|
||||||
|
"k41" => "password",
|
||||||
|
"k42" => "copied_id",
|
||||||
|
"k43" => "two_player",
|
||||||
|
"k45" => "custom_song_id",
|
||||||
|
"k46" => "revision",
|
||||||
|
"k47" => "edited",
|
||||||
|
"k48" => "objects",
|
||||||
|
"k50" => "binary_version",
|
||||||
|
"k60" => "account_id",
|
||||||
|
"k61" => "first_coin_collected",
|
||||||
|
"k62" => "second_coin_collected",
|
||||||
|
"k63" => "third_coin_collected",
|
||||||
|
"k64" => "total_coins",
|
||||||
|
"k65" => "verified_coins",
|
||||||
|
"k66" => "requested_stars",
|
||||||
|
"k67" => "capacity_string",
|
||||||
|
"k68" => "anti_cheat_triggered",
|
||||||
|
"k69" => "high_object_count",
|
||||||
|
"k71" => "mana_orb_percentage",
|
||||||
|
"k72" => "ldm",
|
||||||
|
"k73" => "ldm_enabled",
|
||||||
|
"k74" => "timely_id",
|
||||||
|
"k75" => "epic_rating",
|
||||||
|
"k76" => "demon_type",
|
||||||
|
"k77" => "is_gauntlet",
|
||||||
|
"k78" => "is_gauntlet_2",
|
||||||
|
"k79" => "unlisted",
|
||||||
|
"k80" => "editor_time",
|
||||||
|
"k81" => "total_editor_time",
|
||||||
|
"k82" => "favourited",
|
||||||
|
"k83" => "saved_level_index",
|
||||||
|
"k84" => "folder",
|
||||||
|
"k85" => "clicks",
|
||||||
|
"k86" => "best_attempt_time",
|
||||||
|
"k87" => "seed",
|
||||||
|
"k88" => "scores",
|
||||||
|
"k89" => "leaderboard_valid",
|
||||||
|
"k90" => "leaderboard_percentage",
|
||||||
|
"k91" => "unknown_1",
|
||||||
|
"k92" => "unknown_2",
|
||||||
|
"k93" => "unlimited_objects_?",
|
||||||
|
"k94" => "platformer_?",
|
||||||
|
"k95" => "ticks_to_complete",
|
||||||
|
"k101" => "unknown_3",
|
||||||
|
"k104" => "used_songs",
|
||||||
|
"k105" => "used_sfx",
|
||||||
|
"k106" => "unknown_4",
|
||||||
|
"k107" => "best_time",
|
||||||
|
"k108" => "best_points",
|
||||||
|
"k109" => "local_times",
|
||||||
|
"k110" => "local_scores",
|
||||||
|
"k111" => "platformer_seed",
|
||||||
|
"k112" => "disable_shake",
|
||||||
|
"kI1" => "editor_camera_x",
|
||||||
|
"kI2" => "editor_camera_y",
|
||||||
|
"kI3" => "editor_camera_zoom",
|
||||||
|
"kI4" => "editor_build_tab_page",
|
||||||
|
"kI5" => "editor_build_tab_category",
|
||||||
|
"kI6" => "editor_recent_pages",
|
||||||
|
"kI7" => "editor_layer"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const KCEK_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"4" => "level",
|
||||||
|
"6" => "song",
|
||||||
|
"7" => "quest",
|
||||||
|
"8" => "reward",
|
||||||
|
"9" => "reward_data",
|
||||||
|
"10" => "smart_templates",
|
||||||
|
"11" => "smart_template_variation",
|
||||||
|
"12" => "lists"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SMART_TEMPLATE_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "id",
|
||||||
|
"2" => "name",
|
||||||
|
"3" => "variations",
|
||||||
|
"4" => "unknown_1",
|
||||||
|
"5" => "unknown_2",
|
||||||
|
"6" => "unknown_3",
|
||||||
|
"7" => "unknown_4",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const STAT_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "jumps",
|
||||||
|
"2" => "attempts",
|
||||||
|
"3" => "official_completed_levels",
|
||||||
|
"4" => "online_completed_levels",
|
||||||
|
"5" => "completed_demons",
|
||||||
|
"6" => "stars",
|
||||||
|
"7" => "completed_map_packs",
|
||||||
|
"8" => "secret_coins",
|
||||||
|
"9" => "destroyed_players",
|
||||||
|
"10" => "liked_levels",
|
||||||
|
"11" => "rated_levels",
|
||||||
|
"12" => "user_coins",
|
||||||
|
"13" => "diamonds",
|
||||||
|
"14" => "orbs",
|
||||||
|
"15" => "completed_dailies",
|
||||||
|
"16" => "fire_shards",
|
||||||
|
"17" => "ice_shards",
|
||||||
|
"18" => "poison_shards",
|
||||||
|
"19" => "shadow_shards",
|
||||||
|
"20" => "lava_shards",
|
||||||
|
"21" => "demon_keys",
|
||||||
|
"22" => "total_orbs",
|
||||||
|
"23" => "earth_shards",
|
||||||
|
"24" => "blood_shards",
|
||||||
|
"25" => "metal_shards",
|
||||||
|
"26" => "light_shards",
|
||||||
|
"27" => "soul_shards",
|
||||||
|
"28" => "moons",
|
||||||
|
"29" => "spendable_diamonds",
|
||||||
|
"30" => "fire_path",
|
||||||
|
"31" => "ice_path",
|
||||||
|
"32" => "poison_path",
|
||||||
|
"33" => "shadow_path",
|
||||||
|
"34" => "lava_path",
|
||||||
|
"35" => "earth_path",
|
||||||
|
"36" => "blood_path",
|
||||||
|
"37" => "metal_path",
|
||||||
|
"38" => "light_path",
|
||||||
|
"39" => "soul_path",
|
||||||
|
"40" => "gauntlets",
|
||||||
|
"41" => "list_rewards",
|
||||||
|
"42" => "insane_levels_completed",
|
||||||
|
"43" => "event_levels_completed",
|
||||||
|
"unique_secretB03" => "glubfub_coin",
|
||||||
|
"unique_secret04" => "level_page_coin",
|
||||||
|
"unique_secret06" => "sparky_coin",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const QUEST_TYPE: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "orbs",
|
||||||
|
"2" => "coins",
|
||||||
|
"3" => "stars",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const QUEST_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "type",
|
||||||
|
"2" => "obtained_items",
|
||||||
|
"3" => "required_items",
|
||||||
|
"4" => "diamonds",
|
||||||
|
"5" => "time_left",
|
||||||
|
"6" => "active",
|
||||||
|
"7" => "name",
|
||||||
|
"8" => "tier"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ITEMS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "fire_shards",
|
||||||
|
"2" => "ice_shards",
|
||||||
|
"3" => "poison_shards",
|
||||||
|
"4" => "shadow_shards",
|
||||||
|
"5" => "lava_shards",
|
||||||
|
"6" => "demon_key",
|
||||||
|
"7" => "orbs",
|
||||||
|
"8" => "diamonds",
|
||||||
|
"9" => "icon",
|
||||||
|
"10" => "earth_shards",
|
||||||
|
"11" => "blood_shards",
|
||||||
|
"12" => "metal_shards",
|
||||||
|
"13" => "light_shards",
|
||||||
|
"14" => "soul_shards",
|
||||||
|
"15" => "golden_keys"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ICON_FORMS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "cube",
|
||||||
|
"2" => "color_1",
|
||||||
|
"3" => "color_2",
|
||||||
|
"4" => "ship",
|
||||||
|
"5" => "ball",
|
||||||
|
"6" => "ufo",
|
||||||
|
"7" => "wave",
|
||||||
|
"8" => "robot",
|
||||||
|
"9" => "spider",
|
||||||
|
"10" => "trail",
|
||||||
|
"11" => "death_effect",
|
||||||
|
"12" => "item",
|
||||||
|
"13" => "swing",
|
||||||
|
"14" => "jetpack",
|
||||||
|
"15" => "ship_fire"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CHEST_IDS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "4_hour",
|
||||||
|
"2" => "24_hour",
|
||||||
|
"3" => "1_key",
|
||||||
|
"4" => "5_key",
|
||||||
|
"5" => "10_key",
|
||||||
|
"6" => "25_key",
|
||||||
|
"7" => "50_key",
|
||||||
|
"8" => "100_key"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SONG_KEYS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "id",
|
||||||
|
"2" => "name",
|
||||||
|
"3" => "artist_id",
|
||||||
|
"4" => "artist",
|
||||||
|
"5" => "size_mb",
|
||||||
|
"6" => "youtube_id",
|
||||||
|
"7" => "youtube_channel",
|
||||||
|
"8" => "verified",
|
||||||
|
"9" => "priority",
|
||||||
|
"10" => "mp3_url",
|
||||||
|
"11" => "nong",
|
||||||
|
"12" => "extra_artists",
|
||||||
|
"13" => "new",
|
||||||
|
"14" => "new_type",
|
||||||
|
"15" => "extra_artist_names"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SPECIAL_SHOP_PURCHASES: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"187" => "menu_music_customizer",
|
||||||
|
"188" => "practice_music_unlocker",
|
||||||
|
"244" => "robot_animation_slow",
|
||||||
|
"245" => "robot_animation_fast",
|
||||||
|
"246" => "spider_animation_fast"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const GAME_VARIABLES: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"gv_0001" => "editor.follow_player",
|
||||||
|
"gv_0002" => "editor.play_music",
|
||||||
|
"gv_0003" => "editor.swipe",
|
||||||
|
"gv_0004" => "editor.free_move",
|
||||||
|
"gv_0005" => "editor.delete_filter",
|
||||||
|
"gv_0006" => "editor.delete_object_id",
|
||||||
|
"gv_0007" => "editor.rotate_enabled",
|
||||||
|
"gv_0008" => "editor.snap_enabled",
|
||||||
|
"gv_0009" => "editor.ignore_damage",
|
||||||
|
"gv_0010" => "flip2_player_controls",
|
||||||
|
"gv_0011" => "always_limit_controls",
|
||||||
|
"gv_0012" => "accepted_comment_rules",
|
||||||
|
"gv_0013" => "increase_max_undo",
|
||||||
|
"gv_0014" => "disable_explosion_shake",
|
||||||
|
"gv_0015" => "flip_pause_button",
|
||||||
|
"gv_0016" => "accepted_song_tos",
|
||||||
|
"gv_0018" => "no_song_limit",
|
||||||
|
"gv_0019" => "load_songs_to_memory",
|
||||||
|
"gv_0022" => "higher_audio_quality",
|
||||||
|
"gv_0023" => "smooth_fix",
|
||||||
|
"gv_0024" => "show_cursor",
|
||||||
|
"gv_0025" => "fullscreen",
|
||||||
|
"gv_0026" => "auto_retry",
|
||||||
|
"gv_0027" => "auto_checkpoints",
|
||||||
|
"gv_0028" => "disable_thumbstick",
|
||||||
|
"gv_0029" => "showed_upload_popup",
|
||||||
|
"gv_0030" => "vsync",
|
||||||
|
"gv_0033" => "change_song_location",
|
||||||
|
"gv_0034" => "game_center",
|
||||||
|
"gv_0036" => "editor.preview_mode",
|
||||||
|
"gv_0037" => "editor.show_ground",
|
||||||
|
"gv_0038" => "editor.show_grid",
|
||||||
|
"gv_0039" => "editor.grid_on_top",
|
||||||
|
"gv_0040" => "show_percentage",
|
||||||
|
"gv_0041" => "editor.show_object_info",
|
||||||
|
"gv_0042" => "increase_max_levels",
|
||||||
|
"gv_0043" => "editor.effect_lines",
|
||||||
|
"gv_0044" => "editor.trigger_boxes",
|
||||||
|
"gv_0045" => "editor.debug_draw",
|
||||||
|
"gv_0046" => "editor.hide_ui_on_test",
|
||||||
|
"gv_0047" => "showed_profile_text",
|
||||||
|
"gv_0049" => "editor.columns",
|
||||||
|
"gv_0050" => "editor.rows",
|
||||||
|
"gv_0051" => "showed_ng_message",
|
||||||
|
"gv_0052" => "fast_respawn",
|
||||||
|
"gv_0053" => "showed_free_games_popup",
|
||||||
|
"gv_0056" => "disable_high_object_alert",
|
||||||
|
"gv_0057" => "editor.hold_to_swipe",
|
||||||
|
"gv_0058" => "editor.duration_lines",
|
||||||
|
"gv_0059" => "editor.swipe_cycle_mode",
|
||||||
|
"gv_0060" => "default_mini_icon",
|
||||||
|
"gv_0061" => "switch_spider_teleport_color",
|
||||||
|
"gv_0062" => "switch_dash_fire_color",
|
||||||
|
"gv_0063" => "showed_unverified_coins_message",
|
||||||
|
"gv_0064" => "editor.select_filter",
|
||||||
|
"gv_0065" => "enable_move_optimization",
|
||||||
|
"gv_0066" => "high_capacity_mode",
|
||||||
|
"gv_0067" => "high_start_pos_accuracy",
|
||||||
|
"gv_0068" => "quick_checkpoint_mode",
|
||||||
|
"gv_0069" => "comment_mode",
|
||||||
|
"gv_0070" => "showed_unlisted_level_message",
|
||||||
|
"gv_0072" => "disable_gravity_effect",
|
||||||
|
"gv_0073" => "new_completed_filter",
|
||||||
|
"gv_0074" => "show_restart_button",
|
||||||
|
"gv_0075" => "parental.disable_comments",
|
||||||
|
"gv_0076" => "parental.disable_account_comments",
|
||||||
|
"gv_0077" => "parental.featured_levels_only",
|
||||||
|
"gv_0078" => "editor.hide_background",
|
||||||
|
"gv_0079" => "editor.hide_grid_on_play",
|
||||||
|
"gv_0081" => "disable_shake",
|
||||||
|
"gv_0082" => "disable_high_object_alert",
|
||||||
|
"gv_0083" => "disable_song_alert",
|
||||||
|
"gv_0084" => "manual_order",
|
||||||
|
"gv_0088" => "compact_comments",
|
||||||
|
"gv_0089" => "extended_info_mode",
|
||||||
|
"gv_0090" => "auto_load_comments",
|
||||||
|
"gv_0091" => "created_level_folder",
|
||||||
|
"gv_0092" => "saved_level_folder",
|
||||||
|
"gv_0093" => "increase_levels_per_page",
|
||||||
|
"gv_0094" => "more_comments",
|
||||||
|
"gv_0095" => "do_not",
|
||||||
|
"gv_0096" => "switch_wave_trail_color",
|
||||||
|
"gv_0097" => "editor.enable_link_controls",
|
||||||
|
"gv_0098" => "level_leaderboard_type",
|
||||||
|
"gv_0099" => "show_leaderboard_percent",
|
||||||
|
"gv_0100" => "practice_death_effect",
|
||||||
|
"gv_0101" => "force_smooth_fix",
|
||||||
|
"gv_0102" => "editor.editor_smooth_fix",
|
||||||
|
"gv_0103" => "editor.layer_locking",
|
||||||
|
"gv_0108" => "auto_enable_low_detail",
|
||||||
|
"gv_0112" => "editor.increase_scale_limit",
|
||||||
|
"gv_0119" => "dont_save_levels",
|
||||||
|
"gv_0125" => "editor.unlock_practice_music",
|
||||||
|
"gv_0126" => "decimal_percentage",
|
||||||
|
"gv_0127" => "save_gauntlet_levels",
|
||||||
|
"gv_0129" => "disable_portal_labels",
|
||||||
|
"gv_0130" => "enable_orb_labels",
|
||||||
|
"gv_0134" => "hide_attempts_practice",
|
||||||
|
"gv_0135" => "hide_attempts",
|
||||||
|
"gv_0136" => "extra_ldm",
|
||||||
|
"gv_0137" => "editor.hide_particle_icons",
|
||||||
|
"gv_0140" => "disable_orb_scale",
|
||||||
|
"gv_0141" => "disable_trigger_orb_scale",
|
||||||
|
"gv_0142" => "reduce_audio_quality",
|
||||||
|
"gv_0149" => "editor.show_clicks",
|
||||||
|
"gv_0150" => "editor.auto_pause",
|
||||||
|
"gv_0151" => "editor.start_optimization",
|
||||||
|
"gv_0152" => "editor.hide_path",
|
||||||
|
"gv_0155" => "disable_shader_anti_aliasing",
|
||||||
|
"gv_0156" => "editor.disable_paste_state_groups",
|
||||||
|
"gv_0159" => "audio_fix_01"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const GAME_EVENTS: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"ugv_1" => "challenge_unlocked",
|
||||||
|
"ugv_2" => "glubfub_hint_1",
|
||||||
|
"ugv_3" => "glubfub_hint_2",
|
||||||
|
"ugv_4" => "challenge_completed",
|
||||||
|
"ugv_5" => "treasure_room_unlocked",
|
||||||
|
"ugv_6" => "chamber_of_time_unlocked",
|
||||||
|
"ugv_7" => "chamber_of_time_discovered",
|
||||||
|
"ugv_8" => "found_master_emblem",
|
||||||
|
"ugv_9" => "gatekeeper_dialogue",
|
||||||
|
"ugv_10" => "scratch_dialogue",
|
||||||
|
"ugv_11" => "scratch_shop_unlocked",
|
||||||
|
"ugv_12" => "monster_dialogue",
|
||||||
|
"ugv_13" => "demon_gauntlet_key",
|
||||||
|
"ugv_14" => "blue_key",
|
||||||
|
"ugv_15" => "green_key",
|
||||||
|
"ugv_16" => "orange_key",
|
||||||
|
"ugv_17" => "shopkeeper_dialogue",
|
||||||
|
"ugv_18" => "gdw_online_unlocked",
|
||||||
|
"ugv_19" => "monster_encountered",
|
||||||
|
"ugv_20" => "community_shop_unlocked",
|
||||||
|
"ugv_21" => "potbor_dialogue",
|
||||||
|
"ugv_22" => "youtube_chest",
|
||||||
|
"ugv_23" => "facebook_chest",
|
||||||
|
"ugv_24" => "twitter_chest",
|
||||||
|
"ugv_25" => "explorers_unlocked",
|
||||||
|
"ugv_26" => "twitch_chest",
|
||||||
|
"ugv_27" => "discord_chest",
|
||||||
|
"ugv_28" => "tower_clicked",
|
||||||
|
"ugv_29" => "tower_entered",
|
||||||
|
"ugv_30" => "accepted_tos",
|
||||||
|
"ugv_31" => "zolguroth_encountered",
|
||||||
|
"ugv_32" => "reddit_chest",
|
||||||
|
"ugv_33" => "tower_floor_1_completed",
|
||||||
|
"ugv_34" => "diamond_shop_unlocked",
|
||||||
|
"ugv_35" => "mechanic_unlocked",
|
||||||
|
"ugv_36" => "mechanic_dialogue",
|
||||||
|
"ugv_37" => "diamond_shopkeeper_dialogue",
|
||||||
|
"ugv_38" => "unknown_1"
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OFFICIAL_LEVEL_NAMES: Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"1" => "stereo_madness",
|
||||||
|
"2" => "back_on_track",
|
||||||
|
"3" => "polargeist",
|
||||||
|
"4" => "dry_out",
|
||||||
|
"5" => "base_after_base",
|
||||||
|
"6" => "cant_let_go",
|
||||||
|
"7" => "jumper",
|
||||||
|
"8" => "time_machine",
|
||||||
|
"9" => "cycles",
|
||||||
|
"10" => "xstep",
|
||||||
|
"11" => "clutterfunk",
|
||||||
|
"12" => "theory_of_everything",
|
||||||
|
"13" => "electroman_adventures",
|
||||||
|
"14" => "clubstep",
|
||||||
|
"15" => "electrodynamix",
|
||||||
|
"16" => "hexagon_force",
|
||||||
|
"17" => "blast_processing",
|
||||||
|
"18" => "theory_of_everything_2",
|
||||||
|
"19" => "geometrical_dominator",
|
||||||
|
"20" => "deadlocked",
|
||||||
|
"21" => "fingerdash",
|
||||||
|
"22" => "dash",
|
||||||
|
"23" => "explorers",
|
||||||
|
"1001" => "the_seven_seas",
|
||||||
|
"1002" => "viking_arena",
|
||||||
|
"1003" => "airborne_robots",
|
||||||
|
"2001" => "payload",
|
||||||
|
"2002" => "beast_mode",
|
||||||
|
"2003" => "machina",
|
||||||
|
"2004" => "years",
|
||||||
|
"2005" => "frontlines",
|
||||||
|
"2006" => "space_pirates",
|
||||||
|
"2007" => "striker",
|
||||||
|
"2008" => "embers",
|
||||||
|
"2009" => "round_1",
|
||||||
|
"2010" => "monster_dance_off",
|
||||||
|
"3001" => "the_challenge",
|
||||||
|
"4001" => "press_start",
|
||||||
|
"4002" => "nock_em",
|
||||||
|
"4003" => "power_trip",
|
||||||
|
"5001" => "the_tower",
|
||||||
|
"5002" => "the_sewers",
|
||||||
|
"5003" => "the_cellar",
|
||||||
|
"5004" => "the_secret_hollow"
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod constants;
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use constants::XOR_KEY;
|
use constants::XOR_KEY;
|
||||||
|
use serde_json::Value;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use util::decrypt::*;
|
use util::decrypt::*;
|
||||||
use util::file::*;
|
use util::file::*;
|
||||||
@@ -10,15 +11,14 @@ use util::xml::*;
|
|||||||
fn main() {
|
fn main() {
|
||||||
if let Some(data_encrypted) = get_dat_data() {
|
if let Some(data_encrypted) = get_dat_data() {
|
||||||
let data: Vec<u8> =
|
let data: Vec<u8> =
|
||||||
gz_decompress(&base64_decode(&xor_decrypt(&data_encrypted, XOR_KEY)).unwrap());
|
gz_decompress(&decode_urlsafe_base64(&xor_decrypt(&data_encrypted, XOR_KEY)).unwrap());
|
||||||
|
|
||||||
match validate_xml(&data) {
|
match validate_xml(&data) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("{:#?}", parse_save(&data));
|
let parsed_data: Value = parse_save(&data);
|
||||||
println!("Decryption successful");
|
|
||||||
|
|
||||||
if let Some(output_path) = prompt_output_path() {
|
if let Some(output_path) = prompt_output_path() {
|
||||||
write_decrypted_data(&data, output_path);
|
write_decrypted_json(&parsed_data, output_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
83
src/util/convert.rs
Normal file
83
src/util/convert.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use serde_json::{Map, Number, Value};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::constants::{ArrayInnerType, ValueType, BOOLEAN_KEYS, KEY_VALUE_TYPES};
|
||||||
|
|
||||||
|
pub fn convert_value(value: &str, key: Option<&str>) -> Value {
|
||||||
|
let parsed: Value = parse_number(value);
|
||||||
|
|
||||||
|
if KEY_VALUE_TYPES.contains_key(key.unwrap_or_default()) {
|
||||||
|
match KEY_VALUE_TYPES[key.unwrap_or_default()] {
|
||||||
|
ValueType::Array(separator, inner_type) => {
|
||||||
|
split_or_single_string(value, separator, &inner_type)
|
||||||
|
}
|
||||||
|
ValueType::ChunkMap(separator) => chunk_map(value, separator),
|
||||||
|
}
|
||||||
|
} else if BOOLEAN_KEYS.contains(&key.unwrap_or_default()) {
|
||||||
|
number_to_bool(parsed)
|
||||||
|
} else {
|
||||||
|
parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_number(input: &str) -> Value {
|
||||||
|
if let Ok(int_val) = input.parse::<i64>() {
|
||||||
|
Value::Number(Number::from(int_val))
|
||||||
|
} else if let Ok(float_val) = input.parse::<f64>() {
|
||||||
|
Number::from_f64(float_val)
|
||||||
|
.map(Value::Number)
|
||||||
|
.unwrap_or_else(|| Value::String(input.to_string()))
|
||||||
|
} else {
|
||||||
|
Value::String(input.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn number_to_bool(value: Value) -> Value {
|
||||||
|
match value {
|
||||||
|
Value::Number(num) => {
|
||||||
|
if let Some(float_val) = num.as_f64() {
|
||||||
|
Value::Bool(float_val.trunc() as i64 >= 1)
|
||||||
|
} else {
|
||||||
|
Value::Bool(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Value::Bool(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunk_map(value: &str, separator: char) -> Value {
|
||||||
|
let parts: Vec<&str> = value.split(separator).collect();
|
||||||
|
|
||||||
|
if parts.len() % 2 != 0 {
|
||||||
|
Value::String(value.to_string())
|
||||||
|
} else {
|
||||||
|
let mut map: Map<String, Value> = Map::new();
|
||||||
|
|
||||||
|
for chunk in parts.chunks(2) {
|
||||||
|
if let (Ok(key), value) = (i64::from_str(chunk[0]), chunk[1].to_string()) {
|
||||||
|
map.insert(key.to_string(), Value::String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Object(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split_or_single_string(input: &str, separator: char, inner_type: &ArrayInnerType) -> Value {
|
||||||
|
let values: Vec<Value> = if input.contains(separator) {
|
||||||
|
input
|
||||||
|
.split(separator)
|
||||||
|
.map(|s| match inner_type {
|
||||||
|
// ArrayInnerType::String => Value::String(s.to_string()), // ! Uncomment if needed
|
||||||
|
ArrayInnerType::Number => parse_number(s),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![match inner_type {
|
||||||
|
// ArrayInnerType::String => Value::String(input), // ! Uncomment if needed
|
||||||
|
ArrayInnerType::Number => parse_number(input),
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
Value::Array(values)
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
use base64::{engine::general_purpose::URL_SAFE, DecodeError, Engine};
|
use base64::{
|
||||||
|
engine::general_purpose::{STANDARD, URL_SAFE},
|
||||||
|
DecodeError, Engine,
|
||||||
|
};
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
@@ -8,7 +11,7 @@ pub fn xor_decrypt(data: &[u8], key: u8) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2
|
// Step 2
|
||||||
pub fn base64_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
pub fn decode_urlsafe_base64(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
||||||
let url_safe_data: Vec<u8> = data
|
let url_safe_data: Vec<u8> = data
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&&byte| byte != 0)
|
.filter(|&&byte| byte != 0)
|
||||||
@@ -29,3 +32,12 @@ pub fn gz_decompress(data: &[u8]) -> Vec<u8> {
|
|||||||
decoder.read_to_end(&mut decompressed).unwrap();
|
decoder.read_to_end(&mut decompressed).unwrap();
|
||||||
decompressed
|
decompressed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode_base64(data: &str) -> String {
|
||||||
|
STANDARD
|
||||||
|
.decode(data)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|&byte| byte as char)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
use dirs::{data_local_dir, desktop_dir};
|
use dirs::{data_local_dir, desktop_dir};
|
||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use std::{fs::File, io::Read, io::Write, path::PathBuf};
|
use serde_json::{to_writer_pretty, Value};
|
||||||
|
use std::{fs::File, io::Read, path::PathBuf};
|
||||||
|
|
||||||
pub fn get_dat_data() -> Option<Vec<u8>> {
|
pub fn get_dat_data() -> Option<Vec<u8>> {
|
||||||
if let Some(path) = prompt_input_path() {
|
match prompt_input_path() {
|
||||||
|
Some(path) => {
|
||||||
let mut file: File = File::open(path).unwrap();
|
let mut file: File = File::open(path).unwrap();
|
||||||
let mut data: Vec<u8> = Vec::new();
|
let mut data: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
file.read_to_end(&mut data).unwrap();
|
file.read_to_end(&mut data).unwrap();
|
||||||
return Some(data);
|
|
||||||
|
Some(data)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prompt_input_path() -> Option<PathBuf> {
|
pub fn prompt_input_path() -> Option<PathBuf> {
|
||||||
@@ -24,13 +29,13 @@ pub fn prompt_input_path() -> Option<PathBuf> {
|
|||||||
pub fn prompt_output_path() -> Option<PathBuf> {
|
pub fn prompt_output_path() -> Option<PathBuf> {
|
||||||
FileDialog::new()
|
FileDialog::new()
|
||||||
.set_title("Select Output Location")
|
.set_title("Select Output Location")
|
||||||
.set_file_name("CCGameManager.xml")
|
.set_file_name("CCGameManager.json")
|
||||||
.add_filter("Geometry Dash Decrypted Save", &["xml"])
|
.add_filter("Geometry Dash Translated Save", &["json"])
|
||||||
.set_directory(desktop_dir().unwrap())
|
.set_directory(desktop_dir().unwrap())
|
||||||
.save_file()
|
.save_file()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_decrypted_data(data: &[u8], output_path: PathBuf) {
|
pub fn write_decrypted_json(data: &Value, path: PathBuf) {
|
||||||
let mut output_file: File = File::create(output_path).unwrap();
|
let output_file: File = File::create(path).expect("Failed to create file");
|
||||||
output_file.write_all(data).unwrap()
|
to_writer_pretty(output_file, data).expect("Failed to write JSON");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod convert;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod xml;
|
pub mod xml;
|
||||||
|
|||||||
199
src/util/xml.rs
199
src/util/xml.rs
@@ -1,48 +1,140 @@
|
|||||||
|
use super::super::constants::*;
|
||||||
|
use super::convert::{convert_value, number_to_bool};
|
||||||
|
use super::decrypt::decode_base64;
|
||||||
use quick_xml::{escape::unescape, events::Event, reader::Reader};
|
use quick_xml::{escape::unescape, events::Event, reader::Reader};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
pub fn validate_xml(xml: &[u8]) -> Result<(), String> {
|
pub fn validate_xml(xml: &[u8]) -> Result<(), &str> {
|
||||||
let xml_str: String = String::from_utf8_lossy(xml).to_string();
|
const XML_HEADER: &[u8] = b"<?xml version=\"1.0\"?>";
|
||||||
|
|
||||||
if xml_str.starts_with("<?xml version=\"1.0\"?>") {
|
if xml.starts_with(XML_HEADER) {
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
return Err("Invalid/missing XML header".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Invalid/missing XML header")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_save(data: &[u8]) -> Value {
|
pub fn parse_save(data: &[u8]) -> Value {
|
||||||
let mut reader: Reader<&[u8]> = Reader::from_reader(data);
|
let mut reader: Reader<&[u8]> = Reader::from_reader(data);
|
||||||
reader.config_mut().trim_text(true);
|
reader.config_mut().trim_text(true);
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
parse_dict(&mut reader, &mut buf)
|
let mut buf: Vec<u8> = Vec::with_capacity(data.len());
|
||||||
|
|
||||||
|
parse_dict(None, None, &mut reader, &mut buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_dict(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> Value {
|
fn parse_dict(
|
||||||
|
parent_parent_key: Option<&str>,
|
||||||
|
parent_key: Option<&str>,
|
||||||
|
reader: &mut Reader<&[u8]>,
|
||||||
|
buf: &mut Vec<u8>,
|
||||||
|
) -> Value {
|
||||||
let mut map: Map<String, Value> = Map::new();
|
let mut map: Map<String, Value> = Map::new();
|
||||||
let mut current_key: Option<String> = None;
|
let mut current_key: Option<String> = None;
|
||||||
|
let mut current_kcek: Option<&str> = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match reader.read_event_into(buf) {
|
match reader.read_event_into(buf) {
|
||||||
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
|
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
|
||||||
Ok(Event::Eof) => break,
|
Ok(Event::Eof) => break,
|
||||||
|
|
||||||
Ok(Event::Start(ref e)) => match e.name().as_ref() {
|
Ok(Event::Start(ref e)) => match e.name().as_ref() {
|
||||||
b"k" => current_key = Some(read_text(reader, buf).to_string()),
|
b"k" => {
|
||||||
|
let key: &str = &read_text(reader, buf, &mut None, &mut current_kcek, None);
|
||||||
|
|
||||||
|
current_key = match key {
|
||||||
|
key if GAMESAVE_KEYS.contains_key(key) => {
|
||||||
|
Some(GAMESAVE_KEYS[key].to_string())
|
||||||
|
}
|
||||||
|
key if GAME_EVENTS.contains_key(key) => Some(GAME_EVENTS[key].to_string()),
|
||||||
|
key if GAME_VARIABLES.contains_key(key) => {
|
||||||
|
Some(GAME_VARIABLES[key].to_string())
|
||||||
|
}
|
||||||
|
key if parent_key.unwrap_or_default() == "stats" => {
|
||||||
|
let stat_key: &str = if STAT_KEYS.contains_key(key) {
|
||||||
|
STAT_KEYS[key]
|
||||||
|
} else {
|
||||||
|
key
|
||||||
|
};
|
||||||
|
|
||||||
|
if stat_key.starts_with("unique_") {
|
||||||
|
let parts: Vec<&str> = stat_key.split("_").collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
match parts.len() {
|
||||||
|
3 => Some(format!(
|
||||||
|
"secret_coin_{}_{}",
|
||||||
|
OFFICIAL_LEVEL_NAMES[parts[1]], parts[2]
|
||||||
|
)),
|
||||||
|
_ => Some(stat_key.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(stat_key.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key if parent_key.unwrap_or_default() == "enabled_items" => {
|
||||||
|
let parts: Vec<&str> = key.split("_").collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
match parts.len() {
|
||||||
|
2 => Some(format!("{}_{}", ICON_FORMS[parts[1]], parts[0])),
|
||||||
|
_ => Some(key.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key if parent_key.unwrap_or_default() == "daily_rewards" => {
|
||||||
|
let parts: Vec<&str> = key.split("_").collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
match parts.len() {
|
||||||
|
2 => Some(format!("{}_{}", CHEST_IDS[parts[0]], parts[1])),
|
||||||
|
_ => Some(key.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key if parent_key.unwrap_or_default() == "amount"
|
||||||
|
&& key.starts_with("k_") =>
|
||||||
|
{
|
||||||
|
let replaced_key = key.replace("k_", "");
|
||||||
|
Some(replaced_key)
|
||||||
|
}
|
||||||
|
key if parent_key.unwrap_or_default() == "shop_purchases"
|
||||||
|
&& SPECIAL_SHOP_PURCHASES.contains_key(key) =>
|
||||||
|
{
|
||||||
|
Some(SPECIAL_SHOP_PURCHASES[key].to_string())
|
||||||
|
}
|
||||||
|
key if current_kcek.is_some() => match *current_kcek.as_ref().unwrap() {
|
||||||
|
"level" => Some(LEVEL_KEYS[key].to_string()),
|
||||||
|
"song" => Some(SONG_KEYS[key].to_string()),
|
||||||
|
"quest" => Some(QUEST_KEYS[key].to_string()),
|
||||||
|
"reward" => Some(REWARD_KEYS[key].to_string()),
|
||||||
|
"reward_data" => Some(REWARD_DATA_KEYS[key].to_string()),
|
||||||
|
"smart_templates" => Some(SMART_TEMPLATE_KEYS[key].to_string()),
|
||||||
|
"lists" => Some(LIST_KEYS[key].to_string()),
|
||||||
|
_ => Some(key.to_string()),
|
||||||
|
},
|
||||||
|
_ => Some(key.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
b"s" | b"r" | b"i" => {
|
b"s" | b"r" | b"i" => {
|
||||||
if let Some(key) = current_key.take() {
|
if let Some(key) = current_key.take() {
|
||||||
let value = read_text(reader, buf);
|
let value: String = read_text(
|
||||||
map.insert(key, Value::String(value));
|
reader,
|
||||||
|
buf,
|
||||||
|
&mut Some(key.as_str()),
|
||||||
|
&mut current_kcek,
|
||||||
|
parent_parent_key,
|
||||||
|
);
|
||||||
|
map.insert(key.to_string(), convert_value(&value, Some(key.as_str())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"d" => {
|
b"d" => {
|
||||||
if let Some(key) = current_key.take() {
|
if let Some(key) = current_key.take() {
|
||||||
let nested_dict = parse_dict(reader, buf);
|
let nested_dict: Value =
|
||||||
map.insert(key, nested_dict);
|
parse_dict(parent_key, Some(key.as_str()), reader, buf);
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
key.to_string(),
|
||||||
|
if BOOLEAN_PARENT_KEYS.contains(&key.as_str()) {
|
||||||
|
filter_true_keys(nested_dict)
|
||||||
|
} else {
|
||||||
|
nested_dict
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -57,11 +149,76 @@ fn parse_dict(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> Value {
|
|||||||
Value::Object(map)
|
Value::Object(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_text(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> String {
|
fn filter_true_keys(object: Value) -> Value {
|
||||||
|
if let Value::Object(map) = object {
|
||||||
|
let keys_with_true_values: Vec<Value> = map
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(key, value)| {
|
||||||
|
if number_to_bool(value) == Value::Bool(true) {
|
||||||
|
Some(Value::String(key))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Value::Array(keys_with_true_values)
|
||||||
|
} else {
|
||||||
|
Value::Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(
|
||||||
|
reader: &mut Reader<&[u8]>,
|
||||||
|
buf: &mut Vec<u8>,
|
||||||
|
key: &mut Option<&str>,
|
||||||
|
kcek: &mut Option<&str>,
|
||||||
|
parent_parent_key: Option<&str>,
|
||||||
|
) -> String {
|
||||||
match reader.read_event_into(buf) {
|
match reader.read_event_into(buf) {
|
||||||
Ok(Event::Text(e)) => unescape(String::from_utf8_lossy(&e).to_string().as_str())
|
Ok(Event::Text(e)) => {
|
||||||
|
let text: String = unescape(String::from_utf8_lossy(&e).to_string().as_str())
|
||||||
.expect("Failed to unescape text")
|
.expect("Failed to unescape text")
|
||||||
.into_owned(),
|
.into_owned();
|
||||||
|
|
||||||
|
if let Some(key) = key {
|
||||||
|
return match *key {
|
||||||
|
"type" => {
|
||||||
|
if KCEK_KEYS.contains_key(text.as_str()) {
|
||||||
|
let kcek_type: &str = KCEK_KEYS[text.as_str()];
|
||||||
|
*kcek = Some(kcek_type);
|
||||||
|
kcek_type.to_string()
|
||||||
|
} else if parent_parent_key.unwrap_or_default() == "quests" {
|
||||||
|
QUEST_TYPE[text.as_str()].to_string()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"item_unlock_value" => {
|
||||||
|
if ICON_FORMS.contains_key(text.as_str()) {
|
||||||
|
ICON_FORMS[text.as_str()].to_string()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"difficulty" => DIFFICULTY[text.as_str()].to_string(),
|
||||||
|
"level_type" => LEVEL_TYPE[text.as_str()].to_string(),
|
||||||
|
"level_length" => LEVEL_LENGTH[text.as_str()].to_string(),
|
||||||
|
"epic_rating" => EPIC_RATING[text.as_str()].to_string(),
|
||||||
|
"item_type" => ITEMS[text.as_str()].to_string(),
|
||||||
|
"description" => decode_base64(&text),
|
||||||
|
"level_data" => {
|
||||||
|
if TRUNCATE_LEVEL_DATA {
|
||||||
|
"truncated to minimize size".to_string()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
text
|
||||||
|
}
|
||||||
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
|
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
|
||||||
_ => panic!("Failed to read text"),
|
_ => panic!("Failed to read text"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user