└── addons └── ForgeJSONGD ├── ForgeJSONGD.gd.uid ├── ForgeJSONGDBase.gd.uid ├── ForgeJSONGDHelper.gd.uid ├── AssetIcon.jpg ├── AssetIcon.jpg.import ├── ForgeJSONGDHelper.gd ├── ForgeJSONGDBase.gd └── ForgeJSONGD.gd /addons/ForgeJSONGD/ForgeJSONGD.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ch1r4c54yetfx 2 | -------------------------------------------------------------------------------- /addons/ForgeJSONGD/ForgeJSONGDBase.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cinw4o6lgngkk 2 | -------------------------------------------------------------------------------- /addons/ForgeJSONGD/ForgeJSONGDHelper.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cir145sb44cpq 2 | -------------------------------------------------------------------------------- /addons/ForgeJSONGD/AssetIcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EiTaNBaRiBoA/ForgeJSONGD/HEAD/addons/ForgeJSONGD/AssetIcon.jpg -------------------------------------------------------------------------------- /addons/ForgeJSONGD/AssetIcon.jpg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cbt51sclatky7" 6 | path="res://.godot/imported/AssetIcon.jpg-183f7a71974d1444cd2f2cd8ac0baba9.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/ForgeJSONGD/addons/ForgeJSONGD/AssetIcon.jpg" 14 | dest_files=["res://.godot/imported/AssetIcon.jpg-183f7a71974d1444cd2f2cd8ac0baba9.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/uastc_level=0 22 | compress/rdo_quality_loss=0.0 23 | compress/hdr_compression=1 24 | compress/normal_map=0 25 | compress/channel_pack=0 26 | mipmaps/generate=false 27 | mipmaps/limit=-1 28 | roughness/mode=0 29 | roughness/src_normal="" 30 | process/channel_remap/red=0 31 | process/channel_remap/green=1 32 | process/channel_remap/blue=2 33 | process/channel_remap/alpha=3 34 | process/fix_alpha_border=true 35 | process/premult_alpha=false 36 | process/normal_map_invert_y=false 37 | process/hdr_as_srgb=false 38 | process/hdr_clamp_exposure=false 39 | process/size_limit=0 40 | detect_3d/compress_to=1 41 | -------------------------------------------------------------------------------- /addons/ForgeJSONGD/ForgeJSONGDHelper.gd: -------------------------------------------------------------------------------- 1 | @abstract class_name ForgeJSONGDHelper extends ForgeJSONGDBase 2 | 3 | #region Comparison 4 | 5 | # Internal recursive function to perform the comparison. 6 | static func compare_recursive(a: Variant, b: Variant) -> Dictionary: 7 | # If the types are different, they are not equal. Return the change. 8 | if typeof(a) != typeof(b): 9 | return {"old": a, "new": b} 10 | # Handle comparison based on the type of the variables. 11 | match typeof(a): 12 | TYPE_DICTIONARY: 13 | return _compare_dictionaries(a, b) 14 | TYPE_ARRAY: 15 | return _compare_arrays(a, b) 16 | _: 17 | # For all other primitive types (int, float, bool, string, null). 18 | if a != b: 19 | return {"old": a, "new": b} 20 | else: 21 | # They are identical, so there is no difference. 22 | return {} 23 | 24 | # Compares two dictionaries. 25 | static func _compare_dictionaries(a: Dictionary, b: Dictionary) -> Dictionary: 26 | var diff: Dictionary = {} 27 | var all_keys: Array = a.keys() 28 | for key in b.keys(): 29 | if not all_keys.has(key): 30 | all_keys.append(key) 31 | for key in all_keys: 32 | var key_in_a: bool = a.has(key) 33 | var key_in_b: bool = b.has(key) 34 | 35 | if key_in_a and not key_in_b: 36 | # Key was removed in 'b'. 37 | diff.set(key, {"old": a.get(key), "new": null}) 38 | elif not key_in_a and key_in_b: 39 | # Key was added in 'b'. 40 | diff.set(key, {"old": null, "new": b.get(key)}) 41 | else: 42 | # Key exists in both, so we recurse to compare their values. 43 | var result: Dictionary = compare_recursive(a.get(key), b.get(key)) 44 | if not result.is_empty(): 45 | # If the recursive comparison found a difference, add it to our diff report. 46 | diff.set(key, result) 47 | return diff 48 | 49 | 50 | # Compares two arrays. 51 | static func _compare_arrays(a: Array, b: Array) -> Dictionary: 52 | # This correctly handles nested structures within the arrays. 53 | if JSON.stringify(a) != JSON.stringify(b): 54 | return {"old": a, "new": b} 55 | # The arrays are identical. 56 | return {} 57 | 58 | #endregion 59 | 60 | #region Operation Helpers 61 | 62 | ## Recursively applies an operation to a variant. Dispatches to dictionary/array handlers. 63 | static func apply_operation_recursively(base_var: Variant, ref_var: Variant, op_type: Operation) -> Variant: 64 | if ref_var is Dictionary and base_var is Dictionary: 65 | return _process_dictionary(base_var, ref_var, op_type) 66 | elif ref_var is Array and base_var is Array: 67 | return _process_array(base_var, ref_var, op_type) 68 | else: 69 | # Handle primitives or type mismatches 70 | return _process_primitive(base_var, ref_var, op_type) 71 | 72 | 73 | ## Handles the recursive operation logic for Dictionaries. 74 | static func _process_dictionary(base_dict: Dictionary, ref_dict: Dictionary, op_type: Operation) -> Dictionary: 75 | for key in ref_dict: 76 | if key != SCRIPT_INHERITANCE: 77 | var ref_value = ref_dict[key] 78 | 79 | if base_dict.has(key): 80 | var base_value = base_dict.get(key) 81 | var result = apply_operation_recursively(base_value, ref_value, op_type) 82 | 83 | # For remove operations, a null result signifies deletion. 84 | if result == null and (op_type == Operation.Remove or op_type == Operation.RemoveValue): 85 | base_dict.erase(key) 86 | else: 87 | base_dict.set(key, result) 88 | 89 | # If the key doesn't exist in base, add it (for relevant operations). 90 | elif op_type == Operation.Add or op_type == Operation.AddDiffer or op_type == Operation.Replace: 91 | base_dict.set(key, ref_value) 92 | 93 | return base_dict 94 | 95 | 96 | ## Handles the recursive operation logic for Arrays. 97 | static func _process_array(base_arr: Array, ref_arr: Array, op_type: Operation) -> Array: 98 | match op_type: 99 | Operation.Add, Operation.AddDiffer: 100 | for item in ref_arr: 101 | if not base_arr.has(item) || op_type == Operation.Add: 102 | base_arr.append(item) 103 | return base_arr 104 | 105 | Operation.Replace: 106 | # Replacing an array means returning the reference array. 107 | return ref_arr 108 | 109 | Operation.Remove, Operation.RemoveValue: 110 | # Filter the base array, keeping only items NOT in the reference array. 111 | var new_arr: Array = [] 112 | for item in base_arr: 113 | if not ref_arr.has(item): 114 | new_arr.append(item) 115 | return new_arr 116 | 117 | return base_arr 118 | 119 | 120 | ## Handles the operation logic for primitive values. 121 | static func _process_primitive(base_val: Variant, ref_val: Variant, op_type: Operation) -> Variant: 122 | match op_type: 123 | Operation.Add: 124 | return [base_val, ref_val] 125 | Operation.AddDiffer: 126 | return base_val if base_val == ref_val else [base_val, ref_val] 127 | Operation.Replace: 128 | return ref_val 129 | Operation.Remove: 130 | # A null return signals to the dictionary processor to erase the key. 131 | return null 132 | Operation.RemoveValue: 133 | return null if base_val == ref_val else base_val 134 | return base_val 135 | #endregion 136 | -------------------------------------------------------------------------------- /addons/ForgeJSONGD/ForgeJSONGDBase.gd: -------------------------------------------------------------------------------- 1 | @abstract class_name ForgeJSONGDBase 2 | 3 | const SCRIPT_INHERITANCE = "script_inheritance" 4 | 5 | ## If ture only exported values will be serialized or deserialized. 6 | static var only_exported_values: bool = false 7 | 8 | #region Checks and Gets 9 | 10 | ## Checks if the directory for the given file path exists, creating it if necessary. 11 | static func _check_dir(file_path: String) -> void: 12 | if !DirAccess.dir_exists_absolute(file_path.get_base_dir()): 13 | DirAccess.make_dir_absolute(file_path.get_base_dir()) 14 | 15 | 16 | ## Checks if a property should be included during serialization or deserialization. if script_type is not gdscript , then it is called from other languages 17 | static func _check_valid_property(property: Variant) -> bool: 18 | return property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE and (!only_exported_values or property.usage & PROPERTY_USAGE_STORAGE) 19 | 20 | ## Helper function to find a Script by its class name. 21 | static func _get_gdscript(hint_class: String) -> Script: 22 | for className: Dictionary in ProjectSettings.get_global_class_list(): 23 | if className.class == hint_class: 24 | return load(className.path) 25 | if ResourceLoader.exists(hint_class): 26 | return load(hint_class) 27 | return null 28 | 29 | ## Extracts the main path from a resource path (removes node path if present). 30 | static func _get_main_tres_path(path: String) -> String: 31 | var path_parts: PackedStringArray = path.split("::", true, 1) 32 | if path_parts.size() > 0: 33 | return path_parts[0] 34 | else: 35 | return path 36 | 37 | ## Get's a dictionary from a given string or dictionary if it is a path or dictionary or string 38 | static func _get_dict_from_type(json: Variant) -> Dictionary: 39 | var dict: Dictionary = {} 40 | if json is Dictionary: 41 | dict = json 42 | elif json is Object: 43 | dict = ForgeJSONGD.class_to_json(json) 44 | else: 45 | var result = JSON.parse_string(json) 46 | if result == null: 47 | #first_json is not a file json string , loading it from path 48 | dict = ForgeJSONGD.json_file_to_dict(json) 49 | else: 50 | dict = result 51 | return dict 52 | 53 | #endregion 54 | 55 | #region Converters 56 | 57 | #region Class to Json 58 | 59 | ## Helper function to recursively convert Godot arrays to JSON arrays. 60 | static func convert_array_to_json(array: Array) -> Array: 61 | var json_array: Array = [] 62 | for element: Variant in array: 63 | json_array.append(_serialize_variant(element, array.is_typed())) 64 | return json_array 65 | 66 | ## Helper function to recursively convert Godot dictionaries to JSON dictionaries. 67 | static func convert_dictionary_to_json(dictionary: Dictionary) -> Dictionary: 68 | var json_dictionary: Dictionary = {} 69 | for key: Variant in dictionary.keys(): 70 | var parsed_key: Variant = _serialize_variant(key, dictionary.is_typed()) 71 | var parsed_value: Variant = _serialize_variant(dictionary.get(key), dictionary.is_typed()) 72 | if typeof(parsed_value) == TYPE_INT: # casting to float due to json parse in godot will always parse int and enum as float 73 | json_dictionary.set(parsed_key, float(parsed_value)) 74 | else: 75 | json_dictionary.set(parsed_key, parsed_value) 76 | return json_dictionary 77 | 78 | # Converts a Godot Variant into a JSON-compatible Variant. 79 | static func _serialize_variant(variant_value: Variant, is_parent_typed: bool = false) -> Variant: 80 | if variant_value is Object: 81 | # If the parent is typed, the script path isn't needed. If untyped, it is. 82 | var specify_script = not is_parent_typed 83 | return ForgeJSONGD.class_to_json(variant_value, specify_script) 84 | elif variant_value is Array: 85 | return convert_array_to_json(variant_value) 86 | elif variant_value is Dictionary: 87 | return convert_dictionary_to_json(variant_value) 88 | elif type_string(typeof(variant_value)).begins_with("Vector"): 89 | return var_to_str(variant_value) 90 | elif variant_value is int and not is_parent_typed: 91 | # Godot's JSON.parse_string treats all numbers as floats. 92 | # To ensure type consistency on deserialization, we cast all ints to floats here. 93 | return float(variant_value) 94 | else: 95 | if typeof(variant_value) == TYPE_COLOR: 96 | return variant_value.to_html() 97 | # For all other primitive types (float, string, bool), return as is. 98 | return variant_value 99 | 100 | #endregion 101 | 102 | #region Json to Class 103 | 104 | ## Helper function to recursively convert a JSON dictionary into a target dictionary. 105 | static func _convert_json_to_dictionary(property_dict: Dictionary, json_dict: Dictionary) -> void: 106 | var key_type_script: Script = property_dict.get_typed_key_script() 107 | var value_type_script: Script = property_dict.get_typed_value_script() 108 | for json_key: Variant in json_dict: 109 | var json_value: Variant = json_dict.get(json_key) 110 | var converted_key: Variant = _convert_variant(json_key, key_type_script) 111 | var converted_value: Variant = _convert_variant(json_value, value_type_script) 112 | property_dict.set(converted_key, converted_value) 113 | 114 | ## Helper function to recursively convert a JSON array to a Godot array. 115 | static func _convert_json_to_array(json_array: Array, type: Variant = null) -> Array: 116 | var godot_array: Array = [] 117 | for element: Variant in json_array: 118 | godot_array.append(_convert_variant(element, type)) 119 | return godot_array 120 | 121 | ## Converts a single Variant from JSON into its target Godot type. 122 | static func _convert_variant(json_variant: Variant, type: Variant = null) -> Variant: 123 | var processed_variant: Variant = json_variant 124 | # Process the variant based on its actual type. 125 | if processed_variant is Dictionary or type is Object: 126 | var script: Script = null 127 | if SCRIPT_INHERITANCE in processed_variant: 128 | # Prioritize script path embedded in the JSON data. 129 | script = _get_gdscript(processed_variant.get(SCRIPT_INHERITANCE)) 130 | elif type is Script: 131 | # Fallback to the type hint from the parent array/dictionary. 132 | script = load(type.get_path()) 133 | 134 | if script != null: 135 | if processed_variant is String: 136 | return ForgeJSONGD.json_to_class(script, JSON.parse_string(processed_variant)) 137 | return ForgeJSONGD.json_to_class(script, processed_variant) 138 | else: 139 | return processed_variant 140 | elif processed_variant is Array: 141 | # Recursively call the array converter for nested arrays. 142 | return _convert_json_to_array(processed_variant) 143 | elif processed_variant is String and not processed_variant.is_empty(): 144 | # Try to convert string to a built-in Godot type (e.g., Vector2). 145 | if type != null and type is int and type == TYPE_COLOR: 146 | return Color(processed_variant) 147 | var str_var: Variant = str_to_var(processed_variant) 148 | if str_var == null: 149 | var json := JSON.new() 150 | # Handle cases where a value is a stringified JSON object/array. 151 | var error = json.parse(processed_variant) 152 | if error == OK: 153 | return json.get_data() 154 | else: 155 | return str_var 156 | # primitive types (int, float, bool, null) 157 | return processed_variant 158 | 159 | #endregion 160 | 161 | #endregion 162 | 163 | 164 | #region Json Utilties 165 | ## Defines the types of operations that can be performed on the data structures. 166 | enum Operation { 167 | Add, # Adds values. If key exists, combines them into an array. 168 | AddDiffer, # Adds or merges values only if they are different. 169 | Replace, # Replaces values in the base structure with values from the reference. 170 | Remove, # Removes keys/values present in the reference structure from the base. 171 | RemoveValue # Removes keys/values only if their values match the reference. 172 | } 173 | #endregion 174 | -------------------------------------------------------------------------------- /addons/ForgeJSONGD/ForgeJSONGD.gd: -------------------------------------------------------------------------------- 1 | @abstract class_name ForgeJSONGD extends ForgeJSONGDBase 2 | 3 | 4 | #region Class to Json 5 | 6 | 7 | ## Stores a JSON dictionary to a file, optionally with encryption. 8 | static func store_json_file(file_path: String, data: Dictionary, security_key: String = "") -> bool: 9 | _check_dir(file_path) 10 | var file: FileAccess 11 | if security_key.length() == 0: 12 | file = FileAccess.open(file_path, FileAccess.WRITE) 13 | else: 14 | file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.WRITE, security_key) 15 | if not file: 16 | printerr("Error writing to a file") 17 | return false 18 | var json_string: String = JSON.stringify(data, "\t") 19 | file.store_string(json_string) 20 | file.close() 21 | return true 22 | 23 | 24 | ## Converts a Godot class instance into a JSON string. 25 | static func class_to_json_string(_class: Object, specify_class: bool = false) -> String: 26 | return JSON.stringify(class_to_json(_class, specify_class)) 27 | 28 | 29 | ## Converts a Godot class instance into a JSON dictionary, specify_class for manual class specifying (true under inheritance). 30 | ## This is the core serialization function. 31 | static func class_to_json(_class: Object, specify_class: bool = false) -> Dictionary: 32 | var dictionary: Dictionary = {} 33 | # Store the script name for reference during deserialization if inheritance exists 34 | if specify_class: 35 | dictionary.set(SCRIPT_INHERITANCE, _class.get_script().get_global_name()) 36 | var properties: Array = _class.get_property_list() 37 | 38 | # Iterate through each property of the class 39 | for property: Dictionary in properties: 40 | var property_name: String = property.get("name") 41 | var property_type: Variant = property.get("type") 42 | 43 | # Skip the built-in 'script' property 44 | if property_name == "script": 45 | if specify_class and dictionary.get(SCRIPT_INHERITANCE).is_empty(): 46 | dictionary.set(SCRIPT_INHERITANCE, _class.get_script().resource_path) # In case the class isn't global 47 | continue 48 | var property_value: Variant = _class.get(property_name) 49 | # Only serialize properties that are exported or marked for storage 50 | if not property_name.is_empty() and _check_valid_property(property): 51 | if property_value is Array: 52 | # Recursively convert arrays to JSON 53 | dictionary.set(property_name, convert_array_to_json(property_value)) 54 | elif property_value is Dictionary: 55 | # Recursively convert dictionaries to JSON 56 | dictionary.set(property_name, convert_dictionary_to_json(property_value)) 57 | # If the property is a Resource: 58 | elif property_type == TYPE_OBJECT and property_value != null and property_value.get_property_list(): 59 | if property_value is Resource and ResourceLoader.exists(property_value.resource_path): 60 | var main_src: String = _get_main_tres_path(property_value.resource_path) 61 | if main_src.get_extension() != "tres": 62 | # Store the resource path if it's not a .tres file 63 | dictionary.set(property_name, property_value.resource_path) 64 | else: 65 | # Recursively serialize the nested resource 66 | dictionary.set(property_name, class_to_json(property_value)) 67 | else: 68 | dictionary.set(property_name, class_to_json(property_value, property.get("class_name") != property_value.get_script().get_global_name())) 69 | # Special handling for Vector types (store as strings) 70 | elif type_string(typeof(property_value)).begins_with("Vector"): 71 | dictionary[property_name] = var_to_str(property_value) 72 | elif property_type == TYPE_COLOR: 73 | # Store Color as a hex string 74 | dictionary.set(property_name, property_value.to_html()) 75 | else: 76 | # Store other basic types directly 77 | if property_type == TYPE_INT and property.get("hint") == PROPERTY_HINT_ENUM: 78 | var enum_params: String = property.get("hint_string") 79 | for enum_value: String in enum_params.split(","): 80 | enum_value = enum_value.replace(" ", "_") 81 | if enum_value.contains(":"): 82 | if property_value == (enum_value.split(":")[1]).to_int(): 83 | dictionary.set(property_name, enum_value.split(":")[0]) 84 | else: 85 | dictionary.set(property_name, enum_value) 86 | else: 87 | dictionary.set(property_name, property_value) 88 | return dictionary 89 | 90 | #endregion 91 | 92 | 93 | #region Json to Class 94 | 95 | 96 | ## Loads a JSON file and converts its contents into a Godot class instance. 97 | ## Uses the provided GDScript (castClass) as a template for the class. 98 | static func json_file_to_class(gdscript_or_instace: Variant, file_path: String, security_key: String = "") -> Object: 99 | var parsed_results = json_file_to_dict(file_path, security_key) 100 | if parsed_results.is_empty() and gdscript_or_instace is Script: 101 | return gdscript_or_instace.new() 102 | return json_to_class(gdscript_or_instace, parsed_results) 103 | 104 | 105 | ## Converts a JSON string into a Godot class instance. 106 | static func json_string_to_class(gdscript_or_instace: Variant, json_string: String) -> Object: 107 | var json: JSON = JSON.new() 108 | var parse_result: Error = json.parse(json_string) 109 | if parse_result == Error.OK: 110 | return json_to_class(gdscript_or_instace, json.data) 111 | return null 112 | 113 | 114 | ## Loads a JSON file and parses it into a Dictionary. 115 | ## Supports optional decryption using a security key. 116 | static func json_file_to_dict(file_path: String, security_key: String = "") -> Dictionary: 117 | var file: FileAccess 118 | if FileAccess.file_exists(file_path): 119 | if security_key.length() == 0: 120 | file = FileAccess.open(file_path, FileAccess.READ) 121 | else: 122 | file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.READ, security_key) 123 | if not file: 124 | printerr("Error opening file: ", file_path) 125 | return {} 126 | var parsed_results: Variant = JSON.parse_string(file.get_as_text()) 127 | file.close() 128 | if parsed_results is Dictionary or parsed_results is Array: 129 | return parsed_results 130 | return {} 131 | 132 | 133 | ## Converts a JSON dictionary into a Godot class instance. 134 | ## This is the core deserialization function. 135 | static func json_to_class(script_or_instace: Variant, json: Dictionary) -> Object: 136 | # Create an instance of the target class 137 | var _class: Variant = null 138 | var properties: Array = [] 139 | ## Passing null as a casted class 140 | if script_or_instace == null: 141 | var script_name: String = json.get(SCRIPT_INHERITANCE, null) 142 | # Looking for the script 143 | if script_name != null: 144 | var script_type: Script = _get_gdscript(script_name) 145 | if script_type != null: 146 | _class = script_type.new() as Object 147 | # Creating an class object 148 | elif script_or_instace is Script: 149 | _class = script_or_instace.new() as Object 150 | elif script_or_instace is Object: 151 | _class = script_or_instace 152 | properties = script_or_instace.get_script().get_property_list() 153 | if properties.is_empty(): 154 | if _class == null: 155 | return Object.new() 156 | properties = _class.get_property_list() 157 | # Iterate through each key-value pair in the JSON dictionary 158 | for key: String in json.keys(): 159 | var value: Variant = json.get(key) 160 | # Special handling for Vector types (stored as strings in JSON) 161 | if type_string(typeof(value)) == "String" and value.begins_with("Vector"): 162 | value = str_to_var(value) 163 | 164 | # Find the matching property in the target class 165 | for property: Dictionary in properties: 166 | var property_name: String = property.get("name") 167 | var property_type: Variant = property.get("type") 168 | # Skip the 'script' property (built-in) 169 | if property_name == "script": 170 | continue 171 | 172 | # Get the current value of the property in the class instance 173 | var property_value: Variant = _class.get(property_name) 174 | 175 | # If the property name matches the JSON key and is a script variable: 176 | if property_name == key and _check_valid_property(property): 177 | # Case 1: Property is an Object (not an array) 178 | if not property_value is Array and property_type == TYPE_OBJECT: 179 | var inner_class_path: String = "" 180 | if property_value: 181 | # If the property already holds an object, try to get its script path 182 | for inner_property: Dictionary in property_value.get_property_list(): 183 | if inner_property.has("hint_string") and inner_property.get("hint_string").contains(".gd"): 184 | inner_class_path = inner_property.get("hint_string") 185 | # Recursively deserialize nested objects 186 | _class.set(property_name, json_to_class(load(inner_class_path), value)) 187 | elif value: 188 | var script_type: Script = null 189 | # Determine the script type for the nested object 190 | if value is Dictionary and value.has(SCRIPT_INHERITANCE): 191 | script_type = _get_gdscript(value.get(SCRIPT_INHERITANCE)) 192 | else: 193 | script_type = _get_gdscript(property.get("class_name")) 194 | 195 | # If the value is a resource path, load the resource 196 | if value is String and value.is_absolute_path(): 197 | _class.set(property_name, ResourceLoader.load(_get_main_tres_path(value))) 198 | else: 199 | # Recursively deserialize nested objects 200 | _class.set(property_name, json_to_class(script_type, value)) 201 | 202 | # Case 2: Property is an Array 203 | elif property_value is Array: 204 | var arr_script: Script = null 205 | if property_value.is_typed() and property_value.get_typed_script(): 206 | arr_script = load(property_value.get_typed_script().get_path()) 207 | # Recursively convert the JSON array to a Godot array 208 | if arr_script == null: 209 | _class.get(property_name).assign(_convert_json_to_array(value, property_value.get_typed_builtin())) 210 | else: 211 | _class.get(property_name).assign(_convert_json_to_array(value, arr_script)) 212 | # Case 3: Property is a Typed Dictionary 213 | elif property_value is Dictionary: 214 | _convert_json_to_dictionary(property_value, value) 215 | # Case 4: Property is a simple type (not an object or array) 216 | else: 217 | # Special handling for Color type (stored as a hex string) 218 | if property_type == TYPE_COLOR: 219 | value = Color(value) 220 | if property_type == TYPE_INT and property.get("hint") == PROPERTY_HINT_ENUM: 221 | var enum_strs: Array = property.hint_string.split(",") 222 | var enum_value: int = 0 223 | for enum_str: String in enum_strs: 224 | if enum_str.contains(":"): 225 | var enum_keys: Array = enum_str.split(":") 226 | for i: int in enum_keys.size(): 227 | if enum_keys[i].to_lower() == value.to_lower().replace("_", " "): 228 | enum_value = int(enum_keys[i + 1]) 229 | break 230 | _class.set(property_name, int(enum_value)) 231 | elif property_type == TYPE_INT: 232 | _class.set(property_name, int(value)) 233 | else: 234 | _class.set(property_name, value) 235 | # Return the fully deserialized class instance 236 | return _class 237 | 238 | #endregion 239 | 240 | 241 | #region Json Utilties 242 | 243 | ## Checks if two jsons are equal, can recieve json string, file path , dictionary 244 | static func check_equal_jsons(first_json: Variant, second_json: Variant) -> bool: 245 | if _get_dict_from_type(first_json).hash() == _get_dict_from_type(second_json).hash(): 246 | return true 247 | return false 248 | 249 | 250 | ## Finds the differences between two JSON-like structures. 251 | ## Returns a dictionary showing the old and new values for each changed key. 252 | static func compare_jsons_diff(first_json: Variant, second_json: Variant) -> Dictionary: 253 | var first_dict := _get_dict_from_type(first_json) 254 | var second_dict := _get_dict_from_type(second_json) 255 | if first_dict.hash() == second_dict.hash(): 256 | return {} 257 | return ForgeJSONGDHelper.compare_recursive(first_dict, second_dict) 258 | 259 | 260 | ## Performs a specified operation on a JSON structure ('base_json') using another 261 | ## ('ref_json') as a reference. 262 | ## Returns the modified dictionary. 263 | static func json_operation(base_json: Variant, ref_json: Variant, operation_type: Operation) -> Dictionary: 264 | # Ensure we are working with deep copies to avoid modifying original inputs. 265 | var base_dict: Dictionary = _get_dict_from_type(base_json).duplicate(true) 266 | var ref_dict: Dictionary = _get_dict_from_type(ref_json) 267 | 268 | if base_dict.hash() == ref_dict.hash(): 269 | if operation_type == Operation.Replace: 270 | return base_dict 271 | if operation_type == Operation.Remove || operation_type == Operation.RemoveValue: 272 | return {} 273 | return ForgeJSONGDHelper.apply_operation_recursively(base_dict, ref_dict, operation_type) 274 | 275 | #endregion 276 | --------------------------------------------------------------------------------