└── addons └── csharp_gdextension_bindgen ├── cli_entrypoint.gd.uid ├── csharp_gdextension_bindgen.gd.uid ├── icon.png ├── plugin.cfg ├── cli_entrypoint.gd ├── icon.png.import ├── UNLICENSE └── csharp_gdextension_bindgen.gd /addons/csharp_gdextension_bindgen/cli_entrypoint.gd.uid: -------------------------------------------------------------------------------- 1 | uid://najinjv0m445 2 | -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/csharp_gdextension_bindgen.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cy4tmwy5iyogu 2 | -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilzoide/godot-csharp-gdextension-bindgen/HEAD/addons/csharp_gdextension_bindgen/icon.png -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="C# GDExtension Bindgen" 4 | description="Automatic C# bindings generator for GDExtension classes (Godot 4.4+)" 5 | author="gilzoide" 6 | version="0.3.1" 7 | script="csharp_gdextension_bindgen.gd" 8 | -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/cli_entrypoint.gd: -------------------------------------------------------------------------------- 1 | extends SceneTree 2 | 3 | 4 | const CSharpGDExtensionBindgen = preload("csharp_gdextension_bindgen.gd") 5 | 6 | 7 | func _initialize(): 8 | CSharpGDExtensionBindgen.generate_gdextension_csharp_scripts.callv(OS.get_cmdline_user_args()) 9 | quit() 10 | -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://blckign68biqs" 6 | path="res://.godot/imported/icon.png-6749c396fd9e755f292f935bb84594b0.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/csharp_gdextension_bindgen/icon.png" 14 | dest_files=["res://.godot/imported/icon.png-6749c396fd9e755f292f935bb84594b0.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /addons/csharp_gdextension_bindgen/csharp_gdextension_bindgen.gd: -------------------------------------------------------------------------------- 1 | ## GDExtension to C# binding generator 2 | ## 3 | ## The C# classes generated are not attached scripts, but rather wrappers that 4 | ## forward execution to a GodotObject using dynamic calls. 5 | ## 6 | ## Use the "Project -> Tools -> Generate C# GDExtension Bindings" menu item to 7 | ## generate C# bindings from GDExtension. 8 | @tool 9 | extends EditorPlugin 10 | 11 | 12 | const MENU_ITEM_NAME = "Generate C# GDExtension Bindings" 13 | const GENERATED_NAMESPACE = "GDExtensionBindgen" 14 | const GENERATED_SCRIPTS_FOLDER = "res://GDExtensionBindgen" 15 | 16 | enum StringNameType { 17 | PROPERTY_NAME, 18 | METHOD_NAME, 19 | SIGNAL_NAME, 20 | } 21 | const StringNameTypeName = { 22 | StringNameType.PROPERTY_NAME: "PropertyName", 23 | StringNameType.METHOD_NAME: "MethodName", 24 | StringNameType.SIGNAL_NAME: "SignalName", 25 | } 26 | const PASCAL_CASE_NAME_OVERRIDES = { 27 | "BitMap": "Bitmap", 28 | "JSONRPC": "JsonRpc", 29 | "Object": "GodotObject", 30 | "OpenXRIPBinding": "OpenXRIPBinding", 31 | "SkeletonModification2DCCDIK": "SkeletonModification2DCcdik", 32 | "SkeletonModification2DFABRIK": "SkeletonModification2DFabrik", 33 | "SkeletonModification3DCCDIK": "SkeletonModification3DCcdik", 34 | "SkeletonModification3DFABRIK": "SkeletonModification3DFabrik", 35 | "System": "System_", 36 | "Thread": "GodotThread", 37 | } 38 | const PASCAL_CASE_PART_OVERRIDES = { 39 | "AA": "AA", # Anti Aliasing 40 | "AO": "AO", # Ambient Occlusion 41 | "FILENAME": "FileName", 42 | "FADEIN": "FadeIn", 43 | "FADEOUT": "FadeOut", 44 | "FX": "FX", 45 | "GI": "GI", # Global Illumination 46 | "GZIP": "GZip", 47 | "HBOX": "HBox", # Horizontal Box 48 | "ID": "Id", 49 | "IO": "IO", # Input/Output 50 | "IP": "IP", # Internet Protocol 51 | "IV": "IV", # Initialization Vector 52 | "MACOS": "MacOS", 53 | "NODEPATH": "NodePath", 54 | "SPIRV": "SpirV", 55 | "STDIN": "StdIn", 56 | "STDOUT": "StdOut", 57 | "USERNAME": "UserName", 58 | "UV": "UV", 59 | "UV2": "UV2", 60 | "VBOX": "VBox", # Vertical Box 61 | "WHITESPACE": "WhiteSpace", 62 | "WM": "WM", 63 | "XR": "XR", 64 | "XRAPI": "XRApi", 65 | } 66 | 67 | 68 | func _enter_tree(): 69 | add_tool_menu_item(MENU_ITEM_NAME, generate_gdextension_csharp_scripts) 70 | 71 | 72 | func _exit_tree(): 73 | remove_tool_menu_item(MENU_ITEM_NAME) 74 | 75 | 76 | static func generate_csharp_script( 77 | cls_name: StringName, 78 | output_dir := GENERATED_SCRIPTS_FOLDER, 79 | name_space := GENERATED_NAMESPACE, 80 | ): 81 | var class_is_editor_only = _is_editor_extension_class(cls_name) 82 | var parent_class = ClassDB.get_parent_class(cls_name) 83 | var parent_class_is_extension = _is_extension_class(parent_class) 84 | var no_inheritance = parent_class_is_extension 85 | var engine_class = _first_non_extension_parent(cls_name) 86 | 87 | var regions = PackedStringArray() 88 | 89 | # Engine object used for calling engine methods 90 | if not parent_class_is_extension: 91 | regions.append("// Engine object used for calling engine methods\nprotected %s _object;" % parent_class) 92 | 93 | # Constructors 94 | var ctor_fmt 95 | if parent_class_is_extension: 96 | ctor_fmt = """ 97 | public {cls_name}() : base(NativeName) 98 | { 99 | } 100 | protected {cls_name}(StringName @class) : base(@class) 101 | { 102 | } 103 | protected {cls_name}(Variant variant) : base(variant) 104 | { 105 | } 106 | protected {cls_name}([NotNull] {engine_class} @object) : base(@object) 107 | { 108 | } 109 | """ 110 | else: 111 | ctor_fmt = """ 112 | public {cls_name}() : this(NativeName) 113 | { 114 | } 115 | protected {cls_name}(StringName @class) : this(ClassDB.Instantiate(@class)) 116 | { 117 | } 118 | protected {cls_name}(Variant variant) : this(({engine_class}) variant) 119 | { 120 | } 121 | protected {cls_name}([NotNull] {engine_class} @object) 122 | { 123 | _object = @object; 124 | } 125 | """ 126 | var ctor = ctor_fmt.dedent().format({ 127 | cls_name = cls_name, 128 | engine_class = engine_class, 129 | }).strip_edges() 130 | regions.append(ctor) 131 | 132 | var casts = """ 133 | public static implicit operator {engine_class}({cls_name} self) => self?._object; 134 | public static implicit operator Variant({cls_name} self) => self?._object; 135 | public static explicit operator {cls_name}(Variant variant) => variant.AsGodotObject() != null ? new(variant) : null; 136 | """.dedent().format({ 137 | cls_name = cls_name, 138 | engine_class = engine_class, 139 | }).strip_edges() 140 | regions.append(casts) 141 | 142 | # ENUMS 143 | var enums = PackedStringArray() 144 | for enum_name in ClassDB.class_get_enum_list(cls_name, true): 145 | enums.append(_generate_enum(cls_name, enum_name)) 146 | 147 | # INTEGER CONSTANTS 148 | var integer_constants = PackedStringArray() 149 | for constant_name in ClassDB.class_get_integer_constant_list(cls_name, true): 150 | if not ClassDB.class_get_integer_constant_enum(cls_name, constant_name, true).is_empty(): 151 | continue 152 | integer_constants.append(_generate_integer_constant(cls_name, constant_name)) 153 | 154 | # PROPERTIES 155 | var properties = PackedStringArray() 156 | var property_names = PackedStringArray() 157 | for property in ClassDB.class_get_property_list(cls_name, true): 158 | if property["usage"] & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP): 159 | continue 160 | property_names.append(property["name"]) 161 | properties.append(_generate_property(cls_name, property)) 162 | 163 | var inherited_properties = PackedStringArray() 164 | if not parent_class_is_extension: 165 | for inherited_class in _get_parent_classes(cls_name): 166 | for property in ClassDB.class_get_property_list(inherited_class, true): 167 | if property["usage"] & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP): 168 | continue 169 | inherited_properties.append(_generate_property(inherited_class, property)) 170 | 171 | # METHODS 172 | var methods = PackedStringArray() 173 | var method_names = PackedStringArray() 174 | for method in ClassDB.class_get_method_list(cls_name, true): 175 | if method["flags"] & (METHOD_FLAG_VIRTUAL | METHOD_FLAG_VIRTUAL_REQUIRED): 176 | continue 177 | if method["name"].begins_with("_"): 178 | continue 179 | method_names.append(method["name"]) 180 | methods.append(_generate_method(cls_name, method)) 181 | 182 | var inherited_methods = PackedStringArray() 183 | if not parent_class_is_extension: 184 | for inherited_class in _get_parent_classes(cls_name): 185 | for method in ClassDB.class_get_method_list(inherited_class, true): 186 | if method["flags"] & (METHOD_FLAG_VIRTUAL | METHOD_FLAG_VIRTUAL_REQUIRED): 187 | continue 188 | if method["name"].begins_with("_"): 189 | continue 190 | inherited_methods.append(_generate_method(inherited_class, method)) 191 | 192 | # SIGNALS 193 | var signals = PackedStringArray() 194 | var signal_names = PackedStringArray() 195 | for sig in ClassDB.class_get_signal_list(cls_name, true): 196 | signal_names.append(sig["name"]) 197 | signals.append(_generate_signal(cls_name, sig)) 198 | 199 | var inherited_signals = PackedStringArray() 200 | if not parent_class_is_extension: 201 | for inherited_class in _get_parent_classes(cls_name): 202 | for method in ClassDB.class_get_signal_list(inherited_class, true): 203 | inherited_signals.append(_generate_signal(inherited_class, method)) 204 | 205 | # StringName caches 206 | regions.append(_generate_strings_class(cls_name, StringNameType.PROPERTY_NAME, property_names)) 207 | regions.append(_generate_strings_class(cls_name, StringNameType.METHOD_NAME, method_names)) 208 | regions.append(_generate_strings_class(cls_name, StringNameType.SIGNAL_NAME, signal_names)) 209 | regions.append("private static readonly StringName NativeName = \"{cls_name}\";".format({ 210 | cls_name = cls_name, 211 | })) 212 | 213 | if not enums.is_empty(): 214 | regions.append("#region Enums") 215 | regions.append("\n\n".join(enums)) 216 | regions.append("#endregion") 217 | if not integer_constants.is_empty(): 218 | regions.append("#region Integer Constants") 219 | regions.append("\n\n".join(integer_constants)) 220 | regions.append("#endregion") 221 | if not properties.is_empty(): 222 | regions.append("#region Properties") 223 | regions.append("\n\n".join(properties)) 224 | regions.append("#endregion") 225 | if not inherited_properties.is_empty(): 226 | regions.append("#region Inherited Properties") 227 | regions.append("\n\n".join(inherited_properties)) 228 | regions.append("#endregion") 229 | if not methods.is_empty(): 230 | regions.append("#region Methods") 231 | regions.append("\n\n".join(methods)) 232 | regions.append("#endregion") 233 | if not inherited_methods.is_empty(): 234 | regions.append("#region Inherited Methods") 235 | regions.append("\n\n".join(inherited_methods)) 236 | regions.append("#endregion") 237 | if not signals.is_empty(): 238 | regions.append("#region Signals") 239 | regions.append("\n\n".join(signals)) 240 | regions.append("#endregion") 241 | if not inherited_signals.is_empty(): 242 | regions.append("#region Inherited Signals") 243 | regions.append("\n\n".join(inherited_signals)) 244 | regions.append("#endregion") 245 | 246 | var code = """ 247 | // This code was automatically generated by GDExtension C# Bindgen 248 | using System; 249 | using System.Diagnostics.CodeAnalysis; 250 | using Godot; 251 | 252 | namespace {name_space}; 253 | 254 | public class {cls_name}{inheritance} 255 | { 256 | {regions} 257 | } 258 | """.dedent().format({ 259 | name_space = name_space, 260 | cls_name = cls_name, 261 | inheritance = " : " + parent_class if parent_class_is_extension else "", 262 | regions = "\n\n".join(regions).indent("\t"), 263 | }).strip_edges() 264 | 265 | if class_is_editor_only: 266 | code = """ 267 | #if TOOLS 268 | {code} 269 | #endif 270 | """.dedent().format({ 271 | code = code, 272 | }).strip_edges() 273 | 274 | code += "\n" 275 | 276 | if not DirAccess.dir_exists_absolute(output_dir): 277 | DirAccess.make_dir_recursive_absolute(output_dir) 278 | 279 | var new_script = FileAccess.open(output_dir.path_join(cls_name + ".cs"), FileAccess.WRITE) 280 | new_script.store_string(code) 281 | 282 | 283 | static func generate_gdextension_csharp_scripts( 284 | output_dir := GENERATED_SCRIPTS_FOLDER, 285 | name_space := GENERATED_NAMESPACE, 286 | ): 287 | var classes = ClassDB.get_class_list() 288 | for cls_name in classes: 289 | if _is_extension_class(cls_name): 290 | generate_csharp_script(cls_name, output_dir, name_space) 291 | 292 | 293 | static func _generate_enum(cls_name: StringName, enum_name: StringName) -> String: 294 | var common_prefix = null 295 | for constant_name in ClassDB.class_get_enum_constants(cls_name, enum_name, true): 296 | if common_prefix == null: 297 | common_prefix = constant_name 298 | else: 299 | common_prefix = _get_common_prefix(common_prefix, constant_name) 300 | # Handle special case where one of the constants is present in all constant 301 | # names: remove last word from prefix. 302 | # Example case: Node.ProcessThreadMessages and FLAG_PROCESS_THREAD_MESSAGES 303 | if common_prefix in ClassDB.class_get_enum_constants(cls_name, enum_name, true): 304 | common_prefix = common_prefix.rsplit("_", false, 1)[0] 305 | 306 | var constants = PackedStringArray() 307 | for constant_name in ClassDB.class_get_enum_constants(cls_name, enum_name, true): 308 | constants.append("{csharp_constant_name} = {constant_value}L,".format({ 309 | csharp_constant_name = constant_name.substr(common_prefix.length()).to_pascal_case(), 310 | constant_value = ClassDB.class_get_integer_constant(cls_name, constant_name), 311 | })) 312 | 313 | return """ 314 | {flags} 315 | public enum {enum_name}{maybe_enum_suffix} : long 316 | { 317 | {constants} 318 | } 319 | """.dedent().format({ 320 | flags = "[Flags]" if ClassDB.is_class_enum_bitfield(cls_name, enum_name) else "", 321 | enum_name = enum_name, 322 | constants = "\n".join(constants).indent("\t"), 323 | maybe_enum_suffix = "Enum" if _needs_enum_suffix(cls_name, enum_name) else "", 324 | }).strip_edges() 325 | 326 | 327 | static func _generate_integer_constant(cls_name: StringName, constant_name: StringName) -> String: 328 | return "public const long {csharp_constant_name} = {constant_value}L;".format({ 329 | csharp_constant_name = constant_name.to_pascal_case(), 330 | constant_value = ClassDB.class_get_integer_constant(cls_name, constant_name), 331 | }) 332 | 333 | 334 | static func _generate_property(cls_name: StringName, property: Dictionary) -> String: 335 | var property_name = property["name"] 336 | var csharp_property_name = property_name.to_pascal_case() 337 | var property_type = _get_property_type(cls_name, property) 338 | 339 | var getset = PackedStringArray() 340 | 341 | var getter = ClassDB.class_get_property_getter(cls_name, property_name) 342 | if getter: 343 | if _is_extension_class(cls_name): 344 | getset.append("get => {get_cast}_object.Get(PropertyName.{csharp_property_name});".format({ 345 | get_cast = _property_get_cast(cls_name, property), 346 | csharp_property_name = csharp_property_name, 347 | })) 348 | else: 349 | getset.append("get => _object.{csharp_property_name};".format({ 350 | csharp_property_name = csharp_property_name, 351 | })) 352 | 353 | var setter = ClassDB.class_get_property_setter(cls_name, property_name) 354 | if setter: 355 | if _is_extension_class(cls_name): 356 | getset.append("set => _object.Set(PropertyName.{csharp_property_name}, {set_cast}value);".format({ 357 | set_cast = _property_set_cast(property), 358 | csharp_property_name = csharp_property_name, 359 | })) 360 | else: 361 | getset.append("set => _object.{csharp_property_name} = value;".format({ 362 | csharp_property_name = csharp_property_name, 363 | })) 364 | 365 | return """ 366 | public {property_type} {csharp_property_name} 367 | { 368 | {getset} 369 | } 370 | """.dedent().format({ 371 | property_type = property_type, 372 | csharp_property_name = csharp_property_name, 373 | getset = "\n".join(getset).indent("\t"), 374 | }).strip_edges() 375 | 376 | 377 | static func _generate_method(cls_name: StringName, method: Dictionary) -> String: 378 | var method_name = method["name"] 379 | var csharp_method_name = method_name.to_pascal_case() 380 | var return_type = _get_method_return_type(cls_name, method_name, method["return"]) 381 | var is_static = method["flags"] & METHOD_FLAG_STATIC 382 | 383 | var arg_types = PackedStringArray() 384 | var arg_names = PackedStringArray() 385 | 386 | var args = PackedStringArray() 387 | for argument in method["args"]: 388 | var arg_type = _get_property_type(cls_name, argument) 389 | var arg_name = "@" + argument["name"] 390 | # hardcode type that cannot be known from reflection in GDScript 391 | if method["name"] == "connect" and arg_name == "@flags": 392 | arg_type = "uint" 393 | args.append("{arg_type} {arg_name}".format({ 394 | arg_type = arg_type, 395 | arg_name = arg_name, 396 | })) 397 | arg_types.append(arg_type) 398 | if _property_is_enum(argument): 399 | arg_names.append("(int)" + arg_name) 400 | else: 401 | arg_names.append(arg_name) 402 | 403 | var implementation = PackedStringArray() 404 | var default_args = method["default_args"] 405 | var i = args.size() - default_args.size() 406 | for default_value in default_args: 407 | if default_value == null: 408 | default_value = "default" 409 | # handle enums 410 | elif default_value is int and arg_types[i] != "int": 411 | default_value = ("(%s)" % arg_types[i]) + str(default_value) 412 | # C# requires the "f" suffix for float literals 413 | elif default_value is float and arg_types[i] == "float": 414 | default_value = "%sf" % default_value 415 | # NOTE: don't move this branch below the String one, since most of the 416 | # time when arg_types[i] == StringName, default_value is String 417 | elif default_value is StringName or arg_types[i] == "Godot.StringName": 418 | implementation.append('%s ??= "%s";' % [arg_names[i], default_value]) 419 | default_value = "null" 420 | elif default_value is String: 421 | default_value = '"%s"' % default_value 422 | elif default_value is Array: 423 | assert(default_value.is_empty(), "Populated Array not supported yet! " + str(default_value)) # TODO: support populated array as default value 424 | implementation.append("%s ??= new();" % arg_names[i]) 425 | default_value = "null" 426 | elif default_value is Dictionary: 427 | assert(default_value.is_empty(), "Populated Dictionary not supported yet! " + str(default_value)) # TODO: support populated dictionary as default value 428 | implementation.append("%s ??= new();" % arg_names[i]) 429 | default_value = "null" 430 | elif ( 431 | default_value is Vector2 or default_value is Vector3 or default_value is Vector4 432 | or default_value is Color 433 | ): 434 | args[i] = args[i].replace(arg_types[i], arg_types[i] + "?") 435 | var impl = "%s ??= new%s;" % [arg_names[i], default_value] 436 | if not OS.has_feature("double"): 437 | impl = impl.replace(",", "f,").replace(")", "f)") 438 | implementation.append(impl) 439 | default_value = "null" 440 | elif ( 441 | default_value is PackedByteArray 442 | or default_value is PackedInt32Array or default_value is PackedInt64Array 443 | or default_value is PackedFloat32Array or default_value is PackedFloat64Array 444 | or default_value is PackedVector2Array or default_value is PackedVector3Array or default_value is PackedVector4Array 445 | or default_value is PackedColorArray 446 | ): 447 | assert(default_value.is_empty(), "Populated Packed Array not supported yet! " + str(default_value)) 448 | implementation.append("%s ??= System.Array.Empty<%s>();" % [arg_names[i], arg_types[i].replace("[]", "")]) 449 | default_value = "null" 450 | elif default_value is Transform2D: 451 | assert(default_value == Transform2D.IDENTITY, "Only identity Transform2D is supported as default value") 452 | args[i] = args[i].replace(arg_types[i], arg_types[i] + "?") 453 | implementation.append("%s ??= Godot.Transform2D.Identity;" % arg_names[i]) 454 | default_value = "null" 455 | elif default_value is Transform3D: 456 | assert(default_value == Transform3D.IDENTITY, "Only identity Transform3D is supported as default value") 457 | args[i] = args[i].replace(arg_types[i], arg_types[i] + "?") 458 | implementation.append("%s ??= Godot.Transform3D.Identity;" % arg_names[i]) 459 | default_value = "null" 460 | args[i] += " = " + str(default_value) 461 | i += 1 462 | 463 | if method["flags"] & METHOD_FLAG_VARARG: 464 | args.append("params Variant[] varargs") 465 | arg_names.append("varargs") 466 | 467 | if _is_extension_class(cls_name): 468 | arg_names.insert(0, "MethodName.{csharp_method_name}".format({ 469 | csharp_method_name = csharp_method_name, 470 | })) 471 | if is_static: 472 | implementation.append("{maybe_return}ClassDB.ClassCallStatic(NativeName, {arg_names});".format({ 473 | arg_names = ", ".join(arg_names), 474 | maybe_return = "return " + _property_get_cast(cls_name, method["return"]) if return_type != "void" else "", 475 | })) 476 | else: 477 | implementation.append("{maybe_return}_object.Call({arg_names});".format({ 478 | arg_names = ", ".join(arg_names), 479 | maybe_return = "return " + _property_get_cast(cls_name, method["return"]) if return_type != "void" else "", 480 | })) 481 | else: 482 | if is_static: 483 | implementation.append("{maybe_return}{engine_class}.{csharp_method_name}({arg_names});".format({ 484 | arg_names = ", ".join(arg_names), 485 | engine_class = _first_non_extension_parent(cls_name), 486 | csharp_method_name = csharp_method_name, 487 | maybe_return = "return " if return_type != "void" else "", 488 | })) 489 | else: 490 | implementation.append("{maybe_return}_object.{csharp_method_name}({arg_names});".format({ 491 | arg_names = ", ".join(arg_names), 492 | csharp_method_name = csharp_method_name, 493 | maybe_return = "return " if return_type != "void" else "", 494 | })) 495 | 496 | return """ 497 | public {maybe_static}{maybe_override}{return_type} {csharp_method_name}({args}) 498 | { 499 | {implementation} 500 | } 501 | """.dedent().format({ 502 | args = ", ".join(args), 503 | csharp_method_name = csharp_method_name, 504 | implementation = "\n".join(implementation).indent("\t"), 505 | maybe_override = "override " if csharp_method_name == "ToString" else "", 506 | maybe_static = "static " if is_static else "", 507 | return_type = return_type, 508 | }).strip_edges() 509 | 510 | 511 | static func _generate_signal(cls_name: StringName, sig: Dictionary): 512 | var signal_name = sig["name"] 513 | var csharp_signal_name = signal_name.to_pascal_case() 514 | var return_type = _get_method_return_type(cls_name, signal_name, sig["return"]) 515 | 516 | var arg_types = PackedStringArray() 517 | for argument in sig["args"]: 518 | var arg_type = _get_property_type(cls_name, argument) 519 | arg_types.append(arg_type) 520 | 521 | var delegate_type 522 | if return_type == "void": 523 | if not arg_types.is_empty(): 524 | delegate_type = "Action<{arg_types}>".format({ 525 | arg_types = ", ".join(arg_types) 526 | }) 527 | else: 528 | delegate_type = "Action" 529 | else: 530 | arg_types.append(return_type) 531 | delegate_type = "Func<{arg_types}>".format({ 532 | arg_types = ", ".join(arg_types) 533 | }) 534 | 535 | return """ 536 | public event {delegate_type} {csharp_signal_name} 537 | { 538 | add 539 | { 540 | Connect(SignalName.{csharp_signal_name}, Callable.From(value)); 541 | } 542 | remove 543 | { 544 | Disconnect(SignalName.{csharp_signal_name}, Callable.From(value)); 545 | } 546 | } 547 | """.dedent().format({ 548 | delegate_type = delegate_type, 549 | csharp_signal_name = csharp_signal_name, 550 | }).strip_edges() 551 | 552 | 553 | static func _property_is_enum(property: Dictionary) -> bool: 554 | return property["usage"] & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD) 555 | 556 | 557 | static func _get_property_type(cls_name: StringName, property: Dictionary) -> String: 558 | match property["type"]: 559 | TYPE_NIL: 560 | return "Variant" 561 | TYPE_BOOL: 562 | return "bool" 563 | TYPE_INT: 564 | if _property_is_enum(property): 565 | var enum_name = property["class_name"] 566 | if enum_name == "Error": 567 | return "Godot.Error" 568 | var split = enum_name.split(".") 569 | if split.size() == 1: 570 | return enum_name + ("Enum" if _needs_enum_suffix(cls_name, enum_name) else "") 571 | else: 572 | return enum_name + ("Enum" if _needs_enum_suffix(split[0], split[1]) else "") 573 | return "int" 574 | TYPE_FLOAT: 575 | return "double" if OS.has_feature("double") else "float" 576 | TYPE_STRING: 577 | return "string" 578 | TYPE_VECTOR2I: 579 | return "Godot.Vector2I" 580 | TYPE_RECT2I: 581 | return "Godot.Rect2I" 582 | TYPE_VECTOR3I: 583 | return "Godot.Vector3I" 584 | TYPE_VECTOR4I: 585 | return "Godot.Vector4I" 586 | TYPE_AABB: 587 | return "Godot.Aabb" 588 | TYPE_RID: 589 | return "Godot.Rid" 590 | TYPE_OBJECT: 591 | if property["class_name"] and property["class_name"] != "Object": 592 | return _pascal_to_pascal_case(_get_class_from_class_name(property["class_name"])) 593 | else: 594 | return "GodotObject" 595 | TYPE_ARRAY: 596 | if property["hint"] & PROPERTY_HINT_ARRAY_TYPE: 597 | return "Godot.Collections.Array<%s>" % _get_mapped_variant_type(property["hint_string"]) 598 | else: 599 | return "Godot.Collections.Array" 600 | TYPE_DICTIONARY: 601 | return "Godot.Collections.Dictionary" 602 | TYPE_PACKED_BYTE_ARRAY: 603 | return "byte[]" 604 | TYPE_PACKED_INT32_ARRAY: 605 | return "int[]" 606 | TYPE_PACKED_INT64_ARRAY: 607 | return "long[]" 608 | TYPE_PACKED_FLOAT32_ARRAY: 609 | return "float[]" 610 | TYPE_PACKED_FLOAT64_ARRAY: 611 | return "double[]" 612 | TYPE_PACKED_STRING_ARRAY: 613 | return "string[]" 614 | TYPE_PACKED_VECTOR2_ARRAY: 615 | return "Godot.Vector2[]" 616 | TYPE_PACKED_VECTOR3_ARRAY: 617 | return "Godot.Vector3[]" 618 | TYPE_PACKED_VECTOR4_ARRAY: 619 | return "Godot.Vector4[]" 620 | TYPE_PACKED_COLOR_ARRAY: 621 | return "Godot.Color[]" 622 | var t: 623 | return "Godot." + type_string(t) 624 | 625 | 626 | static func _get_mapped_variant_type(variant_type_name: String) -> String: 627 | var _type_map = { 628 | "Variant": "Variant", 629 | type_string(TYPE_BOOL): "bool", 630 | type_string(TYPE_INT): "int", 631 | type_string(TYPE_FLOAT): "double" if OS.has_feature("double") else "float", 632 | type_string(TYPE_STRING): "string", 633 | type_string(TYPE_STRING_NAME): "StringName", 634 | type_string(TYPE_VECTOR2I): "Godot.Vector2I", 635 | type_string(TYPE_RECT2I): "Godot.Rect2I", 636 | type_string(TYPE_VECTOR3I): "Godot.Vector3I", 637 | type_string(TYPE_VECTOR4I): "Godot.Vector4I", 638 | type_string(TYPE_AABB): "Godot.Aabb", 639 | type_string(TYPE_RID): "Godot.Rid", 640 | type_string(TYPE_OBJECT): "GodotObject", 641 | type_string(TYPE_ARRAY): "Godot.Collections.Array", 642 | type_string(TYPE_DICTIONARY): "Godot.Collections.Dictionary", 643 | type_string(TYPE_PACKED_BYTE_ARRAY): "byte[]", 644 | type_string(TYPE_PACKED_INT32_ARRAY): "int[]", 645 | type_string(TYPE_PACKED_INT64_ARRAY): "long[]", 646 | type_string(TYPE_PACKED_FLOAT32_ARRAY): "float[]", 647 | type_string(TYPE_PACKED_FLOAT64_ARRAY): "double[]", 648 | type_string(TYPE_PACKED_STRING_ARRAY): "string[]", 649 | type_string(TYPE_PACKED_VECTOR2_ARRAY): "Godot.Vector2[]", 650 | type_string(TYPE_PACKED_VECTOR3_ARRAY): "Godot.Vector3[]", 651 | type_string(TYPE_PACKED_VECTOR4_ARRAY): "Godot.Vector4[]", 652 | type_string(TYPE_PACKED_COLOR_ARRAY): "Godot.Color[]", 653 | } 654 | return _type_map.get(variant_type_name, "Godot." + variant_type_name) 655 | 656 | 657 | static func _property_get_cast(cls_name: StringName, property: Dictionary): 658 | var property_type = _get_property_type(cls_name, property) 659 | if _property_is_enum(property): 660 | return "(%s)(int)" % property_type 661 | else: 662 | return "(%s)" % property_type 663 | 664 | 665 | static func _property_set_cast(property: Dictionary): 666 | if _property_is_enum(property): 667 | return "(int)" 668 | else: 669 | return "" 670 | 671 | 672 | static func _is_extension_class(cls_name: StringName) -> bool: 673 | return ClassDB.class_get_api_type(cls_name) in [ 674 | ClassDB.APIType.API_EXTENSION, 675 | ClassDB.APIType.API_EDITOR_EXTENSION, 676 | ] 677 | 678 | 679 | static func _is_editor_extension_class(cls_name: StringName) -> bool: 680 | return ClassDB.class_get_api_type(cls_name) == ClassDB.APIType.API_EDITOR_EXTENSION 681 | 682 | 683 | static func _first_non_extension_parent(cls_name: StringName) -> StringName: 684 | while _is_extension_class(cls_name): 685 | cls_name = ClassDB.get_parent_class(cls_name) 686 | return cls_name 687 | 688 | 689 | static func _get_method_return_type(cls_name: StringName, method_name: StringName, method_return: Dictionary) -> String: 690 | # hardcode type that cannot be known from reflection in GDScript 691 | if method_name == "get_instance_id": 692 | return "ulong" 693 | 694 | if method_return["type"] == TYPE_NIL: 695 | if method_return["usage"] & PROPERTY_USAGE_NIL_IS_VARIANT: 696 | return "Variant" 697 | else: 698 | return "void" 699 | else: 700 | return _get_property_type(cls_name, method_return) 701 | 702 | 703 | static func _get_parent_classes(cls_name: StringName) -> Array[StringName]: 704 | var parent_classes = [] as Array[StringName] 705 | while true: 706 | cls_name = ClassDB.get_parent_class(cls_name) 707 | parent_classes.append(cls_name) 708 | if cls_name == "Object": 709 | break 710 | return parent_classes 711 | 712 | 713 | static func _generate_strings_class(cls_name: StringName, string_name_type: StringNameType, string_names: PackedStringArray) -> String: 714 | var parent_class = ClassDB.get_parent_class(cls_name) 715 | var lines = PackedStringArray() 716 | for name in string_names: 717 | if string_name_type == StringNameType.METHOD_NAME and ClassDB.class_has_method(parent_class, name): 718 | continue 719 | if string_name_type == StringNameType.SIGNAL_NAME and ClassDB.class_has_signal(parent_class, name): 720 | continue 721 | lines.append("public static readonly StringName {cs_name} = \"{name}\";".format({ 722 | cs_name = name.to_pascal_case(), 723 | name = name, 724 | })) 725 | return """ 726 | public {maybe_new}class {strings_class} : {parent_class}.{strings_class} 727 | { 728 | {lines} 729 | } 730 | """.dedent().format({ 731 | lines = "\n".join(lines).indent("\t"), 732 | maybe_new = "new " if _is_extension_class(parent_class) else "", 733 | parent_class = parent_class, 734 | strings_class = StringNameTypeName[string_name_type], 735 | }).strip_edges() 736 | 737 | 738 | static func _get_common_prefix(s1: String, s2: String) -> String: 739 | var common_length = min(s1.length(), s2.length()) 740 | for i in range(common_length): 741 | if s1[i] != s2[i]: 742 | return s1.substr(0, i) 743 | return s1.substr(0, common_length) 744 | 745 | 746 | static func _get_class_from_class_name(cls_name: String) -> String: 747 | var classes = cls_name.split(",") 748 | if classes.size() == 1: 749 | return cls_name 750 | 751 | # Handle special case where 2 or more class names are present separated 752 | # by ",": calculate the common parent class. 753 | # Example case: CanvasItem.material uses "CanvasItemMaterial,ShaderMaterial" 754 | var parent_classes = _get_parent_classes(classes[0]) 755 | for i in range(1, classes.size()): 756 | var test_cls = classes[i] 757 | while not ClassDB.is_parent_class(test_cls, parent_classes[0]): 758 | parent_classes.pop_front() 759 | return parent_classes[0] 760 | 761 | 762 | static func _needs_enum_suffix(cls_name: StringName, enum_name: String) -> bool: 763 | var snake_case_enum_name = enum_name.to_snake_case() 764 | if ClassDB.class_has_method(cls_name, snake_case_enum_name): 765 | return true 766 | if ClassDB.class_has_signal(cls_name, snake_case_enum_name): 767 | return true 768 | var properties = ClassDB.class_get_property_list(cls_name) 769 | for property in properties: 770 | if snake_case_enum_name == property["name"]: 771 | return true 772 | return false 773 | 774 | 775 | # Pascal case conversion used for class names. 776 | # Replicates the logic from `godot/modules/mono/utils/naming_utils.cpp` 777 | static func _is_ascii_upper_case(c: String) -> bool: 778 | return c.to_upper() == c 779 | 780 | 781 | static func _is_ascii_lower_case(c: String) -> bool: 782 | return c.to_lower() == c 783 | 784 | 785 | static func _is_digit(c: String) -> bool: 786 | return c >= "0" and c <= "9" 787 | 788 | 789 | static func _split_pascal_case(p_identifier: String) -> PackedStringArray: 790 | var parts := PackedStringArray() 791 | var current_part_start := 0 792 | var prev_was_upper := _is_ascii_upper_case(p_identifier[0]) 793 | for i in range(1, p_identifier.length()): 794 | if prev_was_upper: 795 | if _is_digit(p_identifier[i]) or _is_ascii_lower_case(p_identifier[i]): 796 | if not _is_digit(p_identifier[i]): 797 | # These conditions only apply when the separator is not a digit. 798 | if i - current_part_start == 1: 799 | # Upper character was only the beginning of a word. 800 | prev_was_upper = false 801 | continue 802 | if i != p_identifier.length(): 803 | # If this is not the last character, the last uppercase 804 | # character is the start of the next word. 805 | i -= 1 806 | if i - current_part_start > 0: 807 | parts.append(p_identifier.substr(current_part_start, i - current_part_start)) 808 | current_part_start = i 809 | prev_was_upper = false 810 | else: 811 | if _is_digit(p_identifier[i]) or _is_ascii_upper_case(p_identifier[i]): 812 | parts.append(p_identifier.substr(current_part_start, i - current_part_start)) 813 | current_part_start = i 814 | prev_was_upper = true 815 | 816 | # Add the rest of the identifier as the last part. 817 | if current_part_start != p_identifier.length(): 818 | parts.append(p_identifier.substr(current_part_start)) 819 | return parts 820 | 821 | 822 | static func _pascal_to_pascal_case(p_identifier: String) -> String: 823 | if p_identifier.length() == 0: 824 | return p_identifier 825 | if p_identifier.length() <= 2: 826 | return p_identifier.to_upper() 827 | if PASCAL_CASE_NAME_OVERRIDES.has(p_identifier): 828 | return PASCAL_CASE_NAME_OVERRIDES[p_identifier] 829 | 830 | var parts := _split_pascal_case(p_identifier) 831 | var ret := "" 832 | for part in parts: 833 | if PASCAL_CASE_PART_OVERRIDES.has(part): 834 | ret += PASCAL_CASE_PART_OVERRIDES[part] 835 | continue 836 | 837 | if part.length() <= 2 and _is_ascii_upper_case(part): 838 | ret += part.to_upper() 839 | continue 840 | 841 | part[0] = part[0].to_upper() 842 | for i in range(1, part.length()): 843 | if _is_digit(part[i - 1]): 844 | # Use uppercase after digits. 845 | part[i] = part[i].to_upper() 846 | else: 847 | part[i] = part[i].to_lower() 848 | ret += part 849 | return ret 850 | --------------------------------------------------------------------------------