JSON Translation

This commit is contained in:
Ahmed Al-Taiar
2024-11-09 21:21:51 -05:00
parent 6270ae6d2b
commit d692f135ac
10 changed files with 1010 additions and 40 deletions

49
Cargo.lock generated
View File

@ -548,6 +548,7 @@ dependencies = [
"base64",
"dirs",
"flate2",
"phf",
"quick-xml 0.37.0",
"rfd",
"serde",
@ -972,6 +973,48 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "pin-project-lite"
version = "0.2.15"
@ -1239,6 +1282,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "slab"
version = "0.4.9"

View File

@ -11,3 +11,4 @@ dirs = "5.0.1"
quick-xml = "0.37.0"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
phf = { version = "0.11.2", features = ["macros"] }

3
README.md Normal file
View 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

View File

@ -1,2 +1,661 @@
use phf::{phf_map, Map};
pub const TRUNCATE_LEVEL_DATA: bool = true;
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"
};

View File

@ -2,6 +2,7 @@ mod constants;
mod util;
use constants::XOR_KEY;
use serde_json::Value;
use std::process::exit;
use util::decrypt::*;
use util::file::*;
@ -10,15 +11,14 @@ use util::xml::*;
fn main() {
if let Some(data_encrypted) = get_dat_data() {
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) {
Ok(_) => {
println!("{:#?}", parse_save(&data));
println!("Decryption successful");
let parsed_data: Value = parse_save(&data);
if let Some(output_path) = prompt_output_path() {
write_decrypted_data(&data, output_path);
write_decrypted_json(&parsed_data, output_path);
}
}
Err(e) => {

83
src/util/convert.rs Normal file
View 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)
}

View File

@ -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 std::io::Read;
@ -8,7 +11,7 @@ pub fn xor_decrypt(data: &[u8], key: u8) -> Vec<u8> {
}
// 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
.iter()
.filter(|&&byte| byte != 0)
@ -29,3 +32,12 @@ pub fn gz_decompress(data: &[u8]) -> Vec<u8> {
decoder.read_to_end(&mut decompressed).unwrap();
decompressed
}
pub fn decode_base64(data: &str) -> String {
STANDARD
.decode(data)
.unwrap()
.iter()
.map(|&byte| byte as char)
.collect()
}

View File

@ -1,15 +1,20 @@
use dirs::{data_local_dir, desktop_dir};
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>> {
if let Some(path) = prompt_input_path() {
let mut file: File = File::open(path).unwrap();
let mut data: Vec<u8> = Vec::new();
file.read_to_end(&mut data).unwrap();
return Some(data);
match prompt_input_path() {
Some(path) => {
let mut file: File = File::open(path).unwrap();
let mut data: Vec<u8> = Vec::new();
file.read_to_end(&mut data).unwrap();
Some(data)
}
None => None,
}
None
}
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> {
FileDialog::new()
.set_title("Select Output Location")
.set_file_name("CCGameManager.xml")
.add_filter("Geometry Dash Decrypted Save", &["xml"])
.set_file_name("CCGameManager.json")
.add_filter("Geometry Dash Translated Save", &["json"])
.set_directory(desktop_dir().unwrap())
.save_file()
}
pub fn write_decrypted_data(data: &[u8], output_path: PathBuf) {
let mut output_file: File = File::create(output_path).unwrap();
output_file.write_all(data).unwrap()
pub fn write_decrypted_json(data: &Value, path: PathBuf) {
let output_file: File = File::create(path).expect("Failed to create file");
to_writer_pretty(output_file, data).expect("Failed to write JSON");
}

View File

@ -1,3 +1,4 @@
pub mod convert;
pub mod decrypt;
pub mod file;
pub mod xml;

View File

@ -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 serde_json::{Map, Value};
pub fn validate_xml(xml: &[u8]) -> Result<(), String> {
let xml_str: String = String::from_utf8_lossy(xml).to_string();
pub fn validate_xml(xml: &[u8]) -> Result<(), &str> {
const XML_HEADER: &[u8] = b"<?xml version=\"1.0\"?>";
if xml_str.starts_with("<?xml version=\"1.0\"?>") {
return Ok(());
if xml.starts_with(XML_HEADER) {
Ok(())
} else {
return Err("Invalid/missing XML header".to_string());
Err("Invalid/missing XML header")
}
#[allow(unreachable_code)]
Ok(())
}
pub fn parse_save(data: &[u8]) -> Value {
let mut reader: Reader<&[u8]> = Reader::from_reader(data);
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 current_key: Option<String> = None;
let mut current_kcek: Option<&str> = None;
loop {
match reader.read_event_into(buf) {
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
Ok(Event::Eof) => break,
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" => {
if let Some(key) = current_key.take() {
let value = read_text(reader, buf);
map.insert(key, Value::String(value));
let value: String = read_text(
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" => {
if let Some(key) = current_key.take() {
let nested_dict = parse_dict(reader, buf);
map.insert(key, nested_dict);
let nested_dict: Value =
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)
}
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) {
Ok(Event::Text(e)) => unescape(String::from_utf8_lossy(&e).to_string().as_str())
.expect("Failed to unescape text")
.into_owned(),
Ok(Event::Text(e)) => {
let text: String = unescape(String::from_utf8_lossy(&e).to_string().as_str())
.expect("Failed to unescape text")
.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),
_ => panic!("Failed to read text"),
}