From 499a7c3536f560e851e897f105d3a95416941f91 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 02:35:42 +0200 Subject: [PATCH 01/15] added gitattributes for auto eol --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf From 259c43fa6f8519b606ebf84b310bfddfdcdaae00 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 03:52:16 +0200 Subject: [PATCH 02/15] converted code to GDScript 2.0 and added string class validation --- addons/gdscript-interfaces/Interfaces.gd | 80 ++++++++++++++++++------ 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/addons/gdscript-interfaces/Interfaces.gd b/addons/gdscript-interfaces/Interfaces.gd index 32a20e4..efd3136 100644 --- a/addons/gdscript-interfaces/Interfaces.gd +++ b/addons/gdscript-interfaces/Interfaces.gd @@ -11,17 +11,24 @@ extends Node ## @tutorial: https://github.com/nsrosenqvist/gdscript-interfaces/tree/main/addons/gdscript-interfaces#readme ## -export(bool) var runtime_validation = false -export(bool) var strict_validation = true -export(Array, String) var validate_dirs = ["res://"] +@export var runtime_validation: bool = false +# TODO: This can be removed as soon as global script names are allow as constants again. +## If this is true, a list of all interfaces is saved in memory to enable using "const implements = ['InterfaceName']" instead of preloads only. +## For big projects with lots of "class_name" scripts this should be off to safe memory (preloads have to be used in that case). +## WARNING: only works if none of the interfaces are outside of the "validate_dirs" directories. Use at your own risk. +@export var allow_string_classes: bool = false +@export var strict_validation: bool = true +@export var validate_dirs: Array[String] = ["res://"] -var _interfaces = {} -var _identifiers = {} -var _implements = {} +var _interfaces := {} +var _identifiers := {} +var _implements := {} + +var _named_classes := {} ## Validate that an entity implements an interface ## -## enitity [Object]: Any GDscript or a node with script attached +## implementation [Object]: Any GDscript or a node with script attached ## interfaces [GDScript|Array]: The interface(s) to validate against ## validate [bool]: Whether validation should run or if only the ## implements constant should be checked @@ -77,10 +84,26 @@ func implementations(objects : Array, interfaces, validate = false) -> Array: return result func _ready(): + # Load all "class_name" scripts + if allow_string_classes: + _build_class_name_cache() # Pre-validate all interfaces on game start if not runtime_validation: _validate_all_implementations() +func _build_class_name_cache() -> void: + var files = [] + for d in validate_dirs: + files.append_array(_files(d, true)) + var scripts = _filter(files, _only_scripts) + + for s in scripts: + var script = load(s) + var identifier := _get_identifier(script, true) + if identifier != "": + _named_classes[identifier] = _get_script(script) + print(_named_classes) + func _validate_all_implementations() -> void: # Get all script files var files = [] @@ -88,7 +111,7 @@ func _validate_all_implementations() -> void: for d in validate_dirs: files.append_array(_files(d, true)) - var scripts = _filter(files, funcref(self, "_only_scripts")) + var scripts = _filter(files, _only_scripts) # Validate all scripts that has the constant "implements" for s in scripts: @@ -104,9 +127,8 @@ func _only_scripts(file : String) -> bool: func _files(path : String, recursive = false) -> Array: var result = [] - var dir = Directory.new() - - if dir.open(path) == OK: + var dir = DirAccess.open(path) + if dir: dir.list_dir_begin() var file_name = dir.get_next() @@ -114,9 +136,9 @@ func _files(path : String, recursive = false) -> Array: if not (file_name == "." or file_name == ".."): if dir.current_is_dir(): if recursive: - result.append_array(_files(path.plus_file(file_name))) + result.append_array(_files(path.path_join(file_name))) else: - result.append(path.plus_file(file_name)) + result.append(path.path_join(file_name)) file_name = dir.get_next() else: @@ -124,11 +146,11 @@ func _files(path : String, recursive = false) -> Array: return result -func _filter(objects : Array, function : FuncRef) -> Array: +func _filter(objects : Array, function : Callable) -> Array: var result = [] for object in objects: - if function.call_func(object): + if function.call(object): result.append(object) return result @@ -153,15 +175,21 @@ func _get_implements(implementation) -> Array: if _implements.has(lookup): return _implements[lookup] - + # Get implements constant from script var consts : Dictionary = script.get_script_constant_map() - _implements[lookup] = consts["implements"] if consts.has("implements") else [] + if consts.has("implements"): + var interfaces: Array[GDScript] = [] + for interface in consts["implements"]: + interfaces.append(_named_classes[interface] if (interface is String) else interface) + _implements[lookup] = interfaces + else: + _implements[lookup] = [] return _implements[lookup] -func _get_identifier(implementation) -> String: +func _get_identifier(implementation, strict = false) -> String: var script : GDScript = _get_script(implementation) var lookup : String = str(script) @@ -177,12 +205,24 @@ func _get_identifier(implementation) -> String: if result: _identifiers[lookup] = result.get_string().substr(11) else: - _identifiers[lookup] = script.resource_path + _identifiers[lookup] = "" if strict else script.resource_path return _identifiers[lookup] return "Unknown" + +func _get_interface_identifier(implementation) -> String: + var script : GDScript = _get_script(implementation) + if script.has_source_code(): + var regex: RegEx = RegEx.new() + regex.compile("^\\s*#\\s*[iI]nterface$") + var result = regex.search(script.source_code) + if result: + return _get_identifier(implementation, true) + + return "" + func _validate_implementation(script : GDScript, interface : GDScript, assert_on_fail = false) -> bool: var implementation_id = _get_identifier(script) var interface_id = _get_identifier(interface) @@ -219,6 +259,8 @@ func _validate_implementation(script : GDScript, interface : GDScript, assert_on var props = _column(script.get_script_property_list(), "name") for p in _column(interface.get_script_property_list(), "name"): + if (p.ends_with(".gd")): + continue if not (p in props): if assert_on_fail: assert(false, implementation_id + ' does not implement the property "'+p+'" on the interface ' + interface_id) From 9f1062234f3fa9aeec69af2c1a4cab71f607b276 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 04:19:52 +0200 Subject: [PATCH 03/15] added stricter type checks for adding interfaces --- addons/gdscript-interfaces/Interfaces.gd | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/addons/gdscript-interfaces/Interfaces.gd b/addons/gdscript-interfaces/Interfaces.gd index efd3136..cbfe987 100644 --- a/addons/gdscript-interfaces/Interfaces.gd +++ b/addons/gdscript-interfaces/Interfaces.gd @@ -99,7 +99,7 @@ func _build_class_name_cache() -> void: for s in scripts: var script = load(s) - var identifier := _get_identifier(script, true) + var identifier := _get_interface_identifier(script) if identifier != "": _named_classes[identifier] = _get_script(script) print(_named_classes) @@ -182,7 +182,12 @@ func _get_implements(implementation) -> Array: if consts.has("implements"): var interfaces: Array[GDScript] = [] for interface in consts["implements"]: - interfaces.append(_named_classes[interface] if (interface is String) else interface) + if interface is String: + if not allow_string_classes: + assert(false, "Cannot use string type in implements as 'allow_string_classes' is false. ('%s' in %s)" % [interface, lookup]) + interfaces.append(_named_classes[interface]) + elif interface is GDScript: + interfaces.append(interface) _implements[lookup] = interfaces else: _implements[lookup] = [] @@ -216,7 +221,7 @@ func _get_interface_identifier(implementation) -> String: if script.has_source_code(): var regex: RegEx = RegEx.new() - regex.compile("^\\s*#\\s*[iI]nterface$") + regex.compile("#\\s*[iI]nterface\\n") var result = regex.search(script.source_code) if result: return _get_identifier(implementation, true) From 272bbd70b0174cfc65b56472dda8e95aa9d854d8 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 04:23:21 +0200 Subject: [PATCH 04/15] updated readme to match changes --- addons/gdscript-interfaces/Interfaces.gd | 2 +- addons/gdscript-interfaces/README.md | 38 ++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/addons/gdscript-interfaces/Interfaces.gd b/addons/gdscript-interfaces/Interfaces.gd index cbfe987..131322f 100644 --- a/addons/gdscript-interfaces/Interfaces.gd +++ b/addons/gdscript-interfaces/Interfaces.gd @@ -15,7 +15,7 @@ extends Node # TODO: This can be removed as soon as global script names are allow as constants again. ## If this is true, a list of all interfaces is saved in memory to enable using "const implements = ['InterfaceName']" instead of preloads only. ## For big projects with lots of "class_name" scripts this should be off to safe memory (preloads have to be used in that case). -## WARNING: only works if none of the interfaces are outside of the "validate_dirs" directories. Use at your own risk. +## WARNING: only works if all of the interfaces are inside of the "validate_dirs" directories. @export var allow_string_classes: bool = false @export var strict_validation: bool = true @export var validate_dirs: Array[String] = ["res://"] diff --git a/addons/gdscript-interfaces/README.md b/addons/gdscript-interfaces/README.md index df2a0b4..c061aaa 100644 --- a/addons/gdscript-interfaces/README.md +++ b/addons/gdscript-interfaces/README.md @@ -10,16 +10,35 @@ Improvements and suggestions are very welcome. If you have the time and know-how ## Usage -Add the `addons/Interfaces.gd` script to your project's autoloaded singletons. Any script which "implements" an interface should have a property constant set called `implements`, which is an Array listing all its implementations (as GDScript references, so simply write the class name). +Add the `addons/Interfaces.gd` script to your project's autoloaded singletons. +Any script which "implements" an interface should have a property constant set called `implements`, which is an Array listing all its implementations. + +Previously this could be done as GDScript references (simply writing the ``class_name``). +Unfortunately Godot 4.0+ no longer allows GDScript globals (the ``class_name``) to be used as const directly. +This means you need to use prelaod instead, as this precompiles the script to make it a const. + +One using preload instead of names: +```GDScript +const implements = [ + preload("path/to/interface/can_take_damage.gd"), + preload("path/to/interface/can_heal.gd") +] +``` +As a workaround there is now a second way of defining implements easily, by enabling the new experimental feature ``allow_string_classes`` in the autoload script. +This enables using strings in the implements array like this: ```GDScript const implements = [ - CanTakeDamage, - CanHeal + "CanTakeDamage", + "CanHeal" ] ``` +However the downside is that the autoload keeps a list of all scripts that define a ``class_name``. +This list is created by looping over all scripts in the project and keeping them in a dictionary. +Therefore this option should not be used in bigger projects with lots of scripts, as it would slow down startup and eat up lots of memory. Then, wherever you wish to check for an implementation, you call the function `implements` on the singleton (the function can either take a single GDScript reference or an array of GDScripts). +Here you can use the ``class_name`` again! ```GDScript func _on_body_entered(body : Node): @@ -34,8 +53,10 @@ var destroyable = Interfaces.implementations([obj1, obj2, obj3], CanTakeDamage) ``` An interface is just a GDScript, defined with a `class_name`, that details the properties, signals, and methods that the implementations must provide. +If you want to use ``allow_string_classes`` the ``# Interface`` or ``# interface`` needs to be present. Otherwise the script is not added to the cache. ```GDScript +# Interface class_name CanTakeDamage extends Object var required @@ -52,15 +73,20 @@ Since GDScript doesn't provide introspection, the validation can only take the e By default, the script validates all found GDScripts in the project when the application is loaded, since this mimics the expected behavior from other languages most closely. However, a few options may be tweaked to change this behavior (these are properties on the singleton). -#### export(bool) var runtime_validation = false +#### @export var runtime_validation: bool = false This toggles whether all implementations should be validated immediately upon load, or if they should first be validated when they're tested against. If you have a lot of classes that may not always be loaded in a play session then this might be preferable for performance reasons, but it introduces the risk of never discovering incomplete implementations. -#### export(Array, String) var validate_dirs = ["res://"] +#### @export var allow_string_classes: bool = false + +If this is true, a list of all interfaces is saved in memory to enable using "const implements = ['InterfaceName']" instead of preloads only. +For big projects with lots of "class_name" scripts this should be off to safe memory (preloads have to be used in that case). + +#### @export var validate_dirs: Array[String] = ["res://"] This option sets what directories the library should scan for classes that implements interfaces in. By default it's set to the project root, but it should preferably be changed to something more specific like "res://src/", or "res://src/contracts/". The option has no effect if the library is configured to only do runtime validation. -#### export(bool) var strict_validation = true +#### @export var strict_validation: bool = true If strict validation is off, the `implements` method will only check if an entity has the provided interfaces in its `implements` constant. This may be preferable if proper validation turns out to incur a significant performance penalty (I haven't tested this system on larger projects). However, each check are usually only run once, since the results of validations are cached. Note that disabling strict validation pretty much removes the benefits of having interfaces in the first place. From e893283c47971deddc7907779b7fa0ba92672096 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 04:33:16 +0200 Subject: [PATCH 05/15] made into a proper plugin that adds the autoload automatically --- addons/gdscript-interfaces/gdscript-interfaces.gd | 11 +++++++++++ addons/gdscript-interfaces/plugin.cfg | 8 ++++++++ 2 files changed, 19 insertions(+) create mode 100644 addons/gdscript-interfaces/gdscript-interfaces.gd create mode 100644 addons/gdscript-interfaces/plugin.cfg diff --git a/addons/gdscript-interfaces/gdscript-interfaces.gd b/addons/gdscript-interfaces/gdscript-interfaces.gd new file mode 100644 index 0000000..a628f70 --- /dev/null +++ b/addons/gdscript-interfaces/gdscript-interfaces.gd @@ -0,0 +1,11 @@ +@tool +extends EditorPlugin + +const AUTOLOAD_NAME = "Interfaces" + +func _enter_tree() -> void: + add_autoload_singleton(AUTOLOAD_NAME, "res://addons/gdscript-interfaces/Interfaces.gd") + + +func _exit_tree() -> void: + remove_autoload_singleton(AUTOLOAD_NAME) diff --git a/addons/gdscript-interfaces/plugin.cfg b/addons/gdscript-interfaces/plugin.cfg new file mode 100644 index 0000000..6054a4f --- /dev/null +++ b/addons/gdscript-interfaces/plugin.cfg @@ -0,0 +1,8 @@ +[plugin] + +name="GDScript Interfaces" +description="Adds a very basic implementation of interfaces to GDScript. +Only validates at runtime." +author="nsrosenqvist, Mastermori" +version="2.0.0" +script="gdscript-interfaces.gd" From 20d92ab9a33350539d3292fe0480d9aa082cc6d5 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 04:33:23 +0200 Subject: [PATCH 06/15] added example code --- example/can_take_damage.gd | 10 ++++++++++ example/killable_object.gd | 15 +++++++++++++++ example/test_scene.tscn | 6 ++++++ 3 files changed, 31 insertions(+) create mode 100644 example/can_take_damage.gd create mode 100644 example/killable_object.gd create mode 100644 example/test_scene.tscn diff --git a/example/can_take_damage.gd b/example/can_take_damage.gd new file mode 100644 index 0000000..759e44d --- /dev/null +++ b/example/can_take_damage.gd @@ -0,0 +1,10 @@ +# Interface +class_name CanTakeDamage +extends Object + +var tester + +signal foobar + +func deal_damage(): + pass diff --git a/example/killable_object.gd b/example/killable_object.gd new file mode 100644 index 0000000..a21d5fe --- /dev/null +++ b/example/killable_object.gd @@ -0,0 +1,15 @@ +class_name KillableObject extends Node + +const implements = [preload("res://example/can_take_damage.gd")] +#const implements = ["CanTakeDamage"] + + +var tester + +signal foobar + +func deal_damage() -> void: + pass + +func _ready() -> void: + print(Interfaces.implements(self, CanTakeDamage)) diff --git a/example/test_scene.tscn b/example/test_scene.tscn new file mode 100644 index 0000000..6fa2fb7 --- /dev/null +++ b/example/test_scene.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://cl36mbv4b32tt"] + +[ext_resource type="Script" path="res://example/killable_object.gd" id="1_el8tc"] + +[node name="TestScene" type="Node2D"] +script = ExtResource("1_el8tc") From b517e4f193d570aeb4a2f193c31026a9ff05a3f4 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 04:39:05 +0200 Subject: [PATCH 07/15] moved readme and added project file to make testing example easier --- .gitignore | 5 +--- .../README.md => README.md | 0 project.godot | 23 +++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) rename addons/gdscript-interfaces/README.md => README.md (100%) create mode 100644 project.godot diff --git a/.gitignore b/.gitignore index 5588062..b5cb4e1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,4 @@ export_presets.cfg *.import # Imported translations (automatically generated from CSV files) -*.translation - -# Ignore project in addon repo -project.godot +*.translation \ No newline at end of file diff --git a/addons/gdscript-interfaces/README.md b/README.md similarity index 100% rename from addons/gdscript-interfaces/README.md rename to README.md diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..9a24ad6 --- /dev/null +++ b/project.godot @@ -0,0 +1,23 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Gdscript-interfaces" +config/features=PackedStringArray("4.2", "Forward Plus") +config/icon="res://icon.svg" + +[autoload] + +Interfaces="*res://addons/gdscript-interfaces/Interfaces.gd" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/gdscript-interfaces/plugin.cfg") From 428afc025a16734f9145c4153fbac45d6ee69e2c Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 14:08:21 +0200 Subject: [PATCH 08/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c061aaa..5dcc333 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Previously this could be done as GDScript references (simply writing the ``class Unfortunately Godot 4.0+ no longer allows GDScript globals (the ``class_name``) to be used as const directly. This means you need to use prelaod instead, as this precompiles the script to make it a const. -One using preload instead of names: +Using preload your ``implements`` statements will look like this. ```GDScript const implements = [ preload("path/to/interface/can_take_damage.gd"), From 2b637403b2b7100d35f9ecd3b528eaf18d26df34 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 14:10:39 +0200 Subject: [PATCH 09/15] Swapped readme on github --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index a33540b..b43bf86 120000 --- a/.github/README.md +++ b/.github/README.md @@ -1 +1 @@ -../addons/gdscript-interfaces/README.md \ No newline at end of file +README.md From 2658d70af3cb82b9d0d5c6f43781c5f7f2cb48a2 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 14:11:47 +0200 Subject: [PATCH 10/15] made proper readme appear on github --- .github/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 120000 .github/README.md diff --git a/.github/README.md b/.github/README.md deleted file mode 120000 index a33540b..0000000 --- a/.github/README.md +++ /dev/null @@ -1 +0,0 @@ -../addons/gdscript-interfaces/README.md \ No newline at end of file From a91c579ec2dc4d55e2fd5b89c7641e7a1eabb9d0 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 21:26:18 +0200 Subject: [PATCH 11/15] added better way to use string implements --- .../{Interfaces.gd => interfaces_2.gd} | 46 ++++--------------- example/can_take_damage.gd | 4 +- example/killable_object.gd | 4 +- project.godot | 2 +- 4 files changed, 14 insertions(+), 42 deletions(-) rename addons/gdscript-interfaces/{Interfaces.gd => interfaces_2.gd} (84%) diff --git a/addons/gdscript-interfaces/Interfaces.gd b/addons/gdscript-interfaces/interfaces_2.gd similarity index 84% rename from addons/gdscript-interfaces/Interfaces.gd rename to addons/gdscript-interfaces/interfaces_2.gd index 131322f..664f41f 100644 --- a/addons/gdscript-interfaces/Interfaces.gd +++ b/addons/gdscript-interfaces/interfaces_2.gd @@ -12,11 +12,7 @@ extends Node ## @export var runtime_validation: bool = false -# TODO: This can be removed as soon as global script names are allow as constants again. -## If this is true, a list of all interfaces is saved in memory to enable using "const implements = ['InterfaceName']" instead of preloads only. -## For big projects with lots of "class_name" scripts this should be off to safe memory (preloads have to be used in that case). -## WARNING: only works if all of the interfaces are inside of the "validate_dirs" directories. -@export var allow_string_classes: bool = false +@export var allow_string_classes: bool = true @export var strict_validation: bool = true @export var validate_dirs: Array[String] = ["res://"] @@ -24,8 +20,6 @@ var _interfaces := {} var _identifiers := {} var _implements := {} -var _named_classes := {} - ## Validate that an entity implements an interface ## ## implementation [Object]: Any GDscript or a node with script attached @@ -84,26 +78,10 @@ func implementations(objects : Array, interfaces, validate = false) -> Array: return result func _ready(): - # Load all "class_name" scripts - if allow_string_classes: - _build_class_name_cache() # Pre-validate all interfaces on game start if not runtime_validation: _validate_all_implementations() -func _build_class_name_cache() -> void: - var files = [] - for d in validate_dirs: - files.append_array(_files(d, true)) - var scripts = _filter(files, _only_scripts) - - for s in scripts: - var script = load(s) - var identifier := _get_interface_identifier(script) - if identifier != "": - _named_classes[identifier] = _get_script(script) - print(_named_classes) - func _validate_all_implementations() -> void: # Get all script files var files = [] @@ -185,7 +163,7 @@ func _get_implements(implementation) -> Array: if interface is String: if not allow_string_classes: assert(false, "Cannot use string type in implements as 'allow_string_classes' is false. ('%s' in %s)" % [interface, lookup]) - interfaces.append(_named_classes[interface]) + interfaces.append(_get_interface_script(interface)) elif interface is GDScript: interfaces.append(interface) _implements[lookup] = interfaces @@ -194,6 +172,14 @@ func _get_implements(implementation) -> Array: return _implements[lookup] +func _get_interface_script(interface_name): + var script = GDScript.new() + script.set_source_code("func eval(): return " + interface_name) + script.reload() + var ref = RefCounted.new() + ref.set_script(script) + return ref.eval() + func _get_identifier(implementation, strict = false) -> String: var script : GDScript = _get_script(implementation) var lookup : String = str(script) @@ -216,18 +202,6 @@ func _get_identifier(implementation, strict = false) -> String: return "Unknown" -func _get_interface_identifier(implementation) -> String: - var script : GDScript = _get_script(implementation) - - if script.has_source_code(): - var regex: RegEx = RegEx.new() - regex.compile("#\\s*[iI]nterface\\n") - var result = regex.search(script.source_code) - if result: - return _get_identifier(implementation, true) - - return "" - func _validate_implementation(script : GDScript, interface : GDScript, assert_on_fail = false) -> bool: var implementation_id = _get_identifier(script) var interface_id = _get_identifier(interface) diff --git a/example/can_take_damage.gd b/example/can_take_damage.gd index 759e44d..d129df3 100644 --- a/example/can_take_damage.gd +++ b/example/can_take_damage.gd @@ -1,6 +1,4 @@ -# Interface -class_name CanTakeDamage -extends Object +class_name CanTakeDamage extends Object var tester diff --git a/example/killable_object.gd b/example/killable_object.gd index a21d5fe..3918fc2 100644 --- a/example/killable_object.gd +++ b/example/killable_object.gd @@ -1,7 +1,7 @@ class_name KillableObject extends Node -const implements = [preload("res://example/can_take_damage.gd")] -#const implements = ["CanTakeDamage"] +#const implements = [preload("res://example/can_take_damage.gd")] +const implements = ["CanTakeDamage"] var tester diff --git a/project.godot b/project.godot index 9a24ad6..5e323a8 100644 --- a/project.godot +++ b/project.godot @@ -16,7 +16,7 @@ config/icon="res://icon.svg" [autoload] -Interfaces="*res://addons/gdscript-interfaces/Interfaces.gd" +Interfaces="*res://addons/gdscript-interfaces/interfaces_2.gd" [editor_plugins] From eb17693f0051349d3c0bec54563fb5771ac2831c Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 21:26:42 +0200 Subject: [PATCH 12/15] renamed to lowercase interfaces --- addons/gdscript-interfaces/{interfaces_2.gd => interfaces.gd} | 0 project.godot | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename addons/gdscript-interfaces/{interfaces_2.gd => interfaces.gd} (100%) diff --git a/addons/gdscript-interfaces/interfaces_2.gd b/addons/gdscript-interfaces/interfaces.gd similarity index 100% rename from addons/gdscript-interfaces/interfaces_2.gd rename to addons/gdscript-interfaces/interfaces.gd diff --git a/project.godot b/project.godot index 5e323a8..e1574d3 100644 --- a/project.godot +++ b/project.godot @@ -16,7 +16,7 @@ config/icon="res://icon.svg" [autoload] -Interfaces="*res://addons/gdscript-interfaces/interfaces_2.gd" +Interfaces="*res://addons/gdscript-interfaces/interfaces.gd" [editor_plugins] From f0c1c9ca5a632d87cb3c82b050e0751b6fcb593e Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 21:28:36 +0200 Subject: [PATCH 13/15] disabled string classes by default --- addons/gdscript-interfaces/interfaces.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/gdscript-interfaces/interfaces.gd b/addons/gdscript-interfaces/interfaces.gd index 664f41f..dd4a260 100644 --- a/addons/gdscript-interfaces/interfaces.gd +++ b/addons/gdscript-interfaces/interfaces.gd @@ -12,7 +12,7 @@ extends Node ## @export var runtime_validation: bool = false -@export var allow_string_classes: bool = true +@export var allow_string_classes: bool = false @export var strict_validation: bool = true @export var validate_dirs: Array[String] = ["res://"] From a603ce6ef5eedb16d67eea8d9ce624883b858988 Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 21:37:12 +0200 Subject: [PATCH 14/15] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5dcc333..8635ebe 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Improvements and suggestions are very welcome. If you have the time and know-how ## Usage -Add the `addons/Interfaces.gd` script to your project's autoloaded singletons. +Add the plugin to your `addons` folder or install it via the asset library (COMING SOON). Then enable it in via Project > Project Settings > Plugins. Any script which "implements" an interface should have a property constant set called `implements`, which is an Array listing all its implementations. Previously this could be done as GDScript references (simply writing the ``class_name``). -Unfortunately Godot 4.0+ no longer allows GDScript globals (the ``class_name``) to be used as const directly. +Unfortunately Godot 4.0+ no longer allows GDScript globals (the ``class_name``) to be used as consts directly. This means you need to use prelaod instead, as this precompiles the script to make it a const. Using preload your ``implements`` statements will look like this. @@ -25,7 +25,7 @@ const implements = [ preload("path/to/interface/can_heal.gd") ] ``` -As a workaround there is now a second way of defining implements easily, by enabling the new experimental feature ``allow_string_classes`` in the autoload script. +As a workaround there is now a second way of defining implements easily, by enabling the experimental feature ``allow_string_classes`` in the autoload script `addons/gdscript-interfaces/interfaces.gd`. This enables using strings in the implements array like this: ```GDScript const implements = [ @@ -33,9 +33,9 @@ const implements = [ "CanHeal" ] ``` -However the downside is that the autoload keeps a list of all scripts that define a ``class_name``. -This list is created by looping over all scripts in the project and keeping them in a dictionary. -Therefore this option should not be used in bigger projects with lots of scripts, as it would slow down startup and eat up lots of memory. +This feature should be used with caution, as it runs the interface names as gdscript code arbitrarily. This should not (as interface names can't be changed at runtime), but could enable some form of arbitrary code execution exploit. +_The code in question can be found in line 175-181 of `interfaces.gd` if you are interested_. +
You have been warned. Then, wherever you wish to check for an implementation, you call the function `implements` on the singleton (the function can either take a single GDScript reference or an array of GDScripts). Here you can use the ``class_name`` again! From 2dfda3f9f618a87af029bf387756cdc01410230f Mon Sep 17 00:00:00 2001 From: Mastermori Date: Sun, 24 Sep 2023 21:38:08 +0200 Subject: [PATCH 15/15] used proper implements array in example --- example/killable_object.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/killable_object.gd b/example/killable_object.gd index 3918fc2..a21d5fe 100644 --- a/example/killable_object.gd +++ b/example/killable_object.gd @@ -1,7 +1,7 @@ class_name KillableObject extends Node -#const implements = [preload("res://example/can_take_damage.gd")] -const implements = ["CanTakeDamage"] +const implements = [preload("res://example/can_take_damage.gd")] +#const implements = ["CanTakeDamage"] var tester