Last active
January 12, 2026 18:16
-
-
Save RpxdYTX/ad8c86191d844254fd1317e2004e0b08 to your computer and use it in GitHub Desktop.
Godot 4 Compile Time* Generics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class_name GenericType extends Node | |
| ## A class helper to create custom generic types through a template script | |
| ## | |
| ## Example: [codeblock] | |
| ## # prevent instances without specified types with @asbtract (very important else code might crash) | |
| ## @abstract class_name Foo extends RefCounted | |
| ## | |
| ## const T := GenericType.Param | |
| ## static func with(type): GenericType.register(Foo, { T = type }) | |
| ## | |
| ## func foo(x: Foo.T) -> Foo.T: | |
| ## # duck typed, will work as long as the type argument has these functions/properties | |
| ## return T.type().some_static_function(T.type().some_static_property) | |
| ## | |
| ## # then, somewhere in the code (like in _ready or in NOTIFICATION_POST_SAVE): | |
| ## | |
| ## Foo.with(&'built in type') # ints, object, node | |
| ## Foo.with(MyCustomClassInAScript) # types defined by class_name (inner classes and enums should use the above option, with full path (aka. Foo.Bar) instead) | |
| ## [/codeblock] | |
| ## | |
| ## Remember, the code needs to be ran once before the class is registered/updated (unless when using NOTIFICATION_POST_SAVE) | |
| ## After running, use it like FooT (e.g, FooInt) | |
| ## | |
| ## This class generates the scripts on _ready | |
| ## If using NOTIFICATION_POST_SAVE, make this a [annotation @GDScript.tool] and call [method GenericType.request_ready] | |
| ## The folder where the generated scripts will be stored | |
| const TARGET_FOLDER := 'res://generics/' | |
| ## A helper constant used to determine what is a generic type parameter | |
| const Param := GenericType | |
| ## A helper function used to support accessing static members and functions | |
| ## | |
| ## Absolutely DO [b]NOT[/b] use it outside of template scripts | |
| static func type(): pass | |
| ## Helper class to make monomorphizations | |
| class Monomorphization: | |
| var base: GDScript | |
| var args: Dictionary[StringName, StringName] | |
| func _init(base: GDScript, args: Dictionary[StringName, StringName]) -> void: | |
| self.base = base | |
| self.args = args | |
| static var _types: Dictionary[Monomorphization, bool] | |
| ## Registers a generic type specialization (monomorphization) | |
| ## The file with said monomorphization will be created on [method GenericType._ready] | |
| ## | |
| ## Note: the class does not check for the existence of properties/methods inside the provided types | |
| static func register(type: GDScript, args: Dictionary[StringName, Variant]) -> void: | |
| for arg in args: | |
| if args[arg] is GDScript: args[arg] = args[arg].get_global_name() | |
| var params: Dictionary[StringName, StringName] | |
| params.assign(args) | |
| _types[Monomorphization.new(type, params)] = true | |
| static var _abstr_rgx := RegEx.create_from_string(r'@abstract *') | |
| static var _xtnds_rgx := RegEx.create_from_string(r'extends +(\w+)') | |
| static var _cname_rgx := RegEx.create_from_string(r'class_name +(\w+)') | |
| static var _param_rgx := RegEx.create_from_string(r'const +(\w+) *:= *GenericType.Param') | |
| func _ready() -> void: | |
| for type: Monomorphization in _types: | |
| var type_name := type.base.get_global_name() | |
| var gen_str := type.base.source_code | |
| gen_str = _abstr_rgx.sub(gen_str, '') | |
| gen_str = _param_rgx.sub(gen_str, '', true) | |
| gen_str = _xtnds_rgx.sub(gen_str, 'extends '+type.base.get_instance_base_type()) | |
| var generics: String = type.args.values().reduce(func(a: StringName, b: StringName): | |
| return a+b.to_pascal_case() | |
| ) | |
| gen_str = _cname_rgx.sub(gen_str, 'class_name $1'+generics.to_pascal_case()) | |
| # this is valid, what | |
| for match in _param_rgx.search_all(type.base.source_code): | |
| var generic := match.strings[1] | |
| gen_str = gen_str.replace(type_name+'.'+generic+'.type()', type.args[generic]) | |
| gen_str = gen_str.replace(type_name+'.'+generic, type.args[generic]) | |
| gen_str = gen_str.replace(generic+'.type()', type.args[generic]) | |
| var file_path := TARGET_FOLDER + _script_name(type.base, type.args) | |
| var f := FileAccess.open(file_path, FileAccess.WRITE) | |
| f.store_string(gen_str) | |
| f.flush() | |
| f.close() | |
| load(file_path).emit_changed() | |
| static func _script_name(type: GDScript, args: Dictionary[StringName, StringName]) -> String: | |
| var ty := type.get_global_name().to_snake_case() | |
| var generics: String = args.values().reduce(func(a: StringName, b: StringName): | |
| return a + b.to_pascal_case() | |
| ) | |
| return '%s_%s.gd'%[ty, generics.to_snake_case()] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment