17b2838f39
This change aims to reduce the number of places that need to be changed when adding or editing a Godot type to the bindings. Since the addition of `Variant.From<T>/As<T>` and `VariantUtils.CreateFrom<T>/ConvertTo<T>`, we can now replace a lot of the previous code in the bindings generator and the source generators that specify these conversions for each type manually. The only exceptions are the generic Godot collections (`Array<T>` and `Dictionary<TKey, TValue>`) which still use the old version, as that one cannot be matched by our new conversion methods (limitation in the language with generics, forcing us to use delegate pointers). The cleanup applies to: - Bindings generator: - `TypeInterface.cs_variant_to_managed` - `TypeInterface.cs_managed_to_variant` - Source generators: - `MarshalUtils.AppendNativeVariantToManagedExpr` - `MarshalUtils.AppendManagedToNativeVariantExpr` - `MarshalUtils.AppendVariantToManagedExpr` - `MarshalUtils.AppendManagedToVariantExpr`
663 lines
26 KiB
C#
663 lines
26 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
namespace Godot.SourceGenerators
|
|
{
|
|
[Generator]
|
|
public class ScriptPropertiesGenerator : ISourceGenerator
|
|
{
|
|
public void Initialize(GeneratorInitializationContext context)
|
|
{
|
|
}
|
|
|
|
public void Execute(GeneratorExecutionContext context)
|
|
{
|
|
if (context.AreGodotSourceGeneratorsDisabled())
|
|
return;
|
|
|
|
INamedTypeSymbol[] godotClasses = context
|
|
.Compilation.SyntaxTrees
|
|
.SelectMany(tree =>
|
|
tree.GetRoot().DescendantNodes()
|
|
.OfType<ClassDeclarationSyntax>()
|
|
.SelectGodotScriptClasses(context.Compilation)
|
|
// Report and skip non-partial classes
|
|
.Where(x =>
|
|
{
|
|
if (x.cds.IsPartial())
|
|
{
|
|
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
|
|
{
|
|
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
|
|
return false;
|
|
})
|
|
.Select(x => x.symbol)
|
|
)
|
|
.Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
|
|
.ToArray();
|
|
|
|
if (godotClasses.Length > 0)
|
|
{
|
|
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
|
|
|
|
foreach (var godotClass in godotClasses)
|
|
{
|
|
VisitGodotScriptClass(context, typeCache, godotClass);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void VisitGodotScriptClass(
|
|
GeneratorExecutionContext context,
|
|
MarshalUtils.TypeCache typeCache,
|
|
INamedTypeSymbol symbol
|
|
)
|
|
{
|
|
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
|
|
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
|
|
namespaceSymbol.FullQualifiedNameOmitGlobal() :
|
|
string.Empty;
|
|
bool hasNamespace = classNs.Length != 0;
|
|
|
|
bool isInnerClass = symbol.ContainingType != null;
|
|
|
|
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
|
|
+ "_ScriptProperties.generated";
|
|
|
|
var source = new StringBuilder();
|
|
|
|
source.Append("using Godot;\n");
|
|
source.Append("using Godot.NativeInterop;\n");
|
|
source.Append("\n");
|
|
|
|
if (hasNamespace)
|
|
{
|
|
source.Append("namespace ");
|
|
source.Append(classNs);
|
|
source.Append(" {\n\n");
|
|
}
|
|
|
|
if (isInnerClass)
|
|
{
|
|
var containingType = symbol.ContainingType;
|
|
|
|
while (containingType != null)
|
|
{
|
|
source.Append("partial ");
|
|
source.Append(containingType.GetDeclarationKeyword());
|
|
source.Append(" ");
|
|
source.Append(containingType.NameWithTypeParameters());
|
|
source.Append("\n{\n");
|
|
|
|
containingType = containingType.ContainingType;
|
|
}
|
|
}
|
|
|
|
source.Append("partial class ");
|
|
source.Append(symbol.NameWithTypeParameters());
|
|
source.Append("\n{\n");
|
|
|
|
var members = symbol.GetMembers();
|
|
|
|
var propertySymbols = members
|
|
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
|
|
.Cast<IPropertySymbol>()
|
|
.Where(s => !s.IsIndexer);
|
|
|
|
var fieldSymbols = members
|
|
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
|
|
.Cast<IFieldSymbol>();
|
|
|
|
var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
|
|
var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
|
|
|
|
source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
|
|
|
|
source.Append(
|
|
$" public new class PropertyName : {symbol.BaseType.FullQualifiedNameIncludeGlobal()}.PropertyName {{\n");
|
|
|
|
// Generate cached StringNames for methods and properties, for fast lookup
|
|
|
|
foreach (var property in godotClassProperties)
|
|
{
|
|
string propertyName = property.PropertySymbol.Name;
|
|
source.Append(" public new static readonly global::Godot.StringName ");
|
|
source.Append(propertyName);
|
|
source.Append(" = \"");
|
|
source.Append(propertyName);
|
|
source.Append("\";\n");
|
|
}
|
|
|
|
foreach (var field in godotClassFields)
|
|
{
|
|
string fieldName = field.FieldSymbol.Name;
|
|
source.Append(" public new static readonly global::Godot.StringName ");
|
|
source.Append(fieldName);
|
|
source.Append(" = \"");
|
|
source.Append(fieldName);
|
|
source.Append("\";\n");
|
|
}
|
|
|
|
source.Append(" }\n"); // class GodotInternal
|
|
|
|
if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
|
|
{
|
|
bool isFirstEntry;
|
|
|
|
// Generate SetGodotClassPropertyValue
|
|
|
|
bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) &&
|
|
godotClassProperties.All(pi => pi.PropertySymbol.IsReadOnly);
|
|
|
|
if (!allPropertiesAreReadOnly)
|
|
{
|
|
source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
|
|
source.Append("in godot_variant value)\n {\n");
|
|
|
|
isFirstEntry = true;
|
|
foreach (var property in godotClassProperties)
|
|
{
|
|
if (property.PropertySymbol.IsReadOnly)
|
|
continue;
|
|
|
|
GeneratePropertySetter(property.PropertySymbol.Name,
|
|
property.PropertySymbol.Type, property.Type, source, isFirstEntry);
|
|
isFirstEntry = false;
|
|
}
|
|
|
|
foreach (var field in godotClassFields)
|
|
{
|
|
if (field.FieldSymbol.IsReadOnly)
|
|
continue;
|
|
|
|
GeneratePropertySetter(field.FieldSymbol.Name,
|
|
field.FieldSymbol.Type, field.Type, source, isFirstEntry);
|
|
isFirstEntry = false;
|
|
}
|
|
|
|
source.Append(" return base.SetGodotClassPropertyValue(name, value);\n");
|
|
|
|
source.Append(" }\n");
|
|
}
|
|
|
|
// Generate GetGodotClassPropertyValue
|
|
|
|
source.Append(" protected override bool GetGodotClassPropertyValue(in godot_string_name name, ");
|
|
source.Append("out godot_variant value)\n {\n");
|
|
|
|
isFirstEntry = true;
|
|
foreach (var property in godotClassProperties)
|
|
{
|
|
GeneratePropertyGetter(property.PropertySymbol.Name,
|
|
property.PropertySymbol.Type, property.Type, source, isFirstEntry);
|
|
isFirstEntry = false;
|
|
}
|
|
|
|
foreach (var field in godotClassFields)
|
|
{
|
|
GeneratePropertyGetter(field.FieldSymbol.Name,
|
|
field.FieldSymbol.Type, field.Type, source, isFirstEntry);
|
|
isFirstEntry = false;
|
|
}
|
|
|
|
source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n");
|
|
|
|
source.Append(" }\n");
|
|
|
|
// Generate GetGodotPropertyList
|
|
|
|
string dictionaryType = "global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>";
|
|
|
|
source.Append(" internal new static ")
|
|
.Append(dictionaryType)
|
|
.Append(" GetGodotPropertyList()\n {\n");
|
|
|
|
source.Append(" var properties = new ")
|
|
.Append(dictionaryType)
|
|
.Append("();\n");
|
|
|
|
// To retain the definition order (and display categories correctly), we want to
|
|
// iterate over fields and properties at the same time, sorted by line number.
|
|
var godotClassPropertiesAndFields = Enumerable.Empty<GodotPropertyOrFieldData>()
|
|
.Concat(godotClassProperties.Select(propertyData => new GodotPropertyOrFieldData(propertyData)))
|
|
.Concat(godotClassFields.Select(fieldData => new GodotPropertyOrFieldData(fieldData)))
|
|
.OrderBy(data => data.Symbol.Locations[0].Path())
|
|
.ThenBy(data => data.Symbol.Locations[0].StartLine());
|
|
|
|
foreach (var member in godotClassPropertiesAndFields)
|
|
{
|
|
foreach (var groupingInfo in DetermineGroupingPropertyInfo(member.Symbol))
|
|
AppendGroupingPropertyInfo(source, groupingInfo);
|
|
|
|
var propertyInfo = DeterminePropertyInfo(context, typeCache,
|
|
member.Symbol, member.Type);
|
|
|
|
if (propertyInfo == null)
|
|
continue;
|
|
|
|
AppendPropertyInfo(source, propertyInfo.Value);
|
|
}
|
|
|
|
source.Append(" return properties;\n");
|
|
source.Append(" }\n");
|
|
|
|
source.Append("#pragma warning restore CS0109\n");
|
|
}
|
|
|
|
source.Append("}\n"); // partial class
|
|
|
|
if (isInnerClass)
|
|
{
|
|
var containingType = symbol.ContainingType;
|
|
|
|
while (containingType != null)
|
|
{
|
|
source.Append("}\n"); // outer class
|
|
|
|
containingType = containingType.ContainingType;
|
|
}
|
|
}
|
|
|
|
if (hasNamespace)
|
|
{
|
|
source.Append("\n}\n");
|
|
}
|
|
|
|
context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
|
|
}
|
|
|
|
private static void GeneratePropertySetter(
|
|
string propertyMemberName,
|
|
ITypeSymbol propertyTypeSymbol,
|
|
MarshalType propertyMarshalType,
|
|
StringBuilder source,
|
|
bool isFirstEntry
|
|
)
|
|
{
|
|
source.Append(" ");
|
|
|
|
if (!isFirstEntry)
|
|
source.Append("else ");
|
|
|
|
source.Append("if (name == PropertyName.")
|
|
.Append(propertyMemberName)
|
|
.Append(") {\n")
|
|
.Append(" this.")
|
|
.Append(propertyMemberName)
|
|
.Append(" = ")
|
|
.AppendNativeVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType)
|
|
.Append(";\n")
|
|
.Append(" return true;\n")
|
|
.Append(" }\n");
|
|
}
|
|
|
|
private static void GeneratePropertyGetter(
|
|
string propertyMemberName,
|
|
ITypeSymbol propertyTypeSymbol,
|
|
MarshalType propertyMarshalType,
|
|
StringBuilder source,
|
|
bool isFirstEntry
|
|
)
|
|
{
|
|
source.Append(" ");
|
|
|
|
if (!isFirstEntry)
|
|
source.Append("else ");
|
|
|
|
source.Append("if (name == PropertyName.")
|
|
.Append(propertyMemberName)
|
|
.Append(") {\n")
|
|
.Append(" value = ")
|
|
.AppendManagedToNativeVariantExpr("this." + propertyMemberName,
|
|
propertyTypeSymbol, propertyMarshalType)
|
|
.Append(";\n")
|
|
.Append(" return true;\n")
|
|
.Append(" }\n");
|
|
}
|
|
|
|
private static void AppendGroupingPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
|
|
{
|
|
source.Append(" properties.Add(new(type: (Godot.Variant.Type)")
|
|
.Append((int)VariantType.Nil)
|
|
.Append(", name: \"")
|
|
.Append(propertyInfo.Name)
|
|
.Append("\", hint: (Godot.PropertyHint)")
|
|
.Append((int)PropertyHint.None)
|
|
.Append(", hintString: \"")
|
|
.Append(propertyInfo.HintString)
|
|
.Append("\", usage: (Godot.PropertyUsageFlags)")
|
|
.Append((int)propertyInfo.Usage)
|
|
.Append(", exported: true));\n");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
private static IEnumerable<PropertyInfo> DetermineGroupingPropertyInfo(ISymbol memberSymbol)
|
|
{
|
|
foreach (var attr in memberSymbol.GetAttributes())
|
|
{
|
|
PropertyUsageFlags? propertyUsage = attr.AttributeClass?.ToString() switch
|
|
{
|
|
GodotClasses.ExportCategoryAttr => PropertyUsageFlags.Category,
|
|
GodotClasses.ExportGroupAttr => PropertyUsageFlags.Group,
|
|
GodotClasses.ExportSubgroupAttr => PropertyUsageFlags.Subgroup,
|
|
_ => null
|
|
};
|
|
|
|
if (propertyUsage is null)
|
|
continue;
|
|
|
|
if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string name)
|
|
{
|
|
string? hintString = null;
|
|
if (propertyUsage != PropertyUsageFlags.Category && attr.ConstructorArguments.Length > 1)
|
|
hintString = attr.ConstructorArguments[1].Value?.ToString();
|
|
|
|
yield return new PropertyInfo(VariantType.Nil, name, PropertyHint.None, hintString,
|
|
propertyUsage.Value, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static PropertyInfo? DeterminePropertyInfo(
|
|
GeneratorExecutionContext context,
|
|
MarshalUtils.TypeCache typeCache,
|
|
ISymbol memberSymbol,
|
|
MarshalType marshalType
|
|
)
|
|
{
|
|
var exportAttr = memberSymbol.GetAttributes()
|
|
.FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false);
|
|
|
|
var propertySymbol = memberSymbol as IPropertySymbol;
|
|
var fieldSymbol = memberSymbol as IFieldSymbol;
|
|
|
|
if (exportAttr != null && propertySymbol != null)
|
|
{
|
|
if (propertySymbol.GetMethod == null)
|
|
{
|
|
// This should never happen, as we filtered WriteOnly properties, but just in case.
|
|
Common.ReportExportedMemberIsWriteOnly(context, propertySymbol);
|
|
return null;
|
|
}
|
|
|
|
if (propertySymbol.SetMethod == null)
|
|
{
|
|
// This should never happen, as we filtered ReadOnly properties, but just in case.
|
|
Common.ReportExportedMemberIsReadOnly(context, propertySymbol);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
var memberType = propertySymbol?.Type ?? fieldSymbol!.Type;
|
|
|
|
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
|
|
string memberName = memberSymbol.Name;
|
|
|
|
if (exportAttr == null)
|
|
{
|
|
return new PropertyInfo(memberVariantType, memberName, PropertyHint.None,
|
|
hintString: null, PropertyUsageFlags.ScriptVariable, exported: false);
|
|
}
|
|
|
|
if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType,
|
|
isTypeArgument: false, out var hint, out var hintString))
|
|
{
|
|
var constructorArguments = exportAttr.ConstructorArguments;
|
|
|
|
if (constructorArguments.Length > 0)
|
|
{
|
|
var hintValue = exportAttr.ConstructorArguments[0].Value;
|
|
|
|
hint = hintValue switch
|
|
{
|
|
null => PropertyHint.None,
|
|
int intValue => (PropertyHint)intValue,
|
|
_ => (PropertyHint)(long)hintValue
|
|
};
|
|
|
|
hintString = constructorArguments.Length > 1 ?
|
|
exportAttr.ConstructorArguments[1].Value?.ToString() :
|
|
null;
|
|
}
|
|
else
|
|
{
|
|
hint = PropertyHint.None;
|
|
}
|
|
}
|
|
|
|
var propUsage = PropertyUsageFlags.Default | PropertyUsageFlags.ScriptVariable;
|
|
|
|
if (memberVariantType == VariantType.Nil)
|
|
propUsage |= PropertyUsageFlags.NilIsVariant;
|
|
|
|
return new PropertyInfo(memberVariantType, memberName,
|
|
hint, hintString, propUsage, exported: true);
|
|
}
|
|
|
|
private static bool TryGetMemberExportHint(
|
|
MarshalUtils.TypeCache typeCache,
|
|
ITypeSymbol type, AttributeData exportAttr,
|
|
VariantType variantType, bool isTypeArgument,
|
|
out PropertyHint hint, out string? hintString
|
|
)
|
|
{
|
|
hint = PropertyHint.None;
|
|
hintString = null;
|
|
|
|
if (variantType == VariantType.Nil)
|
|
return true; // Variant, no export hint
|
|
|
|
if (variantType == VariantType.Int &&
|
|
type.IsValueType && type.TypeKind == TypeKind.Enum)
|
|
{
|
|
bool hasFlagsAttr = type.GetAttributes()
|
|
.Any(a => a.AttributeClass?.IsSystemFlagsAttribute() ?? false);
|
|
|
|
hint = hasFlagsAttr ? PropertyHint.Flags : PropertyHint.Enum;
|
|
|
|
var members = type.GetMembers();
|
|
|
|
var enumFields = members
|
|
.Where(s => s.Kind == SymbolKind.Field && s.IsStatic &&
|
|
s.DeclaredAccessibility == Accessibility.Public &&
|
|
!s.IsImplicitlyDeclared)
|
|
.Cast<IFieldSymbol>().ToArray();
|
|
|
|
var hintStringBuilder = new StringBuilder();
|
|
var nameOnlyHintStringBuilder = new StringBuilder();
|
|
|
|
// True: enum Foo { Bar, Baz, Qux }
|
|
// True: enum Foo { Bar = 0, Baz = 1, Qux = 2 }
|
|
// False: enum Foo { Bar = 0, Baz = 7, Qux = 5 }
|
|
bool usesDefaultValues = true;
|
|
|
|
for (int i = 0; i < enumFields.Length; i++)
|
|
{
|
|
var enumField = enumFields[i];
|
|
|
|
if (i > 0)
|
|
{
|
|
hintStringBuilder.Append(",");
|
|
nameOnlyHintStringBuilder.Append(",");
|
|
}
|
|
|
|
string enumFieldName = enumField.Name;
|
|
hintStringBuilder.Append(enumFieldName);
|
|
nameOnlyHintStringBuilder.Append(enumFieldName);
|
|
|
|
long val = enumField.ConstantValue switch
|
|
{
|
|
sbyte v => v,
|
|
short v => v,
|
|
int v => v,
|
|
long v => v,
|
|
byte v => v,
|
|
ushort v => v,
|
|
uint v => v,
|
|
ulong v => (long)v,
|
|
_ => 0
|
|
};
|
|
|
|
uint expectedVal = (uint)(hint == PropertyHint.Flags ? 1 << i : i);
|
|
if (val != expectedVal)
|
|
usesDefaultValues = false;
|
|
|
|
hintStringBuilder.Append(":");
|
|
hintStringBuilder.Append(val);
|
|
}
|
|
|
|
hintString = !usesDefaultValues ?
|
|
hintStringBuilder.ToString() :
|
|
// If we use the format NAME:VAL, that's what the editor displays.
|
|
// That's annoying if the user is not using custom values for the enum constants.
|
|
// This may not be needed in the future if the editor is changed to not display values.
|
|
nameOnlyHintStringBuilder.ToString();
|
|
|
|
return true;
|
|
}
|
|
|
|
if (variantType == VariantType.Object && type is INamedTypeSymbol memberNamedType)
|
|
{
|
|
if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Resource"))
|
|
{
|
|
string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!;
|
|
|
|
hint = PropertyHint.ResourceType;
|
|
hintString = nativeTypeName;
|
|
|
|
return true;
|
|
}
|
|
|
|
if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Node"))
|
|
{
|
|
string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!;
|
|
|
|
hint = PropertyHint.NodeType;
|
|
hintString = nativeTypeName;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static bool GetStringArrayEnumHint(VariantType elementVariantType,
|
|
AttributeData exportAttr, out string? hintString)
|
|
{
|
|
var constructorArguments = exportAttr.ConstructorArguments;
|
|
|
|
if (constructorArguments.Length > 0)
|
|
{
|
|
var presetHintValue = exportAttr.ConstructorArguments[0].Value;
|
|
|
|
PropertyHint presetHint = presetHintValue switch
|
|
{
|
|
null => PropertyHint.None,
|
|
int intValue => (PropertyHint)intValue,
|
|
_ => (PropertyHint)(long)presetHintValue
|
|
};
|
|
|
|
if (presetHint == PropertyHint.Enum)
|
|
{
|
|
string? presetHintString = constructorArguments.Length > 1 ?
|
|
exportAttr.ConstructorArguments[1].Value?.ToString() :
|
|
null;
|
|
|
|
hintString = (int)elementVariantType + "/" + (int)PropertyHint.Enum + ":";
|
|
|
|
if (presetHintString != null)
|
|
hintString += presetHintString;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
hintString = null;
|
|
return false;
|
|
}
|
|
|
|
if (!isTypeArgument && variantType == VariantType.Array)
|
|
{
|
|
var elementType = MarshalUtils.GetArrayElementType(type);
|
|
|
|
if (elementType == null)
|
|
return false; // Non-generic Array, so there's no hint to add
|
|
|
|
var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache)!.Value;
|
|
var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType)!.Value;
|
|
|
|
bool isPresetHint = false;
|
|
|
|
if (elementVariantType == VariantType.String)
|
|
isPresetHint = GetStringArrayEnumHint(elementVariantType, exportAttr, out hintString);
|
|
|
|
if (!isPresetHint)
|
|
{
|
|
bool hintRes = TryGetMemberExportHint(typeCache, elementType,
|
|
exportAttr, elementVariantType, isTypeArgument: true,
|
|
out var elementHint, out var elementHintString);
|
|
|
|
// Format: type/hint:hint_string
|
|
if (hintRes)
|
|
{
|
|
hintString = (int)elementVariantType + "/" + (int)elementHint + ":";
|
|
|
|
if (elementHintString != null)
|
|
hintString += elementHintString;
|
|
}
|
|
else
|
|
{
|
|
hintString = (int)elementVariantType + "/" + (int)PropertyHint.None + ":";
|
|
}
|
|
}
|
|
|
|
hint = PropertyHint.TypeString;
|
|
|
|
return hintString != null;
|
|
}
|
|
|
|
if (!isTypeArgument && variantType == VariantType.PackedStringArray)
|
|
{
|
|
if (GetStringArrayEnumHint(VariantType.String, exportAttr, out hintString))
|
|
{
|
|
hint = PropertyHint.TypeString;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!isTypeArgument && variantType == VariantType.Dictionary)
|
|
{
|
|
// TODO: Dictionaries are not supported in the inspector
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|