├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── addons └── custom_resource │ ├── json_resource.gd │ ├── json_resource_loader.gd │ ├── json_resource_saver.gd │ ├── plain_text_resource.gd │ ├── plain_text_resource_loader.gd │ ├── plain_text_resource_saver.gd │ ├── plugin.cfg │ └── plugin_script.gd ├── default_env.tres ├── icon.png ├── icon.png.import ├── project.godot ├── sample.json └── test.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot-specific ignores 2 | .import/ 3 | export.cfg 4 | export_presets.cfg 5 | 6 | # Imported translations (automatically generated from CSV files) 7 | *.translation 8 | 9 | # Mono-specific ignores 10 | .mono/ 11 | data_*/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot CustomResource 🤖 2 | An useful collection of custom resources that supports custom formats. Just download it and save it in your project, let the editor do the rest. 3 | 4 | ## Currently supported formats 📋 5 | - Plain text (files that ends with `txt` extension) 6 | - JSON (files that ends with `json` extension. _Because people loves it for some reason_.) 7 | 8 | ## How to install ✏️ 9 | Download or clone the repository, copy the addons folder to your project. 10 | 11 | ## FAQ ❓ 12 | 13 | ### - I want to install just an specific format 14 | Copy the files related to your specific format only. No more, no less. 15 | 16 | I. E: You want only `json` format. Copy just `json_resource`, `json_resource_loader` and `json_resource_saver`. 17 | 18 | ### - Is there a way to make my own custom formats with my own custom extensions for my custom resources? 19 | Yes! 20 | 21 | See `plain_text_resource` related files for a detailed explanation about what does each file. 22 | 23 | ### - I want a custom format! 24 | Do you want to contribute to this repository with that custom format? See contributing 📚 25 | 26 | Do you want us to make your custom format? Open an issue 🦆 27 | 28 | ## Contributing 29 | Want to help? I'm glad that you want to. 30 | 31 | ### - Opening Pull Request 32 | Just open a PR with your contribution, there's no special requeriment. 33 | 34 | ### - Opening Issues 35 | Just open the issue and mention your issue. Include your godot version number and your OS details. 36 | 37 | ### - Sharing 38 | If you share this repository or things that you have made using this as base/inspiration/anything, let me know! I'm @anidemdex on twitter. 39 | 40 | I'm always happy to see people using things that I do. 41 | 42 | ## License 43 | This repository uses MIT license. See details on [LICENSE](LICENSE.md) file 44 | -------------------------------------------------------------------------------- /addons/custom_resource/json_resource.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | class_name JSONResource 4 | 5 | var _data:Dictionary = {} 6 | 7 | func _init() -> void: 8 | _data = {} 9 | 10 | 11 | func _get_recursive(property:String, d:Dictionary): 12 | if property in d: 13 | return d.get(property) 14 | 15 | var p := property.split("/", false, 1) 16 | for p_idx in p.size(): 17 | d = d.get(p[p_idx], {}) 18 | if p_idx+1 >= p.size(): 19 | break 20 | return _get_recursive(p[p_idx + 1], d) 21 | 22 | 23 | func _set_recursive(property:String, value, d:Dictionary) -> void: 24 | if property.ends_with("/"): 25 | return 26 | 27 | if not property: 28 | return 29 | 30 | var p_names:Array = property.split("/") 31 | var p_idx:int = 0 32 | var d_pointer:Dictionary = d 33 | while p_idx < p_names.size(): 34 | var p_name:String = p_names[p_idx] 35 | 36 | if not d_pointer.has(p_name): 37 | d_pointer[p_name] = {} 38 | 39 | if p_idx == p_names.size() - 1: 40 | d_pointer[p_name] = value 41 | break 42 | 43 | d_pointer = d_pointer[p_name] 44 | p_idx += 1 45 | 46 | 47 | func _get(property: String): 48 | if not (property in ClassDB.class_get_property_list("Resource")): 49 | return _get_recursive(property, _data) 50 | 51 | 52 | func _set(property: String, value) -> bool: 53 | # Avoid setting built-in properties 54 | if not (property in ClassDB.class_get_property_list("Resource")): 55 | _set_recursive(property, value, _data) 56 | return true 57 | 58 | return false 59 | 60 | 61 | func get_data() -> Dictionary: 62 | return _data.duplicate(true) 63 | 64 | 65 | func set_data(value:Dictionary) -> void: 66 | _data = value 67 | emit_changed() 68 | property_list_changed_notify() 69 | 70 | 71 | func _get_property_list() -> Array: 72 | var p := [] 73 | p.append({"name":"_data", "type":TYPE_DICTIONARY, "usage":PROPERTY_USAGE_NOEDITOR}) 74 | 75 | p = _create_property_list(_data, p) 76 | return p 77 | 78 | 79 | func _create_property_list(from:Dictionary, _p:Array, prefix:String = "") -> Array: 80 | for key in from: 81 | var property := {"name":prefix+str(key), "type":typeof(from[key]), "usage":PROPERTY_USAGE_EDITOR|PROPERTY_USAGE_SCRIPT_VARIABLE} 82 | 83 | if typeof(from[key]) == TYPE_DICTIONARY: 84 | _create_property_list(from[key], _p, key+"/") 85 | else: 86 | _p.append(property) 87 | return _p 88 | -------------------------------------------------------------------------------- /addons/custom_resource/json_resource_loader.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ResourceFormatLoader 3 | class_name JSONResourceLoader 4 | 5 | const _JSONResource = preload("res://addons/custom_resource/json_resource.gd") 6 | 7 | func get_recognized_extensions() -> PoolStringArray: 8 | return PoolStringArray(["json"]) 9 | 10 | 11 | func get_resource_type(path: String) -> String: 12 | var ext = path.get_extension().to_lower() 13 | if ext == "json": 14 | return "Resource" 15 | 16 | return "" 17 | 18 | 19 | func handles_type(typename: String) -> bool: 20 | # I'll give you a hand for custom resources... use this snipet and that's it ;) 21 | return ClassDB.is_parent_class(typename, "Resource") 22 | 23 | 24 | func load(path: String, original_path: String): 25 | var file := File.new() 26 | 27 | var err:int 28 | 29 | err = file.open(path, File.READ) 30 | if err != OK: 31 | push_error("For some reason, loading JSON resource failed with error code: %s"%err) 32 | return err 33 | 34 | var json_data := JSON.parse(file.get_as_text()) 35 | 36 | if json_data.error: 37 | push_error("Failed parsing JSON file: [%s at line %s]" % [json_data.error_string, json_data.error_line]) 38 | return json_data.error 39 | 40 | var res := _JSONResource.new() 41 | res.set_data(json_data.result) 42 | 43 | file.close() 44 | # Everything went well, and you parsed your file data into your resource. Life is good, return it 45 | return res 46 | -------------------------------------------------------------------------------- /addons/custom_resource/json_resource_saver.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ResourceFormatSaver 3 | class_name JsonResourceSaver 4 | 5 | const _JSONResource = preload("res://addons/custom_resource/json_resource.gd") 6 | 7 | func get_recognized_extensions(resource: Resource) -> PoolStringArray: 8 | return PoolStringArray(["json"]) 9 | 10 | 11 | func recognize(resource: Resource) -> bool: 12 | # Cast instead of using "is" keyword in case is a subclass 13 | resource = resource as _JSONResource 14 | 15 | if resource: 16 | return true 17 | 18 | return false 19 | 20 | 21 | func save(path: String, resource: Resource, flags: int) -> int: 22 | var err:int 23 | var file:File = File.new() 24 | err = file.open(path, File.WRITE) 25 | 26 | if err != OK: 27 | printerr('Can\'t write file: "%s"! code: %d.' % [path, err]) 28 | return err 29 | 30 | file.store_line(JSON.print(resource.get("_data"))) 31 | file.close() 32 | return OK 33 | -------------------------------------------------------------------------------- /addons/custom_resource/plain_text_resource.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | class_name PlainTextResource 4 | 5 | # This is our resource, I'll expose the text property 6 | # That property is just an string, we don't need anything special here. 7 | 8 | export(String, MULTILINE) var text = "" setget set_text 9 | 10 | # Q: Wait, I need a setter? 11 | # A: Yes, you MUST tell the editor that you changed the resource somehow 12 | # If you forgot to do it, the resource will not be saved. Godot things. 13 | func set_text(value:String) -> void: 14 | text = value 15 | emit_changed() 16 | -------------------------------------------------------------------------------- /addons/custom_resource/plain_text_resource_loader.gd: -------------------------------------------------------------------------------- 1 | # Here is where the magic begins 2 | 3 | # A little tool to ensure that it'll work in editor (we never use it in editor anyway) 4 | tool 5 | extends ResourceFormatLoader 6 | 7 | # Docs says that it needs a class_name in order to register it in ResourceLoader 8 | # Who am I to judge the docs? 9 | class_name CustomResFormatLoader 10 | 11 | # Preload to avoid problems with project.godot 12 | const PlainTextClass = preload("res://addons/custom_resource/plain_text_resource.gd") 13 | 14 | # Dude, look at the docs, I'm not going to explain each function... 15 | # Specially when they are self explainatory... 16 | func get_recognized_extensions() -> PoolStringArray: 17 | return PoolStringArray(["txt"]) 18 | 19 | 20 | # Ok, if custom resources were a thing this would be even useful. 21 | # But is not. 22 | # I don't know what is taking longer, Godot 4 or https://github.com/godotengine/godot/pull/48201 23 | func get_resource_type(path: String) -> String: 24 | # For now, the only thing that you need to know is that this thing serves as 25 | # a filter for your resource. You verify whatever you need on the file path 26 | # or even the file itself (with a File.load) 27 | # and you return "Resource" (or whatever you're working on) if you handle it. 28 | # Everything else "" 29 | var ext = path.get_extension().to_lower() 30 | if ext == "txt": 31 | return "Resource" 32 | 33 | return "" 34 | 35 | 36 | # Ok, if custom resources were a thing this would be even useful. 37 | # But is not. (again) 38 | # You need to tell the editor if you handle __this__ type of class (wich is an string) 39 | func handles_type(typename: String) -> bool: 40 | # I'll give you a hand for custom resources... use this snipet and that's it ;) 41 | return ClassDB.is_parent_class(typename, "Resource") 42 | 43 | 44 | # And this is the one that does the magic. 45 | # Read your file, parse the data to your resource, and return the resource 46 | # Is that easy! 47 | 48 | # Even JSON can be accepted here, if you like that (I don't, but I'm not going to judge you) 49 | func load(path: String, original_path: String): 50 | var file := File.new() 51 | 52 | var err:int 53 | 54 | var res := PlainTextClass.new() 55 | 56 | err = file.open(path, File.READ) 57 | if err != OK: 58 | push_error("For some reason, loading custom resource failed with error code: %s"%err) 59 | # You has to return the error constant 60 | return err 61 | 62 | res.text = file.get_as_text() 63 | 64 | file.close() 65 | # Everything went well, and you parsed your file data into your resource. Life is good, return it 66 | return res 67 | 68 | -------------------------------------------------------------------------------- /addons/custom_resource/plain_text_resource_saver.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ResourceFormatSaver 3 | class_name CustomResFormatSaver 4 | 5 | # Preload to avoid problems with project.godot 6 | const PlainTextClass = preload("res://addons/custom_resource/plain_text_resource.gd") 7 | 8 | 9 | func get_recognized_extensions(resource: Resource) -> PoolStringArray: 10 | return PoolStringArray(["txt"]) 11 | 12 | 13 | # Here you see if that resource is the type you need. 14 | # Multiple resources can inherith from the same class 15 | # Even they can modify the structure of the class or be pretty similar to it 16 | # So you verify if that resource is the one you need here, and if it's not 17 | # You let other ResourceFormatSaver deal with it. 18 | func recognize(resource: Resource) -> bool: 19 | # Cast instead of using "is" keyword in case is a subclass 20 | resource = resource as PlainTextClass 21 | 22 | if resource: 23 | return true 24 | 25 | return false 26 | 27 | 28 | # Magic tricks 29 | # Magic tricks 30 | # Don't you love magic tricks? 31 | 32 | # Here you write the file you want to save, and save it to disk too. 33 | # For text is pretty trivial. 34 | # Binary files, custom formats and complex things are done here. 35 | func save(path: String, resource: Resource, flags: int) -> int: 36 | var err:int 37 | var file:File = File.new() 38 | err = file.open(path, File.WRITE) 39 | 40 | if err != OK: 41 | printerr('Can\'t write file: "%s"! code: %d.' % [path, err]) 42 | return err 43 | 44 | file.store_string(resource.get("text")) 45 | file.close() 46 | return OK 47 | -------------------------------------------------------------------------------- /addons/custom_resource/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="custom resource" 4 | description="Example about custom resources" 5 | author="AnidemDex" 6 | version="1.0" 7 | script="plugin_script.gd" 8 | -------------------------------------------------------------------------------- /addons/custom_resource/plugin_script.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | # Q: Why is this file empty Dex? 5 | # A: I'm not being paying enough to fill this script 6 | 7 | # jk, is not necessary 8 | 9 | const PlainTextClass = preload("res://addons/custom_resource/plain_text_resource.gd") 10 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnidemDex/Godot-CustomResource/612ce7e59d374c99cf5c39ef6ad9584a7a100706/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | process/normal_map_invert_y=false 32 | stream=false 33 | size_limit=0 34 | detect_3d=true 35 | svg/scale=1.0 36 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ { 12 | "base": "ResourceFormatLoader", 13 | "class": "CustomResFormatLoader", 14 | "language": "GDScript", 15 | "path": "res://addons/custom_resource/plain_text_resource_loader.gd" 16 | }, { 17 | "base": "ResourceFormatSaver", 18 | "class": "CustomResFormatSaver", 19 | "language": "GDScript", 20 | "path": "res://addons/custom_resource/plain_text_resource_saver.gd" 21 | }, { 22 | "base": "Resource", 23 | "class": "JSONResource", 24 | "language": "GDScript", 25 | "path": "res://addons/custom_resource/json_resource.gd" 26 | }, { 27 | "base": "ResourceFormatLoader", 28 | "class": "JSONResourceLoader", 29 | "language": "GDScript", 30 | "path": "res://addons/custom_resource/json_resource_loader.gd" 31 | }, { 32 | "base": "ResourceFormatSaver", 33 | "class": "JsonResourceSaver", 34 | "language": "GDScript", 35 | "path": "res://addons/custom_resource/json_resource_saver.gd" 36 | }, { 37 | "base": "Resource", 38 | "class": "PlainTextResource", 39 | "language": "GDScript", 40 | "path": "res://addons/custom_resource/plain_text_resource.gd" 41 | } ] 42 | _global_script_class_icons={ 43 | "CustomResFormatLoader": "", 44 | "CustomResFormatSaver": "", 45 | "JSONResource": "", 46 | "JSONResourceLoader": "", 47 | "JsonResourceSaver": "", 48 | "PlainTextResource": "" 49 | } 50 | 51 | [application] 52 | 53 | config/name="custom resource" 54 | config/icon="res://icon.png" 55 | 56 | [editor_plugins] 57 | 58 | enabled=PoolStringArray( "res://addons/custom_resource/plugin.cfg" ) 59 | 60 | [physics] 61 | 62 | common/enable_pause_aware_picking=true 63 | 64 | [rendering] 65 | 66 | quality/driver/driver_name="GLES2" 67 | vram_compression/import_etc=true 68 | vram_compression/import_etc2=false 69 | -------------------------------------------------------------------------------- /sample.json: -------------------------------------------------------------------------------- 1 | {"firstName":"Joe","lastName":"Jackson","gender":"female","age":28,"address":{"streetAddress":"101","city":"San Diego","state":"CA"},"phoneNumbers":[{"type":"home","number":"7349282382"}]} 2 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | This is awesome! You know? --------------------------------------------------------------------------------