diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml index 0c2d7d2e6241..9a8a4a8f1912 100644 --- a/.github/workflows/static_checks.yml +++ b/.github/workflows/static_checks.yml @@ -37,7 +37,7 @@ jobs: fi echo "$files" >> changed.txt cat changed.txt - files=$(echo "$files" | grep -v 'thirdparty' | xargs -I {} sh -c 'echo "./{}"' | tr '\n' ' ') + files=$(echo "$files" | grep -v 'thirdparty' | xargs -I {} sh -c 'echo "\"./{}\""' | tr '\n' ' ') echo "CHANGED_FILES=$files" >> $GITHUB_ENV # This needs to happen before Python and npm execution; it must happen before any extra files are written. diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GenericExports.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GenericExports.cs new file mode 100644 index 000000000000..a2e0560618b8 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GenericExports.cs @@ -0,0 +1,52 @@ +using Godot.Collections; +using System; + +namespace Godot.SourceGenerators.Sample; + +public partial class GenericExports<[MustBeVariant] T> : GodotObject +{ + [Export] public T RegularField; + [Export] public T RegularProperty { get; set; } + + [Export] public Array ArrayProperty { get; set; } + + [Signal] + public delegate T GenericSignalEventHandler(T var); + + public T GenericMethod(T var) + { + return var; + } +} + +public partial class GenericExportsVector2 : GenericExports +{ +} + +public partial class GenericExportsRect2 : GenericExports +{ +} + +public partial class GenericExportsMultiple : GodotObject +{ + [Export] public Array ArrayExport { get; set; } + + // This is not valid because TSome is not [MustBeVariant] + // [Export] public TSome AnotherArray { get; set; } + + // You can still use TSome, just not exported. + public TSome NonExportField; +} + +// Doesn't require the [MustBeVariant] attribute because T is constrained to GodotObject or Enum already. +public partial class InferredVariantConstraintGodotObject : GodotObject + where T : GodotObject +{ + [Export] public T Exported; +} + +public partial class InferredVariantConstraintEnum : GodotObject + where T : Enum +{ + [Export] public T Exported; +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs index 294932eb9a38..49949f85cb15 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs @@ -13,6 +13,15 @@ await CSharpSourceGeneratorVerifier.Verify( ); } + [Fact] + public async void Generic() + { + await CSharpSourceGeneratorVerifier.Verify( + "Generic.cs", + "Generic_T_ScriptMethods.generated.cs" + ); + } + [Fact] public async void ScriptBoilerplate() { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs index 4f6b50cf0285..00266d4067e9 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs @@ -48,7 +48,7 @@ public async void Generic() { var verifier = CSharpSourceGeneratorVerifier.MakeVerifier( new string[] { "Generic.cs" }, - new string[] { "Generic(Of T)_ScriptPath.generated.cs" } + new string[] { "Generic_T_ScriptPath.generated.cs" } ); verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic<>" })); await verifier.RunAsync(); @@ -59,7 +59,7 @@ public async void GenericMultipleClassesSameName() { var verifier = CSharpSourceGeneratorVerifier.MakeVerifier( Array.Empty(), - new string[] { "Generic(Of T)_ScriptPath.generated.cs" } + new string[] { "Generic_T_ScriptPath.generated.cs" } ); verifier.TestState.Sources.Add(("Generic.cs", File.ReadAllText(Path.Combine(Constants.SourceFolderPath, "Generic.GD0003.cs")))); verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic<>", "global::Generic<,>", "global::Generic" })); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs index 724fb164e022..b782b7e9dca1 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs @@ -63,7 +63,16 @@ public async void AbstractGenericNode() { await CSharpSourceGeneratorVerifier.Verify( "AbstractGenericNode.cs", - "AbstractGenericNode(Of T)_ScriptProperties.generated.cs" + "AbstractGenericNode_T_ScriptProperties.generated.cs" + ); + } + + [Fact] + public async void Generic() + { + await CSharpSourceGeneratorVerifier.Verify( + "Generic.cs", + "Generic_T_ScriptProperties.generated.cs" ); } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs index 2a783cedcedd..1b41a0840ddb 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs @@ -12,4 +12,13 @@ await CSharpSourceGeneratorVerifier.Verify( "EventSignals_ScriptSignals.generated.cs" ); } + + [Fact] + public async void Generic() + { + await CSharpSourceGeneratorVerifier.Verify( + "Generic.cs", + "Generic_T_ScriptSignals.generated.cs" + ); + } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode_T_ScriptProperties.generated.cs similarity index 89% rename from modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs rename to modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode_T_ScriptProperties.generated.cs index a561c5fc0db6..e7be624fcc32 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode(Of T)_ScriptProperties.generated.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AbstractGenericNode_T_ScriptProperties.generated.cs @@ -42,7 +42,7 @@ protected override bool GetGodotClassPropertyValue(in godot_string_name name, ou internal new static global::System.Collections.Generic.List GetGodotPropertyList() { var properties = new global::System.Collections.Generic.List(); - properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.MyArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType>(name: PropertyName.MyArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); return properties; } #pragma warning restore CS0109 diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptMethods.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptMethods.generated.cs new file mode 100644 index 000000000000..e58dd815dbf1 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptMethods.generated.cs @@ -0,0 +1,49 @@ +using Godot; +using Godot.NativeInterop; + +partial class Generic +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// + /// Cached StringNames for the methods contained in this class, for fast lookup. + /// + public new class MethodName : global::Godot.GodotObject.MethodName { + /// + /// Cached name for the 'GenericMethod' method. + /// + public new static readonly global::Godot.StringName GenericMethod = "GenericMethod"; + } + /// + /// Get the method information for all the methods declared in this class. + /// This method is used by Godot to register the available methods in the editor. + /// Do not call this method. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List GetGodotMethodList() + { + var methods = new global::System.Collections.Generic.List(1); + methods.Add(new(name: MethodName.GenericMethod, returnVal: global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType(name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType(name: "var", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + return methods; + } +#pragma warning restore CS0109 + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool InvokeGodotClassMethod(in godot_string_name method, NativeVariantPtrArgs args, out godot_variant ret) + { + if (method == MethodName.GenericMethod && args.Count == 1) { + var callRet = GenericMethod(global::Godot.NativeInterop.VariantUtils.ConvertTo(args[0])); + ret = global::Godot.NativeInterop.VariantUtils.CreateFrom(callRet); + return true; + } + return base.InvokeGodotClassMethod(method, args, out ret); + } + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool HasGodotClassMethod(in godot_string_name method) + { + if (method == MethodName.GenericMethod) { + return true; + } + return base.HasGodotClassMethod(method); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic(Of T)_ScriptPath.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptPath.generated.cs similarity index 100% rename from modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic(Of T)_ScriptPath.generated.cs rename to modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptPath.generated.cs diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptProperties.generated.cs new file mode 100644 index 000000000000..6aecf7a5e152 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptProperties.generated.cs @@ -0,0 +1,75 @@ +using Godot; +using Godot.NativeInterop; + +partial class Generic +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// + public new class PropertyName : global::Godot.GodotObject.PropertyName { + /// + /// Cached name for the 'RegularProperty' property. + /// + public new static readonly global::Godot.StringName RegularProperty = "RegularProperty"; + /// + /// Cached name for the 'ArrayProperty' property. + /// + public new static readonly global::Godot.StringName ArrayProperty = "ArrayProperty"; + /// + /// Cached name for the 'RegularField' field. + /// + public new static readonly global::Godot.StringName RegularField = "RegularField"; + } + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName.RegularProperty) { + this.RegularProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo(value); + return true; + } + if (name == PropertyName.ArrayProperty) { + this.ArrayProperty = global::Godot.NativeInterop.VariantUtils.ConvertToArray(value); + return true; + } + if (name == PropertyName.RegularField) { + this.RegularField = global::Godot.NativeInterop.VariantUtils.ConvertTo(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName.RegularProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom(this.RegularProperty); + return true; + } + if (name == PropertyName.ArrayProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromArray(this.ArrayProperty); + return true; + } + if (name == PropertyName.RegularField) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom(this.RegularField); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List(); + properties.Add(global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType(name: PropertyName.RegularField, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType(name: PropertyName.RegularProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType>(name: PropertyName.ArrayProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptSignals.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptSignals.generated.cs new file mode 100644 index 000000000000..77efec5d3d2f --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic_T_ScriptSignals.generated.cs @@ -0,0 +1,54 @@ +using Godot; +using Godot.NativeInterop; + +partial class Generic +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// + /// Cached StringNames for the signals contained in this class, for fast lookup. + /// + public new class SignalName : global::Godot.GodotObject.SignalName { + /// + /// Cached name for the 'GenericSignal' signal. + /// + public new static readonly global::Godot.StringName GenericSignal = "GenericSignal"; + } + /// + /// Get the signal information for all the signals declared in this class. + /// This method is used by Godot to register the available signals in the editor. + /// Do not call this method. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List GetGodotSignalList() + { + var signals = new global::System.Collections.Generic.List(1); + signals.Add(new(name: SignalName.GenericSignal, returnVal: global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType(name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType(name: "var", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + return signals; + } +#pragma warning restore CS0109 + private global::Generic.GenericSignalEventHandler backing_GenericSignal; + /// + public event global::Generic.GenericSignalEventHandler GenericSignal { + add => backing_GenericSignal += value; + remove => backing_GenericSignal -= value; +} + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, NativeVariantPtrArgs args) + { + if (signal == SignalName.GenericSignal && args.Count == 1) { + backing_GenericSignal?.Invoke(global::Godot.NativeInterop.VariantUtils.ConvertTo(args[0])); + return; + } + base.RaiseGodotClassSignalCallbacks(signal, args); + } + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool HasGodotClassSignal(in godot_string_name signal) + { + if (signal == SignalName.GenericSignal) { + return true; + } + return base.HasGodotClassSignal(signal); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs index ce8a7fe2187d..768a9809e99b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs @@ -1,6 +1,18 @@ using Godot; +using Godot.Collections; -public partial class Generic : GodotObject +public partial class Generic<[MustBeVariant] T> : GodotObject { - private int _field; + [Export] public T RegularField; + [Export] public T RegularProperty { get; set; } + + [Export] public Array ArrayProperty { get; set; } + + [Signal] + public delegate T GenericSignalEventHandler(T var); + + public T GenericMethod(T var) + { + return var; + } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index 957d5789df80..e03a9e6b61a3 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -243,8 +243,8 @@ static void ParenEnclosedFullQualifiedSyntax(SyntaxNode node, SemanticModel sm, public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedName) => qualifiedName // AddSource() doesn't support angle brackets - .Replace("<", "(Of ") - .Replace(">", ")"); + .Replace("<", "_") + .Replace(">", ""); public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol) => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ExportAttr; @@ -358,5 +358,61 @@ public static string Path(this Location location) public static int StartLine(this Location location) => location.SourceTree?.GetLineSpan(location.SourceSpan).StartLinePosition.Line ?? location.GetLineSpan().StartLinePosition.Line; + + public static bool HasMustBeVariantAttribute(this ITypeParameterSymbol typeParameter) + { + return typeParameter.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false); + } + + public static bool IsVariantConstrained(this ITypeParameterSymbol symbol) + { + return symbol.HasMustBeVariantAttribute() || + symbol.ConstraintTypes.OfType().Any(t => t.InheritsFrom("GodotSharp", GodotClasses.GodotObject) || t.InheritsFrom("System.Runtime", "System.Enum")); + } + + public static bool HasTypeParameterTypeArguments(this INamedTypeSymbol typeSymbol) + { + foreach (var typeArgument in typeSymbol.TypeArguments) + if (typeArgument is ITypeParameterSymbol || + typeArgument is INamedTypeSymbol namedType && namedType.HasTypeParameterTypeArguments()) + return true; + return false; + } + + public static void AppendPropertyInfo(this StringBuilder source, PropertyInfo propertyInfo, string nameFormat) + { + if (propertyInfo.VariantType.HasValue) + { + source.Append("new(type: (global::Godot.Variant.Type)") + .Append((int)propertyInfo.VariantType) + .Append(", "); + } + else + { + source.Append("global::Godot.Bridge.GenericUtils.PropertyInfoFromGenericType<") + .Append(propertyInfo.PropertyType!.FullQualifiedNameIncludeGlobal()) + .Append(">("); + } + + source.Append("name: ") + .Append(string.Format(nameFormat, propertyInfo.Name)) + .Append(", hint: (global::Godot.PropertyHint)") + .Append((int)propertyInfo.Hint) + .Append(", hintString: \"") + .Append(propertyInfo.HintString) + .Append("\", usage: (global::Godot.PropertyUsageFlags)") + .Append((int)propertyInfo.Usage) + .Append(", exported: ") + .Append(propertyInfo.Exported ? "true" : "false"); + + if (propertyInfo.ClassName != null) + { + source.Append(", className: new global::Godot.StringName(\"") + .Append(propertyInfo.ClassName) + .Append("\")"); + } + + source.Append(")"); + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs index bfb735e72ff2..0e6cc5660253 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs @@ -70,5 +70,10 @@ public enum MarshalType GodotArray, GodotGenericDictionary, GodotGenericArray, + + // Generic + GenericType, + GenericGodotGenericDictionary, + GenericGodotGenericArray, } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index d27283295052..6d6a8aeedda2 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -211,6 +211,9 @@ INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName) if (type.SimpleDerivesFrom(typeCache.GodotObjectType)) return MarshalType.GodotObjectOrDerived; + if (type is ITypeParameterSymbol typeParam && typeParam.IsVariantConstrained()) + return MarshalType.GenericType; + if (type.ContainingAssembly?.Name == "GodotSharp") { switch (type.ContainingNamespace?.Name) @@ -224,18 +227,27 @@ INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName) }; case "Collections" when type.ContainingNamespace?.FullQualifiedNameOmitGlobal() == "Godot.Collections": + { + INamedTypeSymbol namedType = (INamedTypeSymbol)type; + bool hasMoreTypeArguments = namedType.HasTypeParameterTypeArguments(); + return type switch { { Name: "Dictionary" } => - type is INamedTypeSymbol { IsGenericType: false } ? - MarshalType.GodotDictionary : - MarshalType.GodotGenericDictionary, + namedType.IsGenericType ? + hasMoreTypeArguments ? + MarshalType.GenericGodotGenericDictionary : + MarshalType.GodotGenericDictionary : + MarshalType.GodotDictionary, { Name: "Array" } => - type is INamedTypeSymbol { IsGenericType: false } ? - MarshalType.GodotArray : - MarshalType.GodotGenericArray, + namedType.IsGenericType ? + hasMoreTypeArguments ? + MarshalType.GenericGodotGenericArray : + MarshalType.GodotGenericArray : + MarshalType.GodotArray, _ => null }; + } } } } @@ -313,12 +325,12 @@ public static StringBuilder AppendNativeVariantToManagedExpr(this StringBuilder ((IArrayTypeSymbol)typeSymbol).ElementType.FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")"), // We need a special case for generic Godot collections and GodotObjectOrDerived[], because VariantUtils.ConvertTo is slower - MarshalType.GodotGenericDictionary => + MarshalType.GodotGenericDictionary or MarshalType.GenericGodotGenericDictionary => source.Append(VariantUtils, ".ConvertToDictionary<", ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ", ", ((INamedTypeSymbol)typeSymbol).TypeArguments[1].FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")"), - MarshalType.GodotGenericArray => + MarshalType.GodotGenericArray or MarshalType.GenericGodotGenericArray => source.Append(VariantUtils, ".ConvertToArray<", ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")"), @@ -336,9 +348,9 @@ public static StringBuilder AppendManagedToNativeVariantExpr(this StringBuilder MarshalType.GodotObjectOrDerivedArray => source.Append(VariantUtils, ".CreateFromSystemArrayOfGodotObject(", inputExpr, ")"), // We need a special case for generic Godot collections and GodotObjectOrDerived[], because VariantUtils.CreateFrom is slower - MarshalType.GodotGenericDictionary => + MarshalType.GodotGenericDictionary or MarshalType.GenericGodotGenericDictionary => source.Append(VariantUtils, ".CreateFromDictionary(", inputExpr, ")"), - MarshalType.GodotGenericArray => + MarshalType.GodotGenericArray or MarshalType.GenericGodotGenericArray => source.Append(VariantUtils, ".CreateFromArray(", inputExpr, ")"), _ => source.Append(VariantUtils, ".CreateFrom<", typeSymbol.FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")"), @@ -355,11 +367,11 @@ public static StringBuilder AppendVariantToManagedExpr(this StringBuilder source source.Append(inputExpr, ".AsGodotObjectArray<", ((IArrayTypeSymbol)typeSymbol).ElementType.FullQualifiedNameIncludeGlobal(), ">()"), // We need a special case for generic Godot collections and GodotObjectOrDerived[], because Variant.As is slower - MarshalType.GodotGenericDictionary => + MarshalType.GodotGenericDictionary or MarshalType.GenericGodotGenericDictionary => source.Append(inputExpr, ".AsGodotDictionary<", ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ", ", ((INamedTypeSymbol)typeSymbol).TypeArguments[1].FullQualifiedNameIncludeGlobal(), ">()"), - MarshalType.GodotGenericArray => + MarshalType.GodotGenericArray or MarshalType.GenericGodotGenericArray => source.Append(inputExpr, ".AsGodotArray<", ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ">()"), _ => source.Append(inputExpr, ".As<", @@ -376,7 +388,8 @@ public static StringBuilder AppendManagedToVariantExpr(this StringBuilder source MarshalType.GodotObjectOrDerivedArray => source.Append("global::Godot.Variant.CreateFrom(", inputExpr, ")"), // We need a special case for generic Godot collections, because Variant.From is slower - MarshalType.GodotGenericDictionary or MarshalType.GodotGenericArray => + MarshalType.GodotGenericDictionary or MarshalType.GodotGenericArray or + MarshalType.GenericGodotGenericDictionary or MarshalType.GenericGodotGenericArray => source.Append("global::Godot.Variant.CreateFrom(", inputExpr, ")"), _ => source.Append("global::Godot.Variant.From<", typeSymbol.FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")") diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs index e22cc951b16f..7620786f1d3a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs @@ -1,15 +1,18 @@ +using Microsoft.CodeAnalysis; + namespace Godot.SourceGenerators { internal readonly struct PropertyInfo { - public PropertyInfo(VariantType type, string name, PropertyHint hint, + public PropertyInfo(VariantType? variantType, ITypeSymbol? propertyType, string name, PropertyHint hint, string? hintString, PropertyUsageFlags usage, bool exported) - : this(type, name, hint, hintString, usage, className: null, exported) { } + : this(variantType, propertyType, name, hint, hintString, usage, className: null, exported) { } - public PropertyInfo(VariantType type, string name, PropertyHint hint, + public PropertyInfo(VariantType? variantType, ITypeSymbol? propertyType, string name, PropertyHint hint, string? hintString, PropertyUsageFlags usage, string? className, bool exported) { - Type = type; + VariantType = variantType; + PropertyType = propertyType; Name = name; Hint = hint; HintString = hintString; @@ -18,7 +21,8 @@ public PropertyInfo(VariantType type, string name, PropertyHint hint, Exported = exported; } - public VariantType Type { get; } + public VariantType? VariantType { get; } + public ITypeSymbol? PropertyType { get; } public string Name { get; } public PropertyHint Hint { get; } public string? HintString { get; } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs index 39d3a6f94eae..fdee5c73e790 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -291,7 +291,7 @@ private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo .Append(methodInfo.Name) .Append(", returnVal: "); - AppendPropertyInfo(source, methodInfo.ReturnVal); + source.AppendPropertyInfo(methodInfo.ReturnVal, "\"{0}\""); source.Append(", flags: (global::Godot.MethodFlags)") .Append((int)methodInfo.Flags) @@ -303,7 +303,7 @@ private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo foreach (var param in methodInfo.Arguments) { - AppendPropertyInfo(source, param); + source.AppendPropertyInfo(param, "\"{0}\""); // C# allows colon after the last element source.Append(", "); @@ -319,29 +319,6 @@ private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo source.Append(", defaultArguments: null));\n"); } - private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) - { - source.Append("new(type: (global::Godot.Variant.Type)") - .Append((int)propertyInfo.Type) - .Append(", name: \"") - .Append(propertyInfo.Name) - .Append("\", hint: (global::Godot.PropertyHint)") - .Append((int)propertyInfo.Hint) - .Append(", hintString: \"") - .Append(propertyInfo.HintString) - .Append("\", usage: (global::Godot.PropertyUsageFlags)") - .Append((int)propertyInfo.Usage) - .Append(", exported: ") - .Append(propertyInfo.Exported ? "true" : "false"); - if (propertyInfo.ClassName != null) - { - source.Append(", className: new global::Godot.StringName(\"") - .Append(propertyInfo.ClassName) - .Append("\")"); - } - source.Append(")"); - } - private static MethodInfo DetermineMethodInfo(GodotMethodData method) { PropertyInfo returnVal; @@ -354,7 +331,7 @@ private static MethodInfo DetermineMethodInfo(GodotMethodData method) } else { - returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None, + returnVal = new PropertyInfo(VariantType.Nil, null, string.Empty, PropertyHint.None, hintString: null, PropertyUsageFlags.Default, exported: false); } @@ -391,20 +368,23 @@ private static MethodInfo DetermineMethodInfo(GodotMethodData method) private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, ITypeSymbol typeSymbol, string name) { - var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType); var propUsage = PropertyUsageFlags.Default; - - if (memberVariantType == VariantType.Nil) - propUsage |= PropertyUsageFlags.NilIsVariant; - string? className = null; - if (memberVariantType == VariantType.Object && typeSymbol is INamedTypeSymbol namedTypeSymbol) + + if (memberVariantType.HasValue) { - className = namedTypeSymbol.GetGodotScriptNativeClassName(); + if (memberVariantType == VariantType.Nil) + propUsage |= PropertyUsageFlags.NilIsVariant; + + if (memberVariantType == VariantType.Object && typeSymbol is INamedTypeSymbol namedTypeSymbol) + { + className = namedTypeSymbol.GetGodotScriptNativeClassName(); + } } - return new PropertyInfo(memberVariantType, name, + return new PropertyInfo(memberVariantType, typeSymbol, name, PropertyHint.None, string.Empty, propUsage, className, exported: false); } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index 02c2cd4034bd..a3879e02707d 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -365,19 +365,9 @@ private static void AppendGroupingPropertyInfo(StringBuilder source, PropertyInf private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) { - source.Append(" properties.Add(new(type: (global::Godot.Variant.Type)") - .Append((int)propertyInfo.Type) - .Append(", name: PropertyName.") - .Append(propertyInfo.Name) - .Append(", hint: (global::Godot.PropertyHint)") - .Append((int)propertyInfo.Hint) - .Append(", hintString: \"") - .Append(propertyInfo.HintString) - .Append("\", usage: (global::Godot.PropertyUsageFlags)") - .Append((int)propertyInfo.Usage) - .Append(", exported: ") - .Append(propertyInfo.Exported ? "true" : "false") - .Append("));\n"); + source.Append(" properties.Add("); + source.AppendPropertyInfo(propertyInfo, "PropertyName.{0}"); + source.Append(");\n"); } private static IEnumerable DetermineGroupingPropertyInfo(ISymbol memberSymbol) @@ -401,7 +391,7 @@ private static IEnumerable DetermineGroupingPropertyInfo(ISymbol m if (propertyUsage != PropertyUsageFlags.Category && attr.ConstructorArguments.Length > 1) hintString = attr.ConstructorArguments[1].Value?.ToString(); - yield return new PropertyInfo(VariantType.Nil, name, PropertyHint.None, hintString, + yield return new PropertyInfo(VariantType.Nil, null, name, PropertyHint.None, hintString, propertyUsage.Value, true); } } @@ -447,17 +437,19 @@ MarshalType marshalType var memberType = propertySymbol?.Type ?? fieldSymbol!.Type; - var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType); string memberName = memberSymbol.Name; if (exportAttr == null) { - return new PropertyInfo(memberVariantType, memberName, PropertyHint.None, + return new PropertyInfo(memberVariantType, memberType, memberName, PropertyHint.None, hintString: null, PropertyUsageFlags.ScriptVariable, exported: false); } - if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType, - isTypeArgument: false, out var hint, out var hintString)) + TryGetNodeOrResourceType(exportAttr, out PropertyHint hint, out string? hintString); + + if (memberVariantType.HasValue && !TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType.Value, + isTypeArgument: false, out hint, out hintString)) { var constructorArguments = exportAttr.ConstructorArguments; @@ -487,10 +479,12 @@ MarshalType marshalType if (memberVariantType == VariantType.Nil) propUsage |= PropertyUsageFlags.NilIsVariant; - return new PropertyInfo(memberVariantType, memberName, + return new PropertyInfo(memberVariantType, memberType, memberName, hint, hintString, propUsage, exported: true); } + // If you update anything in here, check if the same thing also + // needs to be updated in Godot.Bridge.GenericUtils.GetPropertyHintString private static bool TryGetMemberExportHint( MarshalUtils.TypeCache typeCache, ITypeSymbol type, AttributeData exportAttr, @@ -597,37 +591,6 @@ private static bool TryGetMemberExportHint( } } - static bool TryGetNodeOrResourceType(AttributeData exportAttr, out PropertyHint hint, out string? hintString) - { - hint = PropertyHint.None; - hintString = null; - - if (exportAttr.ConstructorArguments.Length <= 1) return false; - - var hintValue = exportAttr.ConstructorArguments[0].Value; - - var hintEnum = hintValue switch - { - null => PropertyHint.None, - int intValue => (PropertyHint)intValue, - _ => (PropertyHint)(long)hintValue - }; - - if (!hintEnum.HasFlag(PropertyHint.NodeType) && !hintEnum.HasFlag(PropertyHint.ResourceType)) - return false; - - var hintStringValue = exportAttr.ConstructorArguments[1].Value?.ToString(); - if (string.IsNullOrWhiteSpace(hintStringValue)) - { - return false; - } - - hint = hintEnum; - hintString = hintStringValue; - - return true; - } - static string GetTypeName(INamedTypeSymbol memberSymbol) { if (memberSymbol.GetAttributes() @@ -734,5 +697,36 @@ static bool GetStringArrayEnumHint(VariantType elementVariantType, return false; } + + private static bool TryGetNodeOrResourceType(AttributeData exportAttr, out PropertyHint hint, out string? hintString) + { + hint = PropertyHint.None; + hintString = null; + + if (exportAttr.ConstructorArguments.Length <= 1) return false; + + var hintValue = exportAttr.ConstructorArguments[0].Value; + + var hintEnum = hintValue switch + { + null => PropertyHint.None, + int intValue => (PropertyHint)intValue, + _ => (PropertyHint)(long)hintValue + }; + + if (!hintEnum.HasFlag(PropertyHint.NodeType) && !hintEnum.HasFlag(PropertyHint.ResourceType)) + return false; + + var hintStringValue = exportAttr.ConstructorArguments[1].Value?.ToString(); + if (string.IsNullOrWhiteSpace(hintStringValue)) + { + return false; + } + + hint = hintEnum; + hintString = hintStringValue; + + return true; + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs index deac5f2bcf19..1b9fcf2d3312 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -274,7 +274,7 @@ void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) .Append(";\n"); source.Append( - $" /// \n"); + $" /// ', '}')}\"/>\n"); source.Append(" public event ") .Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal()) @@ -357,7 +357,7 @@ private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo .Append(methodInfo.Name) .Append(", returnVal: "); - AppendPropertyInfo(source, methodInfo.ReturnVal); + source.AppendPropertyInfo(methodInfo.ReturnVal, "\"{0}\""); source.Append(", flags: (global::Godot.MethodFlags)") .Append((int)methodInfo.Flags) @@ -369,7 +369,7 @@ private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo foreach (var param in methodInfo.Arguments) { - AppendPropertyInfo(source, param); + source.AppendPropertyInfo(param, "\"{0}\""); // C# allows colon after the last element source.Append(", "); @@ -385,29 +385,6 @@ private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo source.Append(", defaultArguments: null));\n"); } - private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) - { - source.Append("new(type: (global::Godot.Variant.Type)") - .Append((int)propertyInfo.Type) - .Append(", name: \"") - .Append(propertyInfo.Name) - .Append("\", hint: (global::Godot.PropertyHint)") - .Append((int)propertyInfo.Hint) - .Append(", hintString: \"") - .Append(propertyInfo.HintString) - .Append("\", usage: (global::Godot.PropertyUsageFlags)") - .Append((int)propertyInfo.Usage) - .Append(", exported: ") - .Append(propertyInfo.Exported ? "true" : "false"); - if (propertyInfo.ClassName != null) - { - source.Append(", className: new global::Godot.StringName(\"") - .Append(propertyInfo.ClassName) - .Append("\")"); - } - source.Append(")"); - } - private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDelegateData) { var invokeMethodData = signalDelegateData.InvokeMethodData; @@ -422,7 +399,7 @@ private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDele } else { - returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None, + returnVal = new PropertyInfo(VariantType.Nil, null, string.Empty, PropertyHint.None, hintString: null, PropertyUsageFlags.Default, exported: false); } @@ -452,7 +429,7 @@ private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDele private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, ITypeSymbol typeSymbol, string name) { - var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType); var propUsage = PropertyUsageFlags.Default; @@ -465,7 +442,7 @@ private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, IType className = namedTypeSymbol.GetGodotScriptNativeClassName(); } - return new PropertyInfo(memberVariantType, name, + return new PropertyInfo(memberVariantType, typeSymbol, name, PropertyHint.None, string.Empty, propUsage, className, exported: false); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GenericUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GenericUtils.cs new file mode 100644 index 000000000000..cdc49635072a --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GenericUtils.cs @@ -0,0 +1,168 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Text; +using Godot.NativeInterop; + +namespace Godot.Bridge; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +[EditorBrowsable(EditorBrowsableState.Never)] +public class GenericUtils +{ + public static PropertyInfo PropertyInfoFromGenericType<[MustBeVariant] T>(StringName name, PropertyHint hint, + string hintString, PropertyUsageFlags usage, bool exported) + { + Variant.Type variantType = VariantUtils.TypeOf(); + + // If there was an explicit hint on the property don't override it. + if (hint == PropertyHint.None && string.IsNullOrWhiteSpace(hintString)) + GetPropertyHintString(typeof(T), variantType, hint, hintString, out hint, out hintString); + + return new PropertyInfo(variantType, name, hint, hintString, usage, exported); + } + + /// + /// Determines what editor hint and hint string to use for a given managed type. + /// This function shares its logic with ScriptPropertiesGenerator.TryGetMemberExportHint so if you update + /// anything here, check if it needs updating over there too! + /// + private static bool GetPropertyHintString(Type type, Variant.Type variantType, PropertyHint exportHint, + string exportHintString, out PropertyHint hint, out string hintString) + { + hint = PropertyHint.None; + hintString = ""; + + if (variantType == Variant.Type.Nil) return true; + + if (variantType == Variant.Type.Int && typeof(Enum).IsAssignableFrom(type)) + { + hint = type.GetCustomAttribute() != null ? PropertyHint.Flags : PropertyHint.Enum; + + // Build a string of all the enum names and values, e.g: + // Foo:0,Bar:1,Baz:2 + StringBuilder sb = new StringBuilder(); + foreach (FieldInfo enumField in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + sb.Append(enumField.Name).Append(':').Append(enumField.GetRawConstantValue()).Append(','); + } + + // Remove trailing comma + sb.Length -= 1; + hintString = sb.ToString(); + return true; + } + + if (variantType == Variant.Type.Object) + { + if (typeof(Resource).IsAssignableFrom(type)) + { + hint = PropertyHint.ResourceType; + hintString = GetTypeName(type); + return true; + } + + if (typeof(Node).IsAssignableFrom(type)) + { + hint = PropertyHint.NodeType; + hintString = GetTypeName(type); + return true; + } + + return false; + } + + if (variantType == Variant.Type.Array) + { + // No hint needed for generic arrays + if (typeof(Godot.Collections.Array) == type) + { + return true; + } + + // Lets find out what the elements should be hinted as + Type elementType = type!.GetGenericArguments()[0]; + Variant.Type elementVariantType = GetVariantType(elementType); + bool hasElementHint = GetPropertyHintString(elementType, elementVariantType, exportHint, + exportHintString, out var elementHint, out var elementHintString); + + // Special case for string arrays + hint = PropertyHint.TypeString; + if (!GetStringArrayEnumHint(exportHint, exportHintString, elementVariantType, ref hintString)) + { + hintString = hasElementHint + ? $"{(int)elementVariantType}/{(int)elementHint}:{elementHintString}" + : $"{(int)elementVariantType}/{(int)PropertyHint.None}:"; + } + + return true; + } + + if (variantType == Variant.Type.PackedStringArray) + { + if (GetStringArrayEnumHint(exportHint, exportHintString, Variant.Type.String, ref hintString)) + { + hint = PropertyHint.TypeString; + return true; + } + + return false; + } + + if (variantType == Variant.Type.Dictionary) + { + // TODO: Dictionaries are not supported in the editor. + return false; + } + + return false; + } + + private static string GetTypeName(Type type) + { + // If this is a global class, we use the class name + if (type.GetCustomAttribute() != null) + { + return type.Name; + } + + // Otherwise we find the first Godot type and use its name. + Type nativeType = type; + while (nativeType != null) + { + if (nativeType.Namespace == "Godot") + { + var classNameAttribute = nativeType.GetCustomAttribute(false); + return classNameAttribute != null ? classNameAttribute.Name : nativeType.Name; + } + + nativeType = nativeType.BaseType; + } + + // This shouldn't happen + return ""; + } + + private static bool GetStringArrayEnumHint(PropertyHint hint, string hintString, Variant.Type elementVariantType, + ref string newHint) + { + if (hint == PropertyHint.Enum) + { + newHint = $"{(int)elementVariantType}/{(int)PropertyHint.Enum}:{hintString}"; + return true; + } + + return false; + } + + private static Variant.Type GetVariantType(Type type) + { + // There's no non-generic overload for VariantUtils.TypeOf + // But because this is only used for hints and the editor will never be AOT, this is fine. + return (Variant.Type)typeof(VariantUtils) + .GetMethod(nameof(VariantUtils.TypeOf), + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)! + .MakeGenericMethod(type).Invoke(null, null)!; + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs index 2897cc4199e6..788551c54421 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs @@ -413,4 +413,69 @@ public static T ConvertTo<[MustBeVariant] T>(in godot_variant variant) return GenericConversion.FromVariant(variant); } + + public static Variant.Type TypeOf<[MustBeVariant] T>() + { + if (typeof(T) == typeof(bool)) return Variant.Type.Bool; + if (typeof(T) == typeof(char)) return Variant.Type.Int; + if (typeof(T) == typeof(sbyte)) return Variant.Type.Int; + if (typeof(T) == typeof(short)) return Variant.Type.Int; + if (typeof(T) == typeof(int)) return Variant.Type.Int; + if (typeof(T) == typeof(long)) return Variant.Type.Int; + if (typeof(T) == typeof(byte)) return Variant.Type.Int; + if (typeof(T) == typeof(ushort)) return Variant.Type.Int; + if (typeof(T) == typeof(uint)) return Variant.Type.Int; + if (typeof(T) == typeof(ulong)) return Variant.Type.Int; + if (typeof(T) == typeof(float)) return Variant.Type.Float; + if (typeof(T) == typeof(double)) return Variant.Type.Float; + if (typeof(T) == typeof(Vector2)) return Variant.Type.Vector2; + if (typeof(T) == typeof(Vector2I)) return Variant.Type.Vector2I; + if (typeof(T) == typeof(Rect2)) return Variant.Type.Rect2; + if (typeof(T) == typeof(Rect2I)) return Variant.Type.Rect2I; + if (typeof(T) == typeof(Transform2D)) return Variant.Type.Transform2D; + if (typeof(T) == typeof(Projection)) return Variant.Type.Projection; + if (typeof(T) == typeof(Vector3)) return Variant.Type.Vector3; + if (typeof(T) == typeof(Vector3I)) return Variant.Type.Vector3I; + if (typeof(T) == typeof(Basis)) return Variant.Type.Basis; + if (typeof(T) == typeof(Quaternion)) return Variant.Type.Quaternion; + if (typeof(T) == typeof(Transform3D)) return Variant.Type.Transform3D; + if (typeof(T) == typeof(Vector4)) return Variant.Type.Vector4; + if (typeof(T) == typeof(Vector4I)) return Variant.Type.Vector4I; + if (typeof(T) == typeof(Aabb)) return Variant.Type.Aabb; + if (typeof(T) == typeof(Color)) return Variant.Type.Color; + if (typeof(T) == typeof(Plane)) return Variant.Type.Plane; + if (typeof(T) == typeof(Callable)) return Variant.Type.Callable; + if (typeof(T) == typeof(Signal)) return Variant.Type.Signal; + if (typeof(T) == typeof(string)) return Variant.Type.String; + if (typeof(T) == typeof(byte[])) return Variant.Type.PackedByteArray; + if (typeof(T) == typeof(int[])) return Variant.Type.PackedInt32Array; + if (typeof(T) == typeof(long[])) return Variant.Type.PackedInt64Array; + if (typeof(T) == typeof(float[])) return Variant.Type.PackedFloat32Array; + if (typeof(T) == typeof(double[])) return Variant.Type.PackedFloat64Array; + if (typeof(T) == typeof(string[])) return Variant.Type.PackedStringArray; + if (typeof(T) == typeof(Vector2[])) return Variant.Type.PackedVector2Array; + if (typeof(T) == typeof(Vector3[])) return Variant.Type.PackedVector3Array; + if (typeof(T) == typeof(Color[])) return Variant.Type.PackedColorArray; + if (typeof(T) == typeof(StringName[])) return Variant.Type.Array; + if (typeof(T) == typeof(NodePath[])) return Variant.Type.Array; + if (typeof(T) == typeof(Rid[])) return Variant.Type.Array; + if (typeof(T) == typeof(StringName)) return Variant.Type.StringName; + if (typeof(T) == typeof(NodePath)) return Variant.Type.NodePath; + if (typeof(T) == typeof(Rid)) return Variant.Type.Rid; + if (typeof(T) == typeof(Godot.Collections.Dictionary)) return Variant.Type.Dictionary; + if (typeof(T) == typeof(Godot.Collections.Array)) return Variant.Type.Array; + if (typeof(T) == typeof(Variant)) return Variant.Type.Nil; + if (typeof(GodotObject).IsAssignableFrom(typeof(T))) return Variant.Type.Object; + if (typeof(T).IsValueType && typeof(System.Enum).IsAssignableFrom(typeof(T))) return Variant.Type.Int; + + if (typeof(T).IsGenericType) + { + // I think this is OK to do w/ AOT? + Type genericTypeDef = typeof(T).GetGenericTypeDefinition(); + if (typeof(Collections.Array<>) == genericTypeDef) return Variant.Type.Array; + if (typeof(Collections.Dictionary<,>) == genericTypeDef) return Variant.Type.Dictionary; + } + + return Variant.Type.Nil; + } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index 6b25087c93ac..7365011975f7 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -48,6 +48,7 @@ +