850 lines
28 KiB
GDScript
850 lines
28 KiB
GDScript
## GDExtension to C# binding generator
|
|
##
|
|
## The C# classes generated are not attached scripts, but rather wrappers that
|
|
## forward execution to a GodotObject using dynamic calls.
|
|
##
|
|
## Use the "Project -> Tools -> Generate C# GDExtension Bindings" menu item to
|
|
## generate C# bindings from GDExtension.
|
|
@tool
|
|
extends EditorPlugin
|
|
|
|
|
|
const MENU_ITEM_NAME = "Generate C# GDExtension Bindings"
|
|
const GENERATED_NAMESPACE = "GDExtensionBindgen"
|
|
const GENERATED_SCRIPTS_FOLDER = "res://GDExtensionBindgen"
|
|
|
|
enum StringNameType {
|
|
PROPERTY_NAME,
|
|
METHOD_NAME,
|
|
SIGNAL_NAME,
|
|
}
|
|
const StringNameTypeName = {
|
|
StringNameType.PROPERTY_NAME: "PropertyName",
|
|
StringNameType.METHOD_NAME: "MethodName",
|
|
StringNameType.SIGNAL_NAME: "SignalName",
|
|
}
|
|
const PASCAL_CASE_NAME_OVERRIDES = {
|
|
"BitMap": "Bitmap",
|
|
"JSONRPC": "JsonRpc",
|
|
"Object": "GodotObject",
|
|
"OpenXRIPBinding": "OpenXRIPBinding",
|
|
"SkeletonModification2DCCDIK": "SkeletonModification2DCcdik",
|
|
"SkeletonModification2DFABRIK": "SkeletonModification2DFabrik",
|
|
"SkeletonModification3DCCDIK": "SkeletonModification3DCcdik",
|
|
"SkeletonModification3DFABRIK": "SkeletonModification3DFabrik",
|
|
"System": "System_",
|
|
"Thread": "GodotThread",
|
|
}
|
|
const PASCAL_CASE_PART_OVERRIDES = {
|
|
"AA": "AA", # Anti Aliasing
|
|
"AO": "AO", # Ambient Occlusion
|
|
"FILENAME": "FileName",
|
|
"FADEIN": "FadeIn",
|
|
"FADEOUT": "FadeOut",
|
|
"FX": "FX",
|
|
"GI": "GI", # Global Illumination
|
|
"GZIP": "GZip",
|
|
"HBOX": "HBox", # Horizontal Box
|
|
"ID": "Id",
|
|
"IO": "IO", # Input/Output
|
|
"IP": "IP", # Internet Protocol
|
|
"IV": "IV", # Initialization Vector
|
|
"MACOS": "MacOS",
|
|
"NODEPATH": "NodePath",
|
|
"SPIRV": "SpirV",
|
|
"STDIN": "StdIn",
|
|
"STDOUT": "StdOut",
|
|
"USERNAME": "UserName",
|
|
"UV": "UV",
|
|
"UV2": "UV2",
|
|
"VBOX": "VBox", # Vertical Box
|
|
"WHITESPACE": "WhiteSpace",
|
|
"WM": "WM",
|
|
"XR": "XR",
|
|
"XRAPI": "XRApi",
|
|
}
|
|
|
|
|
|
func _enter_tree():
|
|
add_tool_menu_item(MENU_ITEM_NAME, generate_gdextension_csharp_scripts)
|
|
|
|
|
|
func _exit_tree():
|
|
remove_tool_menu_item(MENU_ITEM_NAME)
|
|
|
|
|
|
static func generate_csharp_script(
|
|
cls_name: StringName,
|
|
output_dir := GENERATED_SCRIPTS_FOLDER,
|
|
name_space := GENERATED_NAMESPACE,
|
|
):
|
|
var class_is_editor_only = _is_editor_extension_class(cls_name)
|
|
var parent_class = ClassDB.get_parent_class(cls_name)
|
|
var parent_class_is_extension = _is_extension_class(parent_class)
|
|
var no_inheritance = parent_class_is_extension
|
|
var engine_class = _first_non_extension_parent(cls_name)
|
|
|
|
var regions = PackedStringArray()
|
|
|
|
# Engine object used for calling engine methods
|
|
if not parent_class_is_extension:
|
|
regions.append("// Engine object used for calling engine methods\nprotected %s _object;" % parent_class)
|
|
|
|
# Constructors
|
|
var ctor_fmt
|
|
if parent_class_is_extension:
|
|
ctor_fmt = """
|
|
public {cls_name}() : base(NativeName)
|
|
{
|
|
}
|
|
protected {cls_name}(StringName @class) : base(@class)
|
|
{
|
|
}
|
|
protected {cls_name}(Variant variant) : base(variant)
|
|
{
|
|
}
|
|
protected {cls_name}([NotNull] {engine_class} @object) : base(@object)
|
|
{
|
|
}
|
|
"""
|
|
else:
|
|
ctor_fmt = """
|
|
public {cls_name}() : this(NativeName)
|
|
{
|
|
}
|
|
protected {cls_name}(StringName @class) : this(ClassDB.Instantiate(@class))
|
|
{
|
|
}
|
|
protected {cls_name}(Variant variant) : this(({engine_class}) variant)
|
|
{
|
|
}
|
|
protected {cls_name}([NotNull] {engine_class} @object)
|
|
{
|
|
_object = @object;
|
|
}
|
|
"""
|
|
var ctor = ctor_fmt.dedent().format({
|
|
cls_name = cls_name,
|
|
engine_class = engine_class,
|
|
}).strip_edges()
|
|
regions.append(ctor)
|
|
|
|
var casts = """
|
|
public static implicit operator {engine_class}({cls_name} self) => self?._object;
|
|
public static implicit operator Variant({cls_name} self) => self?._object;
|
|
public static explicit operator {cls_name}(Variant variant) => variant.AsGodotObject() != null ? new(variant) : null;
|
|
""".dedent().format({
|
|
cls_name = cls_name,
|
|
engine_class = engine_class,
|
|
}).strip_edges()
|
|
regions.append(casts)
|
|
|
|
# ENUMS
|
|
var enums = PackedStringArray()
|
|
for enum_name in ClassDB.class_get_enum_list(cls_name, true):
|
|
enums.append(_generate_enum(cls_name, enum_name))
|
|
|
|
# INTEGER CONSTANTS
|
|
var integer_constants = PackedStringArray()
|
|
for constant_name in ClassDB.class_get_integer_constant_list(cls_name, true):
|
|
if not ClassDB.class_get_integer_constant_enum(cls_name, constant_name, true).is_empty():
|
|
continue
|
|
integer_constants.append(_generate_integer_constant(cls_name, constant_name))
|
|
|
|
# PROPERTIES
|
|
var properties = PackedStringArray()
|
|
var property_names = PackedStringArray()
|
|
for property in ClassDB.class_get_property_list(cls_name, true):
|
|
if property["usage"] & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP):
|
|
continue
|
|
property_names.append(property["name"])
|
|
properties.append(_generate_property(cls_name, property))
|
|
|
|
var inherited_properties = PackedStringArray()
|
|
if not parent_class_is_extension:
|
|
for inherited_class in _get_parent_classes(cls_name):
|
|
for property in ClassDB.class_get_property_list(inherited_class, true):
|
|
if property["usage"] & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP):
|
|
continue
|
|
inherited_properties.append(_generate_property(inherited_class, property))
|
|
|
|
# METHODS
|
|
var methods = PackedStringArray()
|
|
var method_names = PackedStringArray()
|
|
for method in ClassDB.class_get_method_list(cls_name, true):
|
|
if method["flags"] & (METHOD_FLAG_VIRTUAL | METHOD_FLAG_VIRTUAL_REQUIRED):
|
|
continue
|
|
if method["name"].begins_with("_"):
|
|
continue
|
|
method_names.append(method["name"])
|
|
methods.append(_generate_method(cls_name, method))
|
|
|
|
var inherited_methods = PackedStringArray()
|
|
if not parent_class_is_extension:
|
|
for inherited_class in _get_parent_classes(cls_name):
|
|
for method in ClassDB.class_get_method_list(inherited_class, true):
|
|
if method["flags"] & (METHOD_FLAG_VIRTUAL | METHOD_FLAG_VIRTUAL_REQUIRED):
|
|
continue
|
|
if method["name"].begins_with("_"):
|
|
continue
|
|
inherited_methods.append(_generate_method(inherited_class, method))
|
|
|
|
# SIGNALS
|
|
var signals = PackedStringArray()
|
|
var signal_names = PackedStringArray()
|
|
for sig in ClassDB.class_get_signal_list(cls_name, true):
|
|
signal_names.append(sig["name"])
|
|
signals.append(_generate_signal(cls_name, sig))
|
|
|
|
var inherited_signals = PackedStringArray()
|
|
if not parent_class_is_extension:
|
|
for inherited_class in _get_parent_classes(cls_name):
|
|
for method in ClassDB.class_get_signal_list(inherited_class, true):
|
|
inherited_signals.append(_generate_signal(inherited_class, method))
|
|
|
|
# StringName caches
|
|
regions.append(_generate_strings_class(cls_name, StringNameType.PROPERTY_NAME, property_names))
|
|
regions.append(_generate_strings_class(cls_name, StringNameType.METHOD_NAME, method_names))
|
|
regions.append(_generate_strings_class(cls_name, StringNameType.SIGNAL_NAME, signal_names))
|
|
regions.append("private static readonly StringName NativeName = \"{cls_name}\";".format({
|
|
cls_name = cls_name,
|
|
}))
|
|
|
|
if not enums.is_empty():
|
|
regions.append("#region Enums")
|
|
regions.append("\n\n".join(enums))
|
|
regions.append("#endregion")
|
|
if not integer_constants.is_empty():
|
|
regions.append("#region Integer Constants")
|
|
regions.append("\n\n".join(integer_constants))
|
|
regions.append("#endregion")
|
|
if not properties.is_empty():
|
|
regions.append("#region Properties")
|
|
regions.append("\n\n".join(properties))
|
|
regions.append("#endregion")
|
|
if not inherited_properties.is_empty():
|
|
regions.append("#region Inherited Properties")
|
|
regions.append("\n\n".join(inherited_properties))
|
|
regions.append("#endregion")
|
|
if not methods.is_empty():
|
|
regions.append("#region Methods")
|
|
regions.append("\n\n".join(methods))
|
|
regions.append("#endregion")
|
|
if not inherited_methods.is_empty():
|
|
regions.append("#region Inherited Methods")
|
|
regions.append("\n\n".join(inherited_methods))
|
|
regions.append("#endregion")
|
|
if not signals.is_empty():
|
|
regions.append("#region Signals")
|
|
regions.append("\n\n".join(signals))
|
|
regions.append("#endregion")
|
|
if not inherited_signals.is_empty():
|
|
regions.append("#region Inherited Signals")
|
|
regions.append("\n\n".join(inherited_signals))
|
|
regions.append("#endregion")
|
|
|
|
var code = """
|
|
// This code was automatically generated by GDExtension C# Bindgen
|
|
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Godot;
|
|
|
|
namespace {name_space};
|
|
|
|
public class {cls_name}{inheritance}
|
|
{
|
|
{regions}
|
|
}
|
|
""".dedent().format({
|
|
name_space = name_space,
|
|
cls_name = cls_name,
|
|
inheritance = " : " + parent_class if parent_class_is_extension else "",
|
|
regions = "\n\n".join(regions).indent("\t"),
|
|
}).strip_edges()
|
|
|
|
if class_is_editor_only:
|
|
code = """
|
|
#if TOOLS
|
|
{code}
|
|
#endif
|
|
""".dedent().format({
|
|
code = code,
|
|
}).strip_edges()
|
|
|
|
code += "\n"
|
|
|
|
if not DirAccess.dir_exists_absolute(output_dir):
|
|
DirAccess.make_dir_recursive_absolute(output_dir)
|
|
|
|
var new_script = FileAccess.open(output_dir.path_join(cls_name + ".cs"), FileAccess.WRITE)
|
|
new_script.store_string(code)
|
|
|
|
|
|
static func generate_gdextension_csharp_scripts(
|
|
output_dir := GENERATED_SCRIPTS_FOLDER,
|
|
name_space := GENERATED_NAMESPACE,
|
|
):
|
|
var classes = ClassDB.get_class_list()
|
|
for cls_name in classes:
|
|
if _is_extension_class(cls_name):
|
|
generate_csharp_script(cls_name, output_dir, name_space)
|
|
|
|
|
|
static func _generate_enum(cls_name: StringName, enum_name: StringName) -> String:
|
|
var common_prefix = null
|
|
for constant_name in ClassDB.class_get_enum_constants(cls_name, enum_name, true):
|
|
if common_prefix == null:
|
|
common_prefix = constant_name
|
|
else:
|
|
common_prefix = _get_common_prefix(common_prefix, constant_name)
|
|
# Handle special case where one of the constants is present in all constant
|
|
# names: remove last word from prefix.
|
|
# Example case: Node.ProcessThreadMessages and FLAG_PROCESS_THREAD_MESSAGES
|
|
if common_prefix in ClassDB.class_get_enum_constants(cls_name, enum_name, true):
|
|
common_prefix = common_prefix.rsplit("_", false, 1)[0]
|
|
|
|
var constants = PackedStringArray()
|
|
for constant_name in ClassDB.class_get_enum_constants(cls_name, enum_name, true):
|
|
constants.append("{csharp_constant_name} = {constant_value}L,".format({
|
|
csharp_constant_name = constant_name.substr(common_prefix.length()).to_pascal_case(),
|
|
constant_value = ClassDB.class_get_integer_constant(cls_name, constant_name),
|
|
}))
|
|
|
|
return """
|
|
{flags}
|
|
public enum {enum_name}{maybe_enum_suffix} : long
|
|
{
|
|
{constants}
|
|
}
|
|
""".dedent().format({
|
|
flags = "[Flags]" if ClassDB.is_class_enum_bitfield(cls_name, enum_name) else "",
|
|
enum_name = enum_name,
|
|
constants = "\n".join(constants).indent("\t"),
|
|
maybe_enum_suffix = "Enum" if _needs_enum_suffix(cls_name, enum_name) else "",
|
|
}).strip_edges()
|
|
|
|
|
|
static func _generate_integer_constant(cls_name: StringName, constant_name: StringName) -> String:
|
|
return "public const long {csharp_constant_name} = {constant_value}L;".format({
|
|
csharp_constant_name = constant_name.to_pascal_case(),
|
|
constant_value = ClassDB.class_get_integer_constant(cls_name, constant_name),
|
|
})
|
|
|
|
|
|
static func _generate_property(cls_name: StringName, property: Dictionary) -> String:
|
|
var property_name = property["name"]
|
|
var csharp_property_name = property_name.to_pascal_case()
|
|
var property_type = _get_property_type(cls_name, property)
|
|
|
|
var getset = PackedStringArray()
|
|
|
|
var getter = ClassDB.class_get_property_getter(cls_name, property_name)
|
|
if getter:
|
|
if _is_extension_class(cls_name):
|
|
getset.append("get => {get_cast}_object.Get(PropertyName.{csharp_property_name});".format({
|
|
get_cast = _property_get_cast(cls_name, property),
|
|
csharp_property_name = csharp_property_name,
|
|
}))
|
|
else:
|
|
getset.append("get => _object.{csharp_property_name};".format({
|
|
csharp_property_name = csharp_property_name,
|
|
}))
|
|
|
|
var setter = ClassDB.class_get_property_setter(cls_name, property_name)
|
|
if setter:
|
|
if _is_extension_class(cls_name):
|
|
getset.append("set => _object.Set(PropertyName.{csharp_property_name}, {set_cast}value);".format({
|
|
set_cast = _property_set_cast(property),
|
|
csharp_property_name = csharp_property_name,
|
|
}))
|
|
else:
|
|
getset.append("set => _object.{csharp_property_name} = value;".format({
|
|
csharp_property_name = csharp_property_name,
|
|
}))
|
|
|
|
return """
|
|
public {property_type} {csharp_property_name}
|
|
{
|
|
{getset}
|
|
}
|
|
""".dedent().format({
|
|
property_type = property_type,
|
|
csharp_property_name = csharp_property_name,
|
|
getset = "\n".join(getset).indent("\t"),
|
|
}).strip_edges()
|
|
|
|
|
|
static func _generate_method(cls_name: StringName, method: Dictionary) -> String:
|
|
var method_name = method["name"]
|
|
var csharp_method_name = method_name.to_pascal_case()
|
|
var return_type = _get_method_return_type(cls_name, method_name, method["return"])
|
|
var is_static = method["flags"] & METHOD_FLAG_STATIC
|
|
|
|
var arg_types = PackedStringArray()
|
|
var arg_names = PackedStringArray()
|
|
|
|
var args = PackedStringArray()
|
|
for argument in method["args"]:
|
|
var arg_type = _get_property_type(cls_name, argument)
|
|
var arg_name = "@" + argument["name"]
|
|
# hardcode type that cannot be known from reflection in GDScript
|
|
if method["name"] == "connect" and arg_name == "@flags":
|
|
arg_type = "uint"
|
|
args.append("{arg_type} {arg_name}".format({
|
|
arg_type = arg_type,
|
|
arg_name = arg_name,
|
|
}))
|
|
arg_types.append(arg_type)
|
|
if _property_is_enum(argument):
|
|
arg_names.append("(int)" + arg_name)
|
|
else:
|
|
arg_names.append(arg_name)
|
|
|
|
var implementation = PackedStringArray()
|
|
var default_args = method["default_args"]
|
|
var i = args.size() - default_args.size()
|
|
for default_value in default_args:
|
|
if default_value == null:
|
|
default_value = "default"
|
|
# handle enums
|
|
elif default_value is int and arg_types[i] != "int":
|
|
default_value = ("(%s)" % arg_types[i]) + str(default_value)
|
|
# C# requires the "f" suffix for float literals
|
|
elif default_value is float and arg_types[i] == "float":
|
|
default_value = "%sf" % default_value
|
|
# NOTE: don't move this branch below the String one, since most of the
|
|
# time when arg_types[i] == StringName, default_value is String
|
|
elif default_value is StringName or arg_types[i] == "Godot.StringName":
|
|
implementation.append('%s ??= "%s";' % [arg_names[i], default_value])
|
|
default_value = "null"
|
|
elif default_value is String:
|
|
default_value = '"%s"' % default_value
|
|
elif default_value is Array:
|
|
assert(default_value.is_empty(), "Populated Array not supported yet! " + str(default_value)) # TODO: support populated array as default value
|
|
implementation.append("%s ??= new();" % arg_names[i])
|
|
default_value = "null"
|
|
elif default_value is Dictionary:
|
|
assert(default_value.is_empty(), "Populated Dictionary not supported yet! " + str(default_value)) # TODO: support populated dictionary as default value
|
|
implementation.append("%s ??= new();" % arg_names[i])
|
|
default_value = "null"
|
|
elif (
|
|
default_value is Vector2 or default_value is Vector3 or default_value is Vector4
|
|
or default_value is Color
|
|
):
|
|
args[i] = args[i].replace(arg_types[i], arg_types[i] + "?")
|
|
var impl = "%s ??= new%s;" % [arg_names[i], default_value]
|
|
if not OS.has_feature("double"):
|
|
impl = impl.replace(",", "f,").replace(")", "f)")
|
|
implementation.append(impl)
|
|
default_value = "null"
|
|
elif (
|
|
default_value is PackedByteArray
|
|
or default_value is PackedInt32Array or default_value is PackedInt64Array
|
|
or default_value is PackedFloat32Array or default_value is PackedFloat64Array
|
|
or default_value is PackedVector2Array or default_value is PackedVector3Array or default_value is PackedVector4Array
|
|
or default_value is PackedColorArray
|
|
):
|
|
assert(default_value.is_empty(), "Populated Packed Array not supported yet! " + str(default_value))
|
|
implementation.append("%s ??= System.Array.Empty<%s>();" % [arg_names[i], arg_types[i].replace("[]", "")])
|
|
default_value = "null"
|
|
elif default_value is Transform2D:
|
|
assert(default_value == Transform2D.IDENTITY, "Only identity Transform2D is supported as default value")
|
|
args[i] = args[i].replace(arg_types[i], arg_types[i] + "?")
|
|
implementation.append("%s ??= Godot.Transform2D.Identity;" % arg_names[i])
|
|
default_value = "null"
|
|
elif default_value is Transform3D:
|
|
assert(default_value == Transform3D.IDENTITY, "Only identity Transform3D is supported as default value")
|
|
args[i] = args[i].replace(arg_types[i], arg_types[i] + "?")
|
|
implementation.append("%s ??= Godot.Transform3D.Identity;" % arg_names[i])
|
|
default_value = "null"
|
|
args[i] += " = " + str(default_value)
|
|
i += 1
|
|
|
|
if method["flags"] & METHOD_FLAG_VARARG:
|
|
args.append("params Variant[] varargs")
|
|
arg_names.append("varargs")
|
|
|
|
if _is_extension_class(cls_name):
|
|
arg_names.insert(0, "MethodName.{csharp_method_name}".format({
|
|
csharp_method_name = csharp_method_name,
|
|
}))
|
|
if is_static:
|
|
implementation.append("{maybe_return}ClassDB.ClassCallStatic(NativeName, {arg_names});".format({
|
|
arg_names = ", ".join(arg_names),
|
|
maybe_return = "return " + _property_get_cast(cls_name, method["return"]) if return_type != "void" else "",
|
|
}))
|
|
else:
|
|
implementation.append("{maybe_return}_object.Call({arg_names});".format({
|
|
arg_names = ", ".join(arg_names),
|
|
maybe_return = "return " + _property_get_cast(cls_name, method["return"]) if return_type != "void" else "",
|
|
}))
|
|
else:
|
|
if is_static:
|
|
implementation.append("{maybe_return}{engine_class}.{csharp_method_name}({arg_names});".format({
|
|
arg_names = ", ".join(arg_names),
|
|
engine_class = _first_non_extension_parent(cls_name),
|
|
csharp_method_name = csharp_method_name,
|
|
maybe_return = "return " if return_type != "void" else "",
|
|
}))
|
|
else:
|
|
implementation.append("{maybe_return}_object.{csharp_method_name}({arg_names});".format({
|
|
arg_names = ", ".join(arg_names),
|
|
csharp_method_name = csharp_method_name,
|
|
maybe_return = "return " if return_type != "void" else "",
|
|
}))
|
|
|
|
return """
|
|
public {maybe_static}{maybe_override}{return_type} {csharp_method_name}({args})
|
|
{
|
|
{implementation}
|
|
}
|
|
""".dedent().format({
|
|
args = ", ".join(args),
|
|
csharp_method_name = csharp_method_name,
|
|
implementation = "\n".join(implementation).indent("\t"),
|
|
maybe_override = "override " if csharp_method_name == "ToString" else "",
|
|
maybe_static = "static " if is_static else "",
|
|
return_type = return_type,
|
|
}).strip_edges()
|
|
|
|
|
|
static func _generate_signal(cls_name: StringName, sig: Dictionary):
|
|
var signal_name = sig["name"]
|
|
var csharp_signal_name = signal_name.to_pascal_case()
|
|
var return_type = _get_method_return_type(cls_name, signal_name, sig["return"])
|
|
|
|
var arg_types = PackedStringArray()
|
|
for argument in sig["args"]:
|
|
var arg_type = _get_property_type(cls_name, argument)
|
|
arg_types.append(arg_type)
|
|
|
|
var delegate_type
|
|
if return_type == "void":
|
|
if not arg_types.is_empty():
|
|
delegate_type = "Action<{arg_types}>".format({
|
|
arg_types = ", ".join(arg_types)
|
|
})
|
|
else:
|
|
delegate_type = "Action"
|
|
else:
|
|
arg_types.append(return_type)
|
|
delegate_type = "Func<{arg_types}>".format({
|
|
arg_types = ", ".join(arg_types)
|
|
})
|
|
|
|
return """
|
|
public event {delegate_type} {csharp_signal_name}
|
|
{
|
|
add
|
|
{
|
|
Connect(SignalName.{csharp_signal_name}, Callable.From(value));
|
|
}
|
|
remove
|
|
{
|
|
Disconnect(SignalName.{csharp_signal_name}, Callable.From(value));
|
|
}
|
|
}
|
|
""".dedent().format({
|
|
delegate_type = delegate_type,
|
|
csharp_signal_name = csharp_signal_name,
|
|
}).strip_edges()
|
|
|
|
|
|
static func _property_is_enum(property: Dictionary) -> bool:
|
|
return property["usage"] & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)
|
|
|
|
|
|
static func _get_property_type(cls_name: StringName, property: Dictionary) -> String:
|
|
match property["type"]:
|
|
TYPE_NIL:
|
|
return "Variant"
|
|
TYPE_BOOL:
|
|
return "bool"
|
|
TYPE_INT:
|
|
if _property_is_enum(property):
|
|
var enum_name = property["class_name"]
|
|
if enum_name == "Error":
|
|
return "Godot.Error"
|
|
var split = enum_name.split(".")
|
|
if split.size() == 1:
|
|
return enum_name + ("Enum" if _needs_enum_suffix(cls_name, enum_name) else "")
|
|
else:
|
|
return enum_name + ("Enum" if _needs_enum_suffix(split[0], split[1]) else "")
|
|
return "int"
|
|
TYPE_FLOAT:
|
|
return "double" if OS.has_feature("double") else "float"
|
|
TYPE_STRING:
|
|
return "string"
|
|
TYPE_VECTOR2I:
|
|
return "Godot.Vector2I"
|
|
TYPE_RECT2I:
|
|
return "Godot.Rect2I"
|
|
TYPE_VECTOR3I:
|
|
return "Godot.Vector3I"
|
|
TYPE_VECTOR4I:
|
|
return "Godot.Vector4I"
|
|
TYPE_AABB:
|
|
return "Godot.Aabb"
|
|
TYPE_RID:
|
|
return "Godot.Rid"
|
|
TYPE_OBJECT:
|
|
if property["class_name"] and property["class_name"] != "Object":
|
|
return _pascal_to_pascal_case(_get_class_from_class_name(property["class_name"]))
|
|
else:
|
|
return "GodotObject"
|
|
TYPE_ARRAY:
|
|
if property["hint"] & PROPERTY_HINT_ARRAY_TYPE:
|
|
return "Godot.Collections.Array<%s>" % _get_mapped_variant_type(property["hint_string"])
|
|
else:
|
|
return "Godot.Collections.Array"
|
|
TYPE_DICTIONARY:
|
|
return "Godot.Collections.Dictionary"
|
|
TYPE_PACKED_BYTE_ARRAY:
|
|
return "byte[]"
|
|
TYPE_PACKED_INT32_ARRAY:
|
|
return "int[]"
|
|
TYPE_PACKED_INT64_ARRAY:
|
|
return "long[]"
|
|
TYPE_PACKED_FLOAT32_ARRAY:
|
|
return "float[]"
|
|
TYPE_PACKED_FLOAT64_ARRAY:
|
|
return "double[]"
|
|
TYPE_PACKED_STRING_ARRAY:
|
|
return "string[]"
|
|
TYPE_PACKED_VECTOR2_ARRAY:
|
|
return "Godot.Vector2[]"
|
|
TYPE_PACKED_VECTOR3_ARRAY:
|
|
return "Godot.Vector3[]"
|
|
TYPE_PACKED_VECTOR4_ARRAY:
|
|
return "Godot.Vector4[]"
|
|
TYPE_PACKED_COLOR_ARRAY:
|
|
return "Godot.Color[]"
|
|
var t:
|
|
return "Godot." + type_string(t)
|
|
|
|
|
|
static func _get_mapped_variant_type(variant_type_name: String) -> String:
|
|
var _type_map = {
|
|
"Variant": "Variant",
|
|
type_string(TYPE_BOOL): "bool",
|
|
type_string(TYPE_INT): "int",
|
|
type_string(TYPE_FLOAT): "double" if OS.has_feature("double") else "float",
|
|
type_string(TYPE_STRING): "string",
|
|
type_string(TYPE_STRING_NAME): "StringName",
|
|
type_string(TYPE_VECTOR2I): "Godot.Vector2I",
|
|
type_string(TYPE_RECT2I): "Godot.Rect2I",
|
|
type_string(TYPE_VECTOR3I): "Godot.Vector3I",
|
|
type_string(TYPE_VECTOR4I): "Godot.Vector4I",
|
|
type_string(TYPE_AABB): "Godot.Aabb",
|
|
type_string(TYPE_RID): "Godot.Rid",
|
|
type_string(TYPE_OBJECT): "GodotObject",
|
|
type_string(TYPE_ARRAY): "Godot.Collections.Array",
|
|
type_string(TYPE_DICTIONARY): "Godot.Collections.Dictionary",
|
|
type_string(TYPE_PACKED_BYTE_ARRAY): "byte[]",
|
|
type_string(TYPE_PACKED_INT32_ARRAY): "int[]",
|
|
type_string(TYPE_PACKED_INT64_ARRAY): "long[]",
|
|
type_string(TYPE_PACKED_FLOAT32_ARRAY): "float[]",
|
|
type_string(TYPE_PACKED_FLOAT64_ARRAY): "double[]",
|
|
type_string(TYPE_PACKED_STRING_ARRAY): "string[]",
|
|
type_string(TYPE_PACKED_VECTOR2_ARRAY): "Godot.Vector2[]",
|
|
type_string(TYPE_PACKED_VECTOR3_ARRAY): "Godot.Vector3[]",
|
|
type_string(TYPE_PACKED_VECTOR4_ARRAY): "Godot.Vector4[]",
|
|
type_string(TYPE_PACKED_COLOR_ARRAY): "Godot.Color[]",
|
|
}
|
|
return _type_map.get(variant_type_name, "Godot." + variant_type_name)
|
|
|
|
|
|
static func _property_get_cast(cls_name: StringName, property: Dictionary):
|
|
var property_type = _get_property_type(cls_name, property)
|
|
if _property_is_enum(property):
|
|
return "(%s)(int)" % property_type
|
|
else:
|
|
return "(%s)" % property_type
|
|
|
|
|
|
static func _property_set_cast(property: Dictionary):
|
|
if _property_is_enum(property):
|
|
return "(int)"
|
|
else:
|
|
return ""
|
|
|
|
|
|
static func _is_extension_class(cls_name: StringName) -> bool:
|
|
return ClassDB.class_get_api_type(cls_name) in [
|
|
ClassDB.APIType.API_EXTENSION,
|
|
ClassDB.APIType.API_EDITOR_EXTENSION,
|
|
]
|
|
|
|
|
|
static func _is_editor_extension_class(cls_name: StringName) -> bool:
|
|
return ClassDB.class_get_api_type(cls_name) == ClassDB.APIType.API_EDITOR_EXTENSION
|
|
|
|
|
|
static func _first_non_extension_parent(cls_name: StringName) -> StringName:
|
|
while _is_extension_class(cls_name):
|
|
cls_name = ClassDB.get_parent_class(cls_name)
|
|
return cls_name
|
|
|
|
|
|
static func _get_method_return_type(cls_name: StringName, method_name: StringName, method_return: Dictionary) -> String:
|
|
# hardcode type that cannot be known from reflection in GDScript
|
|
if method_name == "get_instance_id":
|
|
return "ulong"
|
|
|
|
if method_return["type"] == TYPE_NIL:
|
|
if method_return["usage"] & PROPERTY_USAGE_NIL_IS_VARIANT:
|
|
return "Variant"
|
|
else:
|
|
return "void"
|
|
else:
|
|
return _get_property_type(cls_name, method_return)
|
|
|
|
|
|
static func _get_parent_classes(cls_name: StringName) -> Array[StringName]:
|
|
var parent_classes = [] as Array[StringName]
|
|
while true:
|
|
cls_name = ClassDB.get_parent_class(cls_name)
|
|
parent_classes.append(cls_name)
|
|
if cls_name == "Object":
|
|
break
|
|
return parent_classes
|
|
|
|
|
|
static func _generate_strings_class(cls_name: StringName, string_name_type: StringNameType, string_names: PackedStringArray) -> String:
|
|
var parent_class = ClassDB.get_parent_class(cls_name)
|
|
var lines = PackedStringArray()
|
|
for name in string_names:
|
|
if string_name_type == StringNameType.METHOD_NAME and ClassDB.class_has_method(parent_class, name):
|
|
continue
|
|
if string_name_type == StringNameType.SIGNAL_NAME and ClassDB.class_has_signal(parent_class, name):
|
|
continue
|
|
lines.append("public static readonly StringName {cs_name} = \"{name}\";".format({
|
|
cs_name = name.to_pascal_case(),
|
|
name = name,
|
|
}))
|
|
return """
|
|
public {maybe_new}class {strings_class} : {parent_class}.{strings_class}
|
|
{
|
|
{lines}
|
|
}
|
|
""".dedent().format({
|
|
lines = "\n".join(lines).indent("\t"),
|
|
maybe_new = "new " if _is_extension_class(parent_class) else "",
|
|
parent_class = parent_class,
|
|
strings_class = StringNameTypeName[string_name_type],
|
|
}).strip_edges()
|
|
|
|
|
|
static func _get_common_prefix(s1: String, s2: String) -> String:
|
|
var common_length = min(s1.length(), s2.length())
|
|
for i in range(common_length):
|
|
if s1[i] != s2[i]:
|
|
return s1.substr(0, i)
|
|
return s1.substr(0, common_length)
|
|
|
|
|
|
static func _get_class_from_class_name(cls_name: String) -> String:
|
|
var classes = cls_name.split(",")
|
|
if classes.size() == 1:
|
|
return cls_name
|
|
|
|
# Handle special case where 2 or more class names are present separated
|
|
# by ",": calculate the common parent class.
|
|
# Example case: CanvasItem.material uses "CanvasItemMaterial,ShaderMaterial"
|
|
var parent_classes = _get_parent_classes(classes[0])
|
|
for i in range(1, classes.size()):
|
|
var test_cls = classes[i]
|
|
while not ClassDB.is_parent_class(test_cls, parent_classes[0]):
|
|
parent_classes.pop_front()
|
|
return parent_classes[0]
|
|
|
|
|
|
static func _needs_enum_suffix(cls_name: StringName, enum_name: String) -> bool:
|
|
var snake_case_enum_name = enum_name.to_snake_case()
|
|
if ClassDB.class_has_method(cls_name, snake_case_enum_name):
|
|
return true
|
|
if ClassDB.class_has_signal(cls_name, snake_case_enum_name):
|
|
return true
|
|
var properties = ClassDB.class_get_property_list(cls_name)
|
|
for property in properties:
|
|
if snake_case_enum_name == property["name"]:
|
|
return true
|
|
return false
|
|
|
|
|
|
# Pascal case conversion used for class names.
|
|
# Replicates the logic from `godot/modules/mono/utils/naming_utils.cpp`
|
|
static func _is_ascii_upper_case(c: String) -> bool:
|
|
return c.to_upper() == c
|
|
|
|
|
|
static func _is_ascii_lower_case(c: String) -> bool:
|
|
return c.to_lower() == c
|
|
|
|
|
|
static func _is_digit(c: String) -> bool:
|
|
return c >= "0" and c <= "9"
|
|
|
|
|
|
static func _split_pascal_case(p_identifier: String) -> PackedStringArray:
|
|
var parts := PackedStringArray()
|
|
var current_part_start := 0
|
|
var prev_was_upper := _is_ascii_upper_case(p_identifier[0])
|
|
for i in range(1, p_identifier.length()):
|
|
if prev_was_upper:
|
|
if _is_digit(p_identifier[i]) or _is_ascii_lower_case(p_identifier[i]):
|
|
if not _is_digit(p_identifier[i]):
|
|
# These conditions only apply when the separator is not a digit.
|
|
if i - current_part_start == 1:
|
|
# Upper character was only the beginning of a word.
|
|
prev_was_upper = false
|
|
continue
|
|
if i != p_identifier.length():
|
|
# If this is not the last character, the last uppercase
|
|
# character is the start of the next word.
|
|
i -= 1
|
|
if i - current_part_start > 0:
|
|
parts.append(p_identifier.substr(current_part_start, i - current_part_start))
|
|
current_part_start = i
|
|
prev_was_upper = false
|
|
else:
|
|
if _is_digit(p_identifier[i]) or _is_ascii_upper_case(p_identifier[i]):
|
|
parts.append(p_identifier.substr(current_part_start, i - current_part_start))
|
|
current_part_start = i
|
|
prev_was_upper = true
|
|
|
|
# Add the rest of the identifier as the last part.
|
|
if current_part_start != p_identifier.length():
|
|
parts.append(p_identifier.substr(current_part_start))
|
|
return parts
|
|
|
|
|
|
static func _pascal_to_pascal_case(p_identifier: String) -> String:
|
|
if p_identifier.length() == 0:
|
|
return p_identifier
|
|
if p_identifier.length() <= 2:
|
|
return p_identifier.to_upper()
|
|
if PASCAL_CASE_NAME_OVERRIDES.has(p_identifier):
|
|
return PASCAL_CASE_NAME_OVERRIDES[p_identifier]
|
|
|
|
var parts := _split_pascal_case(p_identifier)
|
|
var ret := ""
|
|
for part in parts:
|
|
if PASCAL_CASE_PART_OVERRIDES.has(part):
|
|
ret += PASCAL_CASE_PART_OVERRIDES[part]
|
|
continue
|
|
|
|
if part.length() <= 2 and _is_ascii_upper_case(part):
|
|
ret += part.to_upper()
|
|
continue
|
|
|
|
part[0] = part[0].to_upper()
|
|
for i in range(1, part.length()):
|
|
if _is_digit(part[i - 1]):
|
|
# Use uppercase after digits.
|
|
part[i] = part[i].to_upper()
|
|
else:
|
|
part[i] = part[i].to_lower()
|
|
ret += part
|
|
return ret
|