JSON Translation
This commit is contained in:
49
Cargo.lock
generated
49
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
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 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;
|
||||
|
||||
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
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 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()
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod convert;
|
||||
pub mod decrypt;
|
||||
pub mod file;
|
||||
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 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"),
|
||||
}
|
||||
|
Reference in New Issue
Block a user