diff options
Diffstat (limited to 'modules/mono/editor')
85 files changed, 6596 insertions, 2744 deletions
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln index d1868f52ef..03a7dc453c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}" EndProject diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index 4e9e7184da..013b210ff4 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -26,16 +26,8 @@ <None Include="Sdk\Sdk.props" Pack="true" PackagePath="Sdk" /> <None Include="Sdk\Sdk.targets" Pack="true" PackagePath="Sdk" /> <!-- SdkPackageVersions.props --> - <None Include="..\..\..\SdkPackageVersions.props" Pack="true" PackagePath="Sdk"> + <None Include="$(GodotSdkPackageVersionsFilePath)" Pack="true" PackagePath="Sdk"> <Link>Sdk\SdkPackageVersions.props</Link> </None> </ItemGroup> - - <Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack"> - <PropertyGroup> - <GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath> - <GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir> - </PropertyGroup> - <Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" /> - </Target> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props index 5a499742e9..59ce1da17b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -77,12 +77,11 @@ <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'windows' ">GODOT_WINDOWS;GODOT_PC</GodotPlatformConstants> <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'linuxbsd' ">GODOT_LINUXBSD;GODOT_PC</GodotPlatformConstants> <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'macos' ">GODOT_OSX;GODOT_MACOS;GODOT_PC</GodotPlatformConstants> - <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'server' ">GODOT_SERVER;GODOT_PC</GodotPlatformConstants> <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'uwp' ">GODOT_UWP;GODOT_PC</GodotPlatformConstants> <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'haiku' ">GODOT_HAIKU;GODOT_PC</GodotPlatformConstants> <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'android' ">GODOT_ANDROID;GODOT_MOBILE</GodotPlatformConstants> <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'ios' ">GODOT_IPHONE;GODOT_IOS;GODOT_MOBILE</GodotPlatformConstants> - <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'javascript' ">GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'web' ">GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB</GodotPlatformConstants> <GodotDefineConstants>$(GodotDefineConstants);$(GodotPlatformConstants)</GodotDefineConstants> </PropertyGroup> @@ -95,21 +94,4 @@ <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants> </PropertyGroup> - - <!-- Godot API references --> - <ItemGroup> - <!-- - TODO: - We should consider a nuget package for reference assemblies. This is difficult because the - Godot scripting API is continuaslly breaking backwards compatibility even in patch releases. - --> - <Reference Include="GodotSharp"> - <Private>false</Private> - <HintPath>$(GodotProjectDir).godot\mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath> - </Reference> - <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' "> - <Private>false</Private> - <HintPath>$(GodotProjectDir).godot\mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath> - </Reference> - </ItemGroup> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets index 397ede9644..aad4ea4553 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets @@ -19,4 +19,10 @@ <ItemGroup Condition=" '$(DisableImplicitGodotGeneratorReferences)' != 'true' "> <PackageReference Include="Godot.SourceGenerators" Version="$(PackageFloatingVersion_Godot)" /> </ItemGroup> + + <!-- Godot API references --> + <ItemGroup Condition=" '$(DisableImplicitGodotSharpReferences)' != 'true' "> + <PackageReference Include="GodotSharp" Version="$(PackageVersion_GodotSharp)" /> + <PackageReference Include="GodotSharpEditor" Version="$(PackageVersion_GodotSharp)" Condition=" '$(Configuration)' == 'Debug' " /> + </ItemGroup> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/EventSignals.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/EventSignals.cs new file mode 100644 index 0000000000..764ba8f121 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/EventSignals.cs @@ -0,0 +1,7 @@ +namespace Godot.SourceGenerators.Sample; + +public partial class EventSignals : Godot.Object +{ + [Signal] + public delegate void MySignalEventHandler(string str, int num); +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs new file mode 100644 index 0000000000..ac8d6473a6 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS0169 +#pragma warning disable CS0414 + +namespace Godot.SourceGenerators.Sample +{ + [SuppressMessage("ReSharper", "BuiltInTypeReferenceStyle")] + [SuppressMessage("ReSharper", "RedundantNameQualifier")] + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public partial class ExportedFields : Godot.Object + { + [Export] private Boolean field_Boolean = true; + [Export] private Char field_Char = 'f'; + [Export] private SByte field_SByte = 10; + [Export] private Int16 field_Int16 = 10; + [Export] private Int32 field_Int32 = 10; + [Export] private Int64 field_Int64 = 10; + [Export] private Byte field_Byte = 10; + [Export] private UInt16 field_UInt16 = 10; + [Export] private UInt32 field_UInt32 = 10; + [Export] private UInt64 field_UInt64 = 10; + [Export] private Single field_Single = 10; + [Export] private Double field_Double = 10; + [Export] private String field_String = "foo"; + + // Godot structs + [Export] private Vector2 field_Vector2 = new(10f, 10f); + [Export] private Vector2i field_Vector2i = Vector2i.Up; + [Export] private Rect2 field_Rect2 = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2i field_Rect2i = new(new Vector2i(10, 10), new Vector2i(10, 10)); + [Export] private Transform2D field_Transform2D = Transform2D.Identity; + [Export] private Vector3 field_Vector3 = new(10f, 10f, 10f); + [Export] private Vector3i field_Vector3i = Vector3i.Back; + [Export] private Basis field_Basis = new Basis(Quaternion.Identity); + [Export] private Quaternion field_Quaternion = new Quaternion(Basis.Identity); + [Export] private Transform3D field_Transform3D = Transform3D.Identity; + [Export] private Vector4 field_Vector4 = new(10f, 10f, 10f, 10f); + [Export] private Vector4i field_Vector4i = Vector4i.One; + [Export] private Projection field_Projection = Projection.Identity; + [Export] private AABB field_AABB = new AABB(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color field_Color = Colors.Aquamarine; + [Export] private Plane field_Plane = Plane.PlaneXZ; + [Export] private Callable field_Callable = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private SignalInfo field_SignalInfo = new SignalInfo(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum field_Enum = MyEnum.C; + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum field_FlagsEnum = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] field_ByteArray = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] field_Int32Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] field_Int64Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] field_SingleArray = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] field_DoubleArray = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] field_StringArray = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] field_StringArrayEnum = { "foo", "bar" }; + [Export] private Vector2[] field_Vector2Array = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] field_Vector3Array = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] field_ColorArray = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private Godot.Object[] field_GodotObjectOrDerivedArray = { null }; + [Export] private StringName[] field_StringNameArray = { "foo", "bar" }; + [Export] private NodePath[] field_NodePathArray = { "foo", "bar" }; + [Export] private RID[] field_RIDArray = { default, default, default }; + + // Variant + [Export] private Variant field_Variant = "foo"; + + // Classes + [Export] private Godot.Object field_GodotObjectOrDerived; + [Export] private Godot.Texture field_GodotResourceTexture; + [Export] private StringName field_StringName = new StringName("foo"); + [Export] private NodePath field_NodePath = new NodePath("foo"); + [Export] private RID field_RID; + + [Export] + private Godot.Collections.Dictionary field_GodotDictionary = + new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] + private Godot.Collections.Array field_GodotArray = + new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] + private Godot.Collections.Dictionary<string, bool> field_GodotGenericDictionary = + new() { { "foo", true }, { "bar", false } }; + + [Export] + private Godot.Collections.Array<int> field_GodotGenericArray = + new() { 0, 1, 2, 3, 4, 5, 6 }; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs new file mode 100644 index 0000000000..3020cfbc50 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS0169 +#pragma warning disable CS0414 + +namespace Godot.SourceGenerators.Sample +{ + [SuppressMessage("ReSharper", "BuiltInTypeReferenceStyle")] + [SuppressMessage("ReSharper", "RedundantNameQualifier")] + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public partial class ExportedProperties : Godot.Object + { + [Export] private Boolean property_Boolean { get; set; } = true; + [Export] private Char property_Char { get; set; } = 'f'; + [Export] private SByte property_SByte { get; set; } = 10; + [Export] private Int16 property_Int16 { get; set; } = 10; + [Export] private Int32 property_Int32 { get; set; } = 10; + [Export] private Int64 property_Int64 { get; set; } = 10; + [Export] private Byte property_Byte { get; set; } = 10; + [Export] private UInt16 property_UInt16 { get; set; } = 10; + [Export] private UInt32 property_UInt32 { get; set; } = 10; + [Export] private UInt64 property_UInt64 { get; set; } = 10; + [Export] private Single property_Single { get; set; } = 10; + [Export] private Double property_Double { get; set; } = 10; + [Export] private String property_String { get; set; } = "foo"; + + // Godot structs + [Export] private Vector2 property_Vector2 { get; set; } = new(10f, 10f); + [Export] private Vector2i property_Vector2i { get; set; } = Vector2i.Up; + [Export] private Rect2 property_Rect2 { get; set; } = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2i property_Rect2i { get; set; } = new(new Vector2i(10, 10), new Vector2i(10, 10)); + [Export] private Transform2D property_Transform2D { get; set; } = Transform2D.Identity; + [Export] private Vector3 property_Vector3 { get; set; } = new(10f, 10f, 10f); + [Export] private Vector3i property_Vector3i { get; set; } = Vector3i.Back; + [Export] private Basis property_Basis { get; set; } = new Basis(Quaternion.Identity); + [Export] private Quaternion property_Quaternion { get; set; } = new Quaternion(Basis.Identity); + [Export] private Transform3D property_Transform3D { get; set; } = Transform3D.Identity; + [Export] private Vector4 property_Vector4 { get; set; } = new(10f, 10f, 10f, 10f); + [Export] private Vector4i property_Vector4i { get; set; } = Vector4i.One; + [Export] private Projection property_Projection { get; set; } = Projection.Identity; + [Export] private AABB property_AABB { get; set; } = new AABB(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color property_Color { get; set; } = Colors.Aquamarine; + [Export] private Plane property_Plane { get; set; } = Plane.PlaneXZ; + [Export] private Callable property_Callable { get; set; } = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private SignalInfo property_SignalInfo { get; set; } = new SignalInfo(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum property_Enum { get; set; } = MyEnum.C; + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum property_FlagsEnum { get; set; } = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] property_ByteArray { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] property_Int32Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] property_Int64Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] property_SingleArray { get; set; } = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] property_DoubleArray { get; set; } = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] property_StringArray { get; set; } = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] property_StringArrayEnum { get; set; } = { "foo", "bar" }; + [Export] private Vector2[] property_Vector2Array { get; set; } = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] property_Vector3Array { get; set; } = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] property_ColorArray { get; set; } = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private Godot.Object[] property_GodotObjectOrDerivedArray { get; set; } = { null }; + [Export] private StringName[] field_StringNameArray { get; set; } = { "foo", "bar" }; + [Export] private NodePath[] field_NodePathArray { get; set; } = { "foo", "bar" }; + [Export] private RID[] field_RIDArray { get; set; } = { default, default, default }; + + // Variant + [Export] private Variant property_Variant { get; set; } = "foo"; + + // Classes + [Export] private Godot.Object property_GodotObjectOrDerived { get; set; } + [Export] private Godot.Texture property_GodotResourceTexture { get; set; } + [Export] private StringName property_StringName { get; set; } = new StringName("foo"); + [Export] private NodePath property_NodePath { get; set; } = new NodePath("foo"); + [Export] private RID property_RID { get; set; } + + [Export] + private Godot.Collections.Dictionary property_GodotDictionary { get; set; } = + new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] + private Godot.Collections.Array property_GodotArray { get; set; } = + new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] + private Godot.Collections.Dictionary<string, bool> property_GodotGenericDictionary { get; set; } = + new() { { "foo", true }, { "bar", false } }; + + [Export] + private Godot.Collections.Array<int> property_GodotGenericArray { get; set; } = + new() { 0, 1, 2, 3, 4, 5, 6 }; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs index 2ddb8880c2..b21b035b4d 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs @@ -1,16 +1,21 @@ +#pragma warning disable CS0169 + namespace Godot.SourceGenerators.Sample { partial class Generic<T> : Godot.Object { + private int _field; } // Generic again but different generic parameters partial class Generic<T, R> : Godot.Object { + private int _field; } // Generic again but without generic parameters partial class Generic : Godot.Object { + private int _field; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj index 24f7909861..8e78e0385d 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj @@ -1,12 +1,14 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net6.0</TargetFramework> </PropertyGroup> <PropertyGroup> <!-- $(GodotProjectDir) would normally be defined by the Godot.NET.Sdk --> <GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir> + <!-- For compiling GetGodotPropertyDefaultValues. --> + <DefineConstants>$(DefineConstants);TOOLS</DefineConstants> </PropertyGroup> <PropertyGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs new file mode 100644 index 0000000000..618ba24abc --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Godot.SourceGenerators.Sample; + +[SuppressMessage("ReSharper", "RedundantNameQualifier")] +public partial class Methods : Godot.Object +{ + private void MethodWithOverload() + { + } + + private void MethodWithOverload(int a) + { + } + + private void MethodWithOverload(int a, int b) + { + } + + // Should be ignored. The previous one is picked. + // ReSharper disable once UnusedMember.Local + private void MethodWithOverload(float a, float b) + { + } + + // Generic methods should be ignored. + // ReSharper disable once UnusedMember.Local + private void GenericMethod<T>(T t) + { + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs new file mode 100644 index 0000000000..a1667dbb8f --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs @@ -0,0 +1,36 @@ +#pragma warning disable CS0169 + +namespace Godot.SourceGenerators.Sample +{ + public partial class ScriptBoilerplate : Node + { + private NodePath _nodePath; + private int _velocity; + + public override void _Process(float delta) + { + _ = delta; + + base._Process(delta); + } + + public int Bazz(StringName name) + { + _ = name; + return 1; + } + + public void IgnoreThisMethodWithByRefParams(ref int a) + { + _ = a; + } + } + + partial struct OuterClass + { + public partial class NesterClass : RefCounted + { + public override Variant _Get(StringName property) => default; + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs index 4867c986e6..e28788ec0b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs @@ -1,5 +1,7 @@ +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; namespace Godot.SourceGenerators { @@ -14,12 +16,11 @@ namespace Godot.SourceGenerators "Missing partial modifier on declaration of type '" + $"{symbol.FullQualifiedName()}' which is a subclass of '{GodotClasses.Object}'"; - string description = $"{message}. Subclasses of '{GodotClasses.Object}' must be " + - "declared with the partial modifier or annotated with the " + - $"attribute '{GodotClasses.DisableGodotGeneratorsAttr}'."; + string description = $"{message}. Subclasses of '{GodotClasses.Object}' " + + "must be declared with the partial modifier."; context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GODOT-G0001", + new DiagnosticDescriptor(id: "GD0001", title: message, messageFormat: message, category: "Usage", @@ -29,5 +30,333 @@ namespace Godot.SourceGenerators cds.GetLocation(), cds.SyntaxTree.FilePath)); } + + public static void ReportNonPartialGodotScriptOuterClass( + GeneratorExecutionContext context, + TypeDeclarationSyntax outerTypeDeclSyntax + ) + { + var outerSymbol = context.Compilation + .GetSemanticModel(outerTypeDeclSyntax.SyntaxTree) + .GetDeclaredSymbol(outerTypeDeclSyntax); + + string fullQualifiedName = outerSymbol is INamedTypeSymbol namedTypeSymbol ? + namedTypeSymbol.FullQualifiedName() : + "type not found"; + + string message = + $"Missing partial modifier on declaration of type '{fullQualifiedName}', " + + $"which contains one or more subclasses of '{GodotClasses.Object}'"; + + string description = $"{message}. Subclasses of '{GodotClasses.Object}' and their " + + "containing types must be declared with the partial modifier."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0002", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + outerTypeDeclSyntax.GetLocation(), + outerTypeDeclSyntax.SyntaxTree.FilePath)); + } + + public static void ReportExportedMemberIsStatic( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"Attempted to export static {(isField ? "field" : "property")}: " + + $"'{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Only instance fields and properties can be exported." + + " Remove the 'static' modifier or the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0101", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberTypeNotSupported( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"The type of the exported {(isField ? "field" : "property")} " + + $"is not supported: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Use a supported type or remove the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0102", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsReadOnly( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"The exported {(isField ? "field" : "property")} " + + $"is read-only: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = isField ? + $"{message}. Exported fields cannot be read-only." : + $"{message}. Exported properties must be writable."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0103", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsWriteOnly( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = $"The exported property is write-only: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Exported properties must be readable."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0104", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsIndexer( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = $"Attempted to export indexer property: " + + $"'{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Indexer properties can't be exported." + + " Remove the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0105", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportSignalDelegateMissingSuffix( + GeneratorExecutionContext context, + INamedTypeSymbol delegateSymbol) + { + var locations = delegateSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = "The name of the delegate must end with 'EventHandler': " + + delegateSymbol.ToDisplayString() + + $". Did you mean '{delegateSymbol.Name}EventHandler'?"; + + string description = $"{message}. Rename the delegate accordingly or remove the '[Signal]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0201", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportSignalParameterTypeNotSupported( + GeneratorExecutionContext context, + IParameterSymbol parameterSymbol) + { + var locations = parameterSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = "The parameter of the delegate signature of the signal " + + $"is not supported: '{parameterSymbol.ToDisplayString()}'"; + + string description = $"{message}. Use supported types only or remove the '[Signal]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0202", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportSignalDelegateSignatureMustReturnVoid( + GeneratorExecutionContext context, + INamedTypeSymbol delegateSymbol) + { + var locations = delegateSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = "The delegate signature of the signal " + + $"must return void: '{delegateSymbol.ToDisplayString()}'"; + + string description = $"{message}. Return void or remove the '[Signal]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0203", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static readonly DiagnosticDescriptor GenericTypeArgumentMustBeVariantRule = + new DiagnosticDescriptor(id: "GD0301", + title: "The generic type argument must be a Variant compatible type", + messageFormat: "The generic type argument must be a Variant compatible type: {0}", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The generic type argument must be a Variant compatible type. Use a Variant compatible type as the generic type argument."); + + public static void ReportGenericTypeArgumentMustBeVariant( + SyntaxNodeAnalysisContext context, + SyntaxNode typeArgumentSyntax, + ISymbol typeArgumentSymbol) + { + string message = "The generic type argument " + + $"must be a Variant compatible type: '{typeArgumentSymbol.ToDisplayString()}'"; + + string description = $"{message}. Use a Variant compatible type as the generic type argument."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0301", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + typeArgumentSyntax.GetLocation(), + typeArgumentSyntax.SyntaxTree.FilePath)); + } + + public static readonly DiagnosticDescriptor GenericTypeParameterMustBeVariantAnnotatedRule = + new DiagnosticDescriptor(id: "GD0302", + title: "The generic type parameter must be annotated with the MustBeVariant attribute", + messageFormat: "The generic type argument must be a Variant type: {0}", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The generic type argument must be a Variant type. Use a Variant type as the generic type argument."); + + public static void ReportGenericTypeParameterMustBeVariantAnnotated( + SyntaxNodeAnalysisContext context, + SyntaxNode typeArgumentSyntax, + ISymbol typeArgumentSymbol) + { + string message = "The generic type parameter must be annotated with the MustBeVariant attribute"; + + string description = $"{message}. Add the MustBeVariant attribute to the generic type parameter."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0302", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + typeArgumentSyntax.GetLocation(), + typeArgumentSyntax.SyntaxTree.FilePath)); + } + + public static readonly DiagnosticDescriptor TypeArgumentParentSymbolUnhandledRule = + new DiagnosticDescriptor(id: "GD0303", + title: "The generic type parameter must be annotated with the MustBeVariant attribute", + messageFormat: "The generic type argument must be a Variant type: {0}", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The generic type argument must be a Variant type. Use a Variant type as the generic type argument."); + + public static void ReportTypeArgumentParentSymbolUnhandled( + SyntaxNodeAnalysisContext context, + SyntaxNode typeArgumentSyntax, + ISymbol parentSymbol) + { + string message = $"Symbol '{parentSymbol.ToDisplayString()}' parent of a type argument " + + "that must be Variant compatible was not handled."; + + string description = $"{message}. Handle type arguments that are children of the unhandled symbol type."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GD0303", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + typeArgumentSyntax.GetLocation(), + typeArgumentSyntax.SyntaxTree.FilePath)); + } } } 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 e16f72f43a..de3b6c862a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -13,30 +15,65 @@ namespace Godot.SourceGenerators ) => context.AnalyzerConfigOptions.GlobalOptions .TryGetValue("build_property." + property, out value); - private static bool InheritsFrom(this INamedTypeSymbol? symbol, string baseName) - { - if (symbol == null) - return false; + public static bool AreGodotSourceGeneratorsDisabled(this GeneratorExecutionContext context) + => context.TryGetGlobalAnalyzerProperty("GodotSourceGenerators", out string? toggle) && + toggle != null && + toggle.Equals("disabled", StringComparison.OrdinalIgnoreCase); + + public static bool IsGodotToolsProject(this GeneratorExecutionContext context) + => context.TryGetGlobalAnalyzerProperty("IsGodotToolsProject", out string? toggle) && + toggle != null && + toggle.Equals("true", StringComparison.OrdinalIgnoreCase); - while (true) + public static bool InheritsFrom(this INamedTypeSymbol? symbol, string assemblyName, string typeFullName) + { + while (symbol != null) { - if (symbol.ToString() == baseName) + if (symbol.ContainingAssembly.Name == assemblyName && + symbol.ToString() == typeFullName) { return true; } - if (symbol.BaseType != null) - { - symbol = symbol.BaseType; - continue; - } - - break; + symbol = symbol.BaseType; } return false; } + public static INamedTypeSymbol? GetGodotScriptNativeClass(this INamedTypeSymbol classTypeSymbol) + { + var symbol = classTypeSymbol; + + while (symbol != null) + { + if (symbol.ContainingAssembly.Name == "GodotSharp") + return symbol; + + symbol = symbol.BaseType; + } + + return null; + } + + public static string? GetGodotScriptNativeClassName(this INamedTypeSymbol classTypeSymbol) + { + var nativeType = classTypeSymbol.GetGodotScriptNativeClass(); + + if (nativeType == null) + return null; + + var godotClassNameAttr = nativeType.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.IsGodotClassNameAttribute() ?? false); + + string? godotClassName = null; + + if (godotClassNameAttr is { ConstructorArguments: { Length: > 0 } }) + godotClassName = godotClassNameAttr.ConstructorArguments[0].Value?.ToString(); + + return godotClassName ?? nativeType.Name; + } + private static bool IsGodotScriptClass( this ClassDeclarationSyntax cds, Compilation compilation, out INamedTypeSymbol? symbol @@ -47,7 +84,7 @@ namespace Godot.SourceGenerators var classTypeSymbol = sm.GetDeclaredSymbol(cds); if (classTypeSymbol?.BaseType == null - || !classTypeSymbol.BaseType.InheritsFrom(GodotClasses.Object)) + || !classTypeSymbol.BaseType.InheritsFrom("GodotSharp", GodotClasses.Object)) { symbol = null; return false; @@ -69,21 +106,182 @@ namespace Godot.SourceGenerators } } - public static bool IsPartial(this ClassDeclarationSyntax cds) + public static bool IsNested(this TypeDeclarationSyntax cds) + => cds.Parent is TypeDeclarationSyntax; + + public static bool IsPartial(this TypeDeclarationSyntax cds) => cds.Modifiers.Any(SyntaxKind.PartialKeyword); - public static bool HasDisableGeneratorsAttribute(this INamedTypeSymbol symbol) - => symbol.GetAttributes().Any(attr => - attr.AttributeClass?.ToString() == GodotClasses.DisableGodotGeneratorsAttr); + public static bool AreAllOuterTypesPartial( + this TypeDeclarationSyntax cds, + out TypeDeclarationSyntax? typeMissingPartial + ) + { + SyntaxNode? outerSyntaxNode = cds.Parent; + + while (outerSyntaxNode is TypeDeclarationSyntax outerTypeDeclSyntax) + { + if (!outerTypeDeclSyntax.IsPartial()) + { + typeMissingPartial = outerTypeDeclSyntax; + return false; + } + + outerSyntaxNode = outerSyntaxNode.Parent; + } + + typeMissingPartial = null; + return true; + } + + public static string GetDeclarationKeyword(this INamedTypeSymbol namedTypeSymbol) + { + string? keyword = namedTypeSymbol.DeclaringSyntaxReferences + .OfType<TypeDeclarationSyntax>().FirstOrDefault()? + .Keyword.Text; + + return keyword ?? namedTypeSymbol.TypeKind switch + { + TypeKind.Interface => "interface", + TypeKind.Struct => "struct", + _ => "class" + }; + } private static SymbolDisplayFormat FullyQualifiedFormatOmitGlobal { get; } = SymbolDisplayFormat.FullyQualifiedFormat .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); - public static string FullQualifiedName(this INamedTypeSymbol symbol) + public static string FullQualifiedName(this ITypeSymbol symbol) => symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal); + public static string NameWithTypeParameters(this INamedTypeSymbol symbol) + { + return symbol.IsGenericType ? + string.Concat(symbol.Name, "<", string.Join(", ", symbol.TypeParameters), ">") : + symbol.Name; + } + public static string FullQualifiedName(this INamespaceSymbol namespaceSymbol) => namespaceSymbol.ToDisplayString(FullyQualifiedFormatOmitGlobal); + + public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedName) + => qualifiedName + // AddSource() doesn't support angle brackets + .Replace("<", "(Of ") + .Replace(">", ")"); + + public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.ExportAttr; + + public static bool IsGodotSignalAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.SignalAttr; + + public static bool IsGodotMustBeVariantAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.MustBeVariantAttr; + + public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.GodotClassNameAttr; + + public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.SystemFlagsAttr; + + public static GodotMethodData? HasGodotCompatibleSignature( + this IMethodSymbol method, + MarshalUtils.TypeCache typeCache + ) + { + if (method.IsGenericMethod) + return null; + + var retSymbol = method.ReturnType; + var retType = method.ReturnsVoid ? + null : + MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache); + + if (retType == null && !method.ReturnsVoid) + return null; + + var parameters = method.Parameters; + + var paramTypes = parameters + // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may) + .Where(p => p.RefKind == RefKind.None) + // Attempt to determine the variant type + .Select(p => MarshalUtils.ConvertManagedTypeToMarshalType(p.Type, typeCache)) + // Discard parameter types that couldn't be determined (null entries) + .Where(t => t != null).Cast<MarshalType>().ToImmutableArray(); + + // If any parameter type was incompatible, it was discarded so the length won't match + if (parameters.Length > paramTypes.Length) + return null; // Ignore incompatible method + + return new GodotMethodData(method, paramTypes, parameters + .Select(p => p.Type).ToImmutableArray(), retType, retSymbol); + } + + public static IEnumerable<GodotMethodData> WhereHasGodotCompatibleSignature( + this IEnumerable<IMethodSymbol> methods, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var method in methods) + { + var methodData = HasGodotCompatibleSignature(method, typeCache); + + if (methodData != null) + yield return methodData.Value; + } + } + + public static IEnumerable<GodotPropertyData> WhereIsGodotCompatibleType( + this IEnumerable<IPropertySymbol> properties, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var property in properties) + { + // TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (property.IsWriteOnly || property.IsReadOnly) + continue; + + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(property.Type, typeCache); + + if (marshalType == null) + continue; + + yield return new GodotPropertyData(property, marshalType.Value); + } + } + + public static IEnumerable<GodotFieldData> WhereIsGodotCompatibleType( + this IEnumerable<IFieldSymbol> fields, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var field in fields) + { + // TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (field.IsReadOnly) + continue; + + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(field.Type, typeCache); + + if (marshalType == null) + continue; + + yield return new GodotFieldData(field, marshalType.Value); + } + } + + public static string Path(this Location location) + => location.SourceTree?.GetLineSpan(location.SourceSpan).Path + ?? location.GetLineSpan().Path; + + public static int StartLine(this Location location) + => location.SourceTree?.GetLineSpan(location.SourceSpan).StartLinePosition.Line + ?? location.GetLineSpan().StartLinePosition.Line; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj index 11d8e0f72b..f51b5970c3 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>8.0</LangVersion> + <LangVersion>9.0</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup> @@ -21,21 +21,13 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" /> - <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> </ItemGroup> <ItemGroup> <!-- Package the generator in the analyzer directory of the nuget package --> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <!-- Package the props file --> - <None Include="Godot.SourceGenerators.props" Pack="true" PackagePath="build" Visible="false" /> + <None Include="Godot.SourceGenerators.props" Pack="true" PackagePath="build" Visible="true" /> </ItemGroup> - - <Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack"> - <PropertyGroup> - <GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath> - <GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir> - </PropertyGroup> - <Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" /> - </Target> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props index f9b47ad5b1..7881ed0a8c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props @@ -2,6 +2,7 @@ <ItemGroup> <!-- $(GodotProjectDir) is defined by Godot.NET.Sdk --> <CompilerVisibleProperty Include="GodotProjectDir" /> - <CompilerVisibleProperty Include="GodotScriptPathAttributeGenerator" /> + <CompilerVisibleProperty Include="GodotSourceGenerators" /> + <CompilerVisibleProperty Include="IsGodotToolsProject" /> </ItemGroup> </Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs index 29e41d155a..1d8ddbabf2 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs @@ -3,7 +3,14 @@ namespace Godot.SourceGenerators public static class GodotClasses { public const string Object = "Godot.Object"; - public const string DisableGodotGeneratorsAttr = "Godot.DisableGodotGeneratorsAttribute"; public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute"; + public const string ExportAttr = "Godot.ExportAttribute"; + public const string ExportCategoryAttr = "Godot.ExportCategoryAttribute"; + public const string ExportGroupAttr = "Godot.ExportGroupAttribute"; + public const string ExportSubgroupAttr = "Godot.ExportSubgroupAttribute"; + public const string SignalAttr = "Godot.SignalAttribute"; + public const string MustBeVariantAttr = "Godot.MustBeVariantAttribute"; + public const string GodotClassNameAttr = "Godot.GodotClassName"; + public const string SystemFlagsAttr = "System.FlagsAttribute"; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs new file mode 100644 index 0000000000..1a25d684a0 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs @@ -0,0 +1,148 @@ +using System; + +namespace Godot.SourceGenerators +{ + // TODO: May need to think about compatibility here. Could Godot change these values between minor versions? + + internal enum VariantType + { + Nil = 0, + Bool = 1, + Int = 2, + Float = 3, + String = 4, + Vector2 = 5, + Vector2i = 6, + Rect2 = 7, + Rect2i = 8, + Vector3 = 9, + Vector3i = 10, + Transform2d = 11, + Vector4 = 12, + Vector4i = 13, + Plane = 14, + Quaternion = 15, + Aabb = 16, + Basis = 17, + Transform3d = 18, + Projection = 19, + Color = 20, + StringName = 21, + NodePath = 22, + Rid = 23, + Object = 24, + Callable = 25, + Signal = 26, + Dictionary = 27, + Array = 28, + PackedByteArray = 29, + PackedInt32Array = 30, + PackedInt64Array = 31, + PackedFloat32Array = 32, + PackedFloat64Array = 33, + PackedStringArray = 34, + PackedVector2Array = 35, + PackedVector3Array = 36, + PackedColorArray = 37, + Max = 38 + } + + internal enum PropertyHint + { + None = 0, + Range = 1, + Enum = 2, + EnumSuggestion = 3, + ExpEasing = 4, + Link = 5, + Flags = 6, + Layers2dRender = 7, + Layers2dPhysics = 8, + Layers2dNavigation = 9, + Layers3dRender = 10, + Layers3dPhysics = 11, + Layers3dNavigation = 12, + File = 13, + Dir = 14, + GlobalFile = 15, + GlobalDir = 16, + ResourceType = 17, + MultilineText = 18, + Expression = 19, + PlaceholderText = 20, + ColorNoAlpha = 21, + ImageCompressLossy = 22, + ImageCompressLossless = 23, + ObjectId = 24, + TypeString = 25, + NodePathToEditedNode = 26, + MethodOfVariantType = 27, + MethodOfBaseType = 28, + MethodOfInstance = 29, + MethodOfScript = 30, + PropertyOfVariantType = 31, + PropertyOfBaseType = 32, + PropertyOfInstance = 33, + PropertyOfScript = 34, + ObjectTooBig = 35, + NodePathValidTypes = 36, + SaveFile = 37, + GlobalSaveFile = 38, + IntIsObjectid = 39, + IntIsPointer = 41, + ArrayType = 40, + LocaleId = 42, + LocalizableString = 43, + NodeType = 44, + Max = 45 + } + + [Flags] + internal enum PropertyUsageFlags + { + None = 0, + Storage = 2, + Editor = 4, + Checkable = 8, + Checked = 16, + Internationalized = 32, + Group = 64, + Category = 128, + Subgroup = 256, + ClassIsBitfield = 512, + NoInstanceState = 1024, + RestartIfChanged = 2048, + ScriptVariable = 4096, + StoreIfNull = 8192, + AnimateAsTrigger = 16384, + UpdateAllIfModified = 32768, + ScriptDefaultValue = 65536, + ClassIsEnum = 131072, + NilIsVariant = 262144, + Internal = 524288, + DoNotShareOnDuplicate = 1048576, + HighEndGfx = 2097152, + NodePathFromSceneRoot = 4194304, + ResourceNotPersistent = 8388608, + KeyingIncrements = 16777216, + DeferredSetResource = 33554432, + EditorInstantiateObject = 67108864, + EditorBasicSetting = 134217728, + Array = 536870912, + Default = 6, + DefaultIntl = 38, + NoEditor = 2 + } + + public enum MethodFlags + { + Normal = 1, + Editor = 2, + Const = 4, + Virtual = 8, + Vararg = 16, + Static = 32, + ObjectCore = 64, + Default = 1 + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs new file mode 100644 index 0000000000..db395e21cb --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs @@ -0,0 +1,84 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + public struct GodotMethodData + { + public GodotMethodData(IMethodSymbol method, ImmutableArray<MarshalType> paramTypes, + ImmutableArray<ITypeSymbol> paramTypeSymbols, MarshalType? retType, ITypeSymbol? retSymbol) + { + Method = method; + ParamTypes = paramTypes; + ParamTypeSymbols = paramTypeSymbols; + RetType = retType; + RetSymbol = retSymbol; + } + + public IMethodSymbol Method { get; } + public ImmutableArray<MarshalType> ParamTypes { get; } + public ImmutableArray<ITypeSymbol> ParamTypeSymbols { get; } + public MarshalType? RetType { get; } + public ITypeSymbol? RetSymbol { get; } + } + + public struct GodotSignalDelegateData + { + public GodotSignalDelegateData(string name, INamedTypeSymbol delegateSymbol, GodotMethodData invokeMethodData) + { + Name = name; + DelegateSymbol = delegateSymbol; + InvokeMethodData = invokeMethodData; + } + + public string Name { get; } + public INamedTypeSymbol DelegateSymbol { get; } + public GodotMethodData InvokeMethodData { get; } + } + + public struct GodotPropertyData + { + public GodotPropertyData(IPropertySymbol propertySymbol, MarshalType type) + { + PropertySymbol = propertySymbol; + Type = type; + } + + public IPropertySymbol PropertySymbol { get; } + public MarshalType Type { get; } + } + + public struct GodotFieldData + { + public GodotFieldData(IFieldSymbol fieldSymbol, MarshalType type) + { + FieldSymbol = fieldSymbol; + Type = type; + } + + public IFieldSymbol FieldSymbol { get; } + public MarshalType Type { get; } + } + + public struct GodotPropertyOrFieldData + { + public GodotPropertyOrFieldData(ISymbol symbol, MarshalType type) + { + Symbol = symbol; + Type = type; + } + + public GodotPropertyOrFieldData(GodotPropertyData propertyData) + : this(propertyData.PropertySymbol, propertyData.Type) + { + } + + public GodotPropertyOrFieldData(GodotFieldData fieldData) + : this(fieldData.FieldSymbol, fieldData.Type) + { + } + + public ISymbol Symbol { get; } + public MarshalType Type { get; } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs new file mode 100644 index 0000000000..7ec3f88e5d --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs @@ -0,0 +1,63 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class GodotPluginsInitializerGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.IsGodotToolsProject()) + return; + + string source = + @"using System; +using System.Runtime.InteropServices; +using Godot.Bridge; +using Godot.NativeInterop; + +namespace GodotPlugins.Game +{ + internal static partial class Main + { + [UnmanagedCallersOnly(EntryPoint = ""godotsharp_game_main_init"")] + private static godot_bool InitializeFromGameProject(IntPtr godotDllHandle, IntPtr outManagedCallbacks, + IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) + { + try + { + DllImportResolver dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport; + + var coreApiAssembly = typeof(Godot.Object).Assembly; + + NativeLibrary.SetDllImportResolver(coreApiAssembly, dllImportResolver); + + NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize); + + ManagedCallbacks.Create(outManagedCallbacks); + + ScriptManagerBridge.LookupScriptsInAssembly(typeof(GodotPlugins.Game.Main).Assembly); + + return godot_bool.True; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return false.ToGodotBool(); + } + } + } +} +"; + + context.AddSource("GodotPlugins.Game_Generated", + SourceText.From(source, Encoding.UTF8)); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs new file mode 100644 index 0000000000..15f5803bf0 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs @@ -0,0 +1,73 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + public enum MarshalType + { + Boolean, + Char, + SByte, + Int16, + Int32, + Int64, + Byte, + UInt16, + UInt32, + UInt64, + Single, + Double, + String, + + // Godot structs + Vector2, + Vector2i, + Rect2, + Rect2i, + Transform2D, + Vector3, + Vector3i, + Basis, + Quaternion, + Transform3D, + Vector4, + Vector4i, + Projection, + AABB, + Color, + Plane, + Callable, + SignalInfo, + + // Enums + Enum, + + // Arrays + ByteArray, + Int32Array, + Int64Array, + Float32Array, + Float64Array, + StringArray, + Vector2Array, + Vector3Array, + ColorArray, + GodotObjectOrDerivedArray, + SystemArrayOfStringName, + SystemArrayOfNodePath, + SystemArrayOfRID, + + // Variant + Variant, + + // Classes + GodotObjectOrDerived, + StringName, + NodePath, + RID, + GodotDictionary, + GodotArray, + GodotGenericDictionary, + GodotGenericArray, + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs new file mode 100644 index 0000000000..efdd50098e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -0,0 +1,678 @@ +using System; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + internal static class MarshalUtils + { + public class TypeCache + { + public INamedTypeSymbol GodotObjectType { get; } + + public TypeCache(Compilation compilation) + { + INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName) + { + return compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? + throw new InvalidOperationException($"Type not found: '{fullyQualifiedMetadataName}'."); + } + + GodotObjectType = GetTypeByMetadataNameOrThrow("Godot.Object"); + } + } + + public static VariantType? ConvertMarshalTypeToVariantType(MarshalType marshalType) + => marshalType switch + { + MarshalType.Boolean => VariantType.Bool, + MarshalType.Char => VariantType.Int, + MarshalType.SByte => VariantType.Int, + MarshalType.Int16 => VariantType.Int, + MarshalType.Int32 => VariantType.Int, + MarshalType.Int64 => VariantType.Int, + MarshalType.Byte => VariantType.Int, + MarshalType.UInt16 => VariantType.Int, + MarshalType.UInt32 => VariantType.Int, + MarshalType.UInt64 => VariantType.Int, + MarshalType.Single => VariantType.Float, + MarshalType.Double => VariantType.Float, + MarshalType.String => VariantType.String, + MarshalType.Vector2 => VariantType.Vector2, + MarshalType.Vector2i => VariantType.Vector2i, + MarshalType.Rect2 => VariantType.Rect2, + MarshalType.Rect2i => VariantType.Rect2i, + MarshalType.Transform2D => VariantType.Transform2d, + MarshalType.Vector3 => VariantType.Vector3, + MarshalType.Vector3i => VariantType.Vector3i, + MarshalType.Basis => VariantType.Basis, + MarshalType.Quaternion => VariantType.Quaternion, + MarshalType.Transform3D => VariantType.Transform3d, + MarshalType.Vector4 => VariantType.Vector4, + MarshalType.Vector4i => VariantType.Vector4i, + MarshalType.Projection => VariantType.Projection, + MarshalType.AABB => VariantType.Aabb, + MarshalType.Color => VariantType.Color, + MarshalType.Plane => VariantType.Plane, + MarshalType.Callable => VariantType.Callable, + MarshalType.SignalInfo => VariantType.Signal, + MarshalType.Enum => VariantType.Int, + MarshalType.ByteArray => VariantType.PackedByteArray, + MarshalType.Int32Array => VariantType.PackedInt32Array, + MarshalType.Int64Array => VariantType.PackedInt64Array, + MarshalType.Float32Array => VariantType.PackedFloat32Array, + MarshalType.Float64Array => VariantType.PackedFloat64Array, + MarshalType.StringArray => VariantType.PackedStringArray, + MarshalType.Vector2Array => VariantType.PackedVector2Array, + MarshalType.Vector3Array => VariantType.PackedVector3Array, + MarshalType.ColorArray => VariantType.PackedColorArray, + MarshalType.GodotObjectOrDerivedArray => VariantType.Array, + MarshalType.SystemArrayOfStringName => VariantType.Array, + MarshalType.SystemArrayOfNodePath => VariantType.Array, + MarshalType.SystemArrayOfRID => VariantType.Array, + MarshalType.Variant => VariantType.Nil, + MarshalType.GodotObjectOrDerived => VariantType.Object, + MarshalType.StringName => VariantType.StringName, + MarshalType.NodePath => VariantType.NodePath, + MarshalType.RID => VariantType.Rid, + MarshalType.GodotDictionary => VariantType.Dictionary, + MarshalType.GodotArray => VariantType.Array, + MarshalType.GodotGenericDictionary => VariantType.Dictionary, + MarshalType.GodotGenericArray => VariantType.Array, + _ => null + }; + + public static MarshalType? ConvertManagedTypeToMarshalType(ITypeSymbol type, TypeCache typeCache) + { + var specialType = type.SpecialType; + + switch (specialType) + { + case SpecialType.System_Boolean: + return MarshalType.Boolean; + case SpecialType.System_Char: + return MarshalType.Char; + case SpecialType.System_SByte: + return MarshalType.SByte; + case SpecialType.System_Int16: + return MarshalType.Int16; + case SpecialType.System_Int32: + return MarshalType.Int32; + case SpecialType.System_Int64: + return MarshalType.Int64; + case SpecialType.System_Byte: + return MarshalType.Byte; + case SpecialType.System_UInt16: + return MarshalType.UInt16; + case SpecialType.System_UInt32: + return MarshalType.UInt32; + case SpecialType.System_UInt64: + return MarshalType.UInt64; + case SpecialType.System_Single: + return MarshalType.Single; + case SpecialType.System_Double: + return MarshalType.Double; + case SpecialType.System_String: + return MarshalType.String; + default: + { + var typeKind = type.TypeKind; + + if (typeKind == TypeKind.Enum) + return MarshalType.Enum; + + if (typeKind == TypeKind.Struct) + { + if (type.ContainingAssembly.Name == "GodotSharp" && + type.ContainingNamespace.Name == "Godot") + { + return type switch + { + { Name: "Vector2" } => MarshalType.Vector2, + { Name: "Vector2i" } => MarshalType.Vector2i, + { Name: "Rect2" } => MarshalType.Rect2, + { Name: "Rect2i" } => MarshalType.Rect2i, + { Name: "Transform2D" } => MarshalType.Transform2D, + { Name: "Vector3" } => MarshalType.Vector3, + { Name: "Vector3i" } => MarshalType.Vector3i, + { Name: "Basis" } => MarshalType.Basis, + { Name: "Quaternion" } => MarshalType.Quaternion, + { Name: "Transform3D" } => MarshalType.Transform3D, + { Name: "Vector4" } => MarshalType.Vector4, + { Name: "Vector4i" } => MarshalType.Vector4i, + { Name: "Projection" } => MarshalType.Projection, + { Name: "AABB" } => MarshalType.AABB, + { Name: "Color" } => MarshalType.Color, + { Name: "Plane" } => MarshalType.Plane, + { Name: "RID" } => MarshalType.RID, + { Name: "Callable" } => MarshalType.Callable, + { Name: "SignalInfo" } => MarshalType.SignalInfo, + { Name: "Variant" } => MarshalType.Variant, + _ => null + }; + } + } + else if (typeKind == TypeKind.Array) + { + var arrayType = (IArrayTypeSymbol)type; + var elementType = arrayType.ElementType; + + switch (elementType.SpecialType) + { + case SpecialType.System_Byte: + return MarshalType.ByteArray; + case SpecialType.System_Int32: + return MarshalType.Int32Array; + case SpecialType.System_Int64: + return MarshalType.Int64Array; + case SpecialType.System_Single: + return MarshalType.Float32Array; + case SpecialType.System_Double: + return MarshalType.Float64Array; + case SpecialType.System_String: + return MarshalType.StringArray; + } + + if (elementType.SimpleDerivesFrom(typeCache.GodotObjectType)) + return MarshalType.GodotObjectOrDerivedArray; + + if (elementType.ContainingAssembly.Name == "GodotSharp" && + elementType.ContainingNamespace.Name == "Godot") + { + switch (elementType) + { + case { Name: "Vector2" }: + return MarshalType.Vector2Array; + case { Name: "Vector3" }: + return MarshalType.Vector3Array; + case { Name: "Color" }: + return MarshalType.ColorArray; + case { Name: "StringName" }: + return MarshalType.SystemArrayOfStringName; + case { Name: "NodePath" }: + return MarshalType.SystemArrayOfNodePath; + case { Name: "RID" }: + return MarshalType.SystemArrayOfRID; + } + } + + return null; + } + else + { + if (type.SimpleDerivesFrom(typeCache.GodotObjectType)) + return MarshalType.GodotObjectOrDerived; + + if (type.ContainingAssembly.Name == "GodotSharp") + { + switch (type.ContainingNamespace.Name) + { + case "Godot": + return type switch + { + { Name: "StringName" } => MarshalType.StringName, + { Name: "NodePath" } => MarshalType.NodePath, + _ => null + }; + case "Collections" + when type.ContainingNamespace.FullQualifiedName() == "Godot.Collections": + return type switch + { + { Name: "Dictionary" } => + type is INamedTypeSymbol { IsGenericType: false } ? + MarshalType.GodotDictionary : + MarshalType.GodotGenericDictionary, + { Name: "Array" } => + type is INamedTypeSymbol { IsGenericType: false } ? + MarshalType.GodotArray : + MarshalType.GodotGenericArray, + _ => null + }; + } + } + } + + break; + } + } + + return null; + } + + private static bool SimpleDerivesFrom(this ITypeSymbol? type, ITypeSymbol candidateBaseType) + { + while (type != null) + { + if (SymbolEqualityComparer.Default.Equals(type, candidateBaseType)) + return true; + + type = type.BaseType; + } + + return false; + } + + public static ITypeSymbol? GetArrayElementType(ITypeSymbol typeSymbol) + { + if (typeSymbol.TypeKind == TypeKind.Array) + { + var arrayType = (IArrayTypeSymbol)typeSymbol; + return arrayType.ElementType; + } + + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType) + return genericType.TypeArguments.FirstOrDefault(); + + return null; + } + + private static StringBuilder Append(this StringBuilder source, string a, string b) + => source.Append(a).Append(b); + + private static StringBuilder Append(this StringBuilder source, string a, string b, string c) + => source.Append(a).Append(b).Append(c); + + private static StringBuilder Append(this StringBuilder source, string a, string b, + string c, string d) + => source.Append(a).Append(b).Append(c).Append(d); + + private static StringBuilder Append(this StringBuilder source, string a, string b, + string c, string d, string e) + => source.Append(a).Append(b).Append(c).Append(d).Append(e); + + private static StringBuilder Append(this StringBuilder source, string a, string b, + string c, string d, string e, string f) + => source.Append(a).Append(b).Append(c).Append(d).Append(e).Append(f); + + private static StringBuilder Append(this StringBuilder source, string a, string b, + string c, string d, string e, string f, string g) + => source.Append(a).Append(b).Append(c).Append(d).Append(e).Append(f).Append(g); + + private static StringBuilder Append(this StringBuilder source, string a, string b, + string c, string d, string e, string f, string g, string h) + => source.Append(a).Append(b).Append(c).Append(d).Append(e).Append(f).Append(g).Append(h); + + private const string VariantUtils = "global::Godot.NativeInterop.VariantUtils"; + + public static StringBuilder AppendNativeVariantToManagedExpr(this StringBuilder source, + string inputExpr, ITypeSymbol typeSymbol, MarshalType marshalType) + { + return marshalType switch + { + MarshalType.Boolean => + source.Append(VariantUtils, ".ConvertToBool(", inputExpr, ")"), + MarshalType.Char => + source.Append("(char)", VariantUtils, ".ConvertToUInt16(", inputExpr, ")"), + MarshalType.SByte => + source.Append(VariantUtils, ".ConvertToInt8(", inputExpr, ")"), + MarshalType.Int16 => + source.Append(VariantUtils, ".ConvertToInt16(", inputExpr, ")"), + MarshalType.Int32 => + source.Append(VariantUtils, ".ConvertToInt32(", inputExpr, ")"), + MarshalType.Int64 => + source.Append(VariantUtils, ".ConvertToInt64(", inputExpr, ")"), + MarshalType.Byte => + source.Append(VariantUtils, ".ConvertToUInt8(", inputExpr, ")"), + MarshalType.UInt16 => + source.Append(VariantUtils, ".ConvertToUInt16(", inputExpr, ")"), + MarshalType.UInt32 => + source.Append(VariantUtils, ".ConvertToUInt32(", inputExpr, ")"), + MarshalType.UInt64 => + source.Append(VariantUtils, ".ConvertToUInt64(", inputExpr, ")"), + MarshalType.Single => + source.Append(VariantUtils, ".ConvertToFloat32(", inputExpr, ")"), + MarshalType.Double => + source.Append(VariantUtils, ".ConvertToFloat64(", inputExpr, ")"), + MarshalType.String => + source.Append(VariantUtils, ".ConvertToStringObject(", inputExpr, ")"), + MarshalType.Vector2 => + source.Append(VariantUtils, ".ConvertToVector2(", inputExpr, ")"), + MarshalType.Vector2i => + source.Append(VariantUtils, ".ConvertToVector2i(", inputExpr, ")"), + MarshalType.Rect2 => + source.Append(VariantUtils, ".ConvertToRect2(", inputExpr, ")"), + MarshalType.Rect2i => + source.Append(VariantUtils, ".ConvertToRect2i(", inputExpr, ")"), + MarshalType.Transform2D => + source.Append(VariantUtils, ".ConvertToTransform2D(", inputExpr, ")"), + MarshalType.Vector3 => + source.Append(VariantUtils, ".ConvertToVector3(", inputExpr, ")"), + MarshalType.Vector3i => + source.Append(VariantUtils, ".ConvertToVector3i(", inputExpr, ")"), + MarshalType.Basis => + source.Append(VariantUtils, ".ConvertToBasis(", inputExpr, ")"), + MarshalType.Quaternion => + source.Append(VariantUtils, ".ConvertToQuaternion(", inputExpr, ")"), + MarshalType.Transform3D => + source.Append(VariantUtils, ".ConvertToTransform3D(", inputExpr, ")"), + MarshalType.Vector4 => + source.Append(VariantUtils, ".ConvertToVector4(", inputExpr, ")"), + MarshalType.Vector4i => + source.Append(VariantUtils, ".ConvertToVector4i(", inputExpr, ")"), + MarshalType.Projection => + source.Append(VariantUtils, ".ConvertToProjection(", inputExpr, ")"), + MarshalType.AABB => + source.Append(VariantUtils, ".ConvertToAABB(", inputExpr, ")"), + MarshalType.Color => + source.Append(VariantUtils, ".ConvertToColor(", inputExpr, ")"), + MarshalType.Plane => + source.Append(VariantUtils, ".ConvertToPlane(", inputExpr, ")"), + MarshalType.Callable => + source.Append(VariantUtils, ".ConvertToCallableManaged(", inputExpr, ")"), + MarshalType.SignalInfo => + source.Append(VariantUtils, ".ConvertToSignalInfo(", inputExpr, ")"), + MarshalType.Enum => + source.Append("(", typeSymbol.FullQualifiedName(), + ")", VariantUtils, ".ConvertToInt32(", inputExpr, ")"), + MarshalType.ByteArray => + source.Append(VariantUtils, ".ConvertAsPackedByteArrayToSystemArray(", inputExpr, ")"), + MarshalType.Int32Array => + source.Append(VariantUtils, ".ConvertAsPackedInt32ArrayToSystemArray(", inputExpr, ")"), + MarshalType.Int64Array => + source.Append(VariantUtils, ".ConvertAsPackedInt64ArrayToSystemArray(", inputExpr, ")"), + MarshalType.Float32Array => + source.Append(VariantUtils, ".ConvertAsPackedFloat32ArrayToSystemArray(", inputExpr, ")"), + MarshalType.Float64Array => + source.Append(VariantUtils, ".ConvertAsPackedFloat64ArrayToSystemArray(", inputExpr, ")"), + MarshalType.StringArray => + source.Append(VariantUtils, ".ConvertAsPackedStringArrayToSystemArray(", inputExpr, ")"), + MarshalType.Vector2Array => + source.Append(VariantUtils, ".ConvertAsPackedVector2ArrayToSystemArray(", inputExpr, ")"), + MarshalType.Vector3Array => + source.Append(VariantUtils, ".ConvertAsPackedVector3ArrayToSystemArray(", inputExpr, ")"), + MarshalType.ColorArray => + source.Append(VariantUtils, ".ConvertAsPackedColorArrayToSystemArray(", inputExpr, ")"), + MarshalType.GodotObjectOrDerivedArray => + source.Append(VariantUtils, ".ConvertToSystemArrayOfGodotObject<", + ((IArrayTypeSymbol)typeSymbol).ElementType.FullQualifiedName(), ">(", inputExpr, ")"), + MarshalType.SystemArrayOfStringName => + source.Append(VariantUtils, ".ConvertToSystemArrayOfStringName(", inputExpr, ")"), + MarshalType.SystemArrayOfNodePath => + source.Append(VariantUtils, ".ConvertToSystemArrayOfNodePath(", inputExpr, ")"), + MarshalType.SystemArrayOfRID => + source.Append(VariantUtils, ".ConvertToSystemArrayOfRID(", inputExpr, ")"), + MarshalType.Variant => + source.Append("global::Godot.Variant.CreateCopyingBorrowed(", inputExpr, ")"), + MarshalType.GodotObjectOrDerived => + source.Append("(", typeSymbol.FullQualifiedName(), + ")", VariantUtils, ".ConvertToGodotObject(", inputExpr, ")"), + MarshalType.StringName => + source.Append(VariantUtils, ".ConvertToStringNameObject(", inputExpr, ")"), + MarshalType.NodePath => + source.Append(VariantUtils, ".ConvertToNodePathObject(", inputExpr, ")"), + MarshalType.RID => + source.Append(VariantUtils, ".ConvertToRID(", inputExpr, ")"), + MarshalType.GodotDictionary => + source.Append(VariantUtils, ".ConvertToDictionaryObject(", inputExpr, ")"), + MarshalType.GodotArray => + source.Append(VariantUtils, ".ConvertToArrayObject(", inputExpr, ")"), + MarshalType.GodotGenericDictionary => + source.Append(VariantUtils, ".ConvertToDictionaryObject<", + ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedName(), ", ", + ((INamedTypeSymbol)typeSymbol).TypeArguments[1].FullQualifiedName(), ">(", inputExpr, ")"), + MarshalType.GodotGenericArray => + source.Append(VariantUtils, ".ConvertToArrayObject<", + ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedName(), ">(", inputExpr, ")"), + _ => throw new ArgumentOutOfRangeException(nameof(marshalType), marshalType, + "Received unexpected marshal type") + }; + } + + public static StringBuilder AppendManagedToNativeVariantExpr( + this StringBuilder source, string inputExpr, MarshalType marshalType) + { + return marshalType switch + { + MarshalType.Boolean => + source.Append(VariantUtils, ".CreateFromBool(", inputExpr, ")"), + MarshalType.Char => + source.Append(VariantUtils, ".CreateFromInt((ushort)", inputExpr, ")"), + MarshalType.SByte => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.Int16 => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.Int32 => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.Int64 => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.Byte => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.UInt16 => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.UInt32 => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.UInt64 => + source.Append(VariantUtils, ".CreateFromInt(", inputExpr, ")"), + MarshalType.Single => + source.Append(VariantUtils, ".CreateFromFloat(", inputExpr, ")"), + MarshalType.Double => + source.Append(VariantUtils, ".CreateFromFloat(", inputExpr, ")"), + MarshalType.String => + source.Append(VariantUtils, ".CreateFromString(", inputExpr, ")"), + MarshalType.Vector2 => + source.Append(VariantUtils, ".CreateFromVector2(", inputExpr, ")"), + MarshalType.Vector2i => + source.Append(VariantUtils, ".CreateFromVector2i(", inputExpr, ")"), + MarshalType.Rect2 => + source.Append(VariantUtils, ".CreateFromRect2(", inputExpr, ")"), + MarshalType.Rect2i => + source.Append(VariantUtils, ".CreateFromRect2i(", inputExpr, ")"), + MarshalType.Transform2D => + source.Append(VariantUtils, ".CreateFromTransform2D(", inputExpr, ")"), + MarshalType.Vector3 => + source.Append(VariantUtils, ".CreateFromVector3(", inputExpr, ")"), + MarshalType.Vector3i => + source.Append(VariantUtils, ".CreateFromVector3i(", inputExpr, ")"), + MarshalType.Basis => + source.Append(VariantUtils, ".CreateFromBasis(", inputExpr, ")"), + MarshalType.Quaternion => + source.Append(VariantUtils, ".CreateFromQuaternion(", inputExpr, ")"), + MarshalType.Transform3D => + source.Append(VariantUtils, ".CreateFromTransform3D(", inputExpr, ")"), + MarshalType.Vector4 => + source.Append(VariantUtils, ".CreateFromVector4(", inputExpr, ")"), + MarshalType.Vector4i => + source.Append(VariantUtils, ".CreateFromVector4i(", inputExpr, ")"), + MarshalType.Projection => + source.Append(VariantUtils, ".CreateFromProjection(", inputExpr, ")"), + MarshalType.AABB => + source.Append(VariantUtils, ".CreateFromAABB(", inputExpr, ")"), + MarshalType.Color => + source.Append(VariantUtils, ".CreateFromColor(", inputExpr, ")"), + MarshalType.Plane => + source.Append(VariantUtils, ".CreateFromPlane(", inputExpr, ")"), + MarshalType.Callable => + source.Append(VariantUtils, ".CreateFromCallable(", inputExpr, ")"), + MarshalType.SignalInfo => + source.Append(VariantUtils, ".CreateFromSignalInfo(", inputExpr, ")"), + MarshalType.Enum => + source.Append(VariantUtils, ".CreateFromInt((int)", inputExpr, ")"), + MarshalType.ByteArray => + source.Append(VariantUtils, ".CreateFromPackedByteArray(", inputExpr, ")"), + MarshalType.Int32Array => + source.Append(VariantUtils, ".CreateFromPackedInt32Array(", inputExpr, ")"), + MarshalType.Int64Array => + source.Append(VariantUtils, ".CreateFromPackedInt64Array(", inputExpr, ")"), + MarshalType.Float32Array => + source.Append(VariantUtils, ".CreateFromPackedFloat32Array(", inputExpr, ")"), + MarshalType.Float64Array => + source.Append(VariantUtils, ".CreateFromPackedFloat64Array(", inputExpr, ")"), + MarshalType.StringArray => + source.Append(VariantUtils, ".CreateFromPackedStringArray(", inputExpr, ")"), + MarshalType.Vector2Array => + source.Append(VariantUtils, ".CreateFromPackedVector2Array(", inputExpr, ")"), + MarshalType.Vector3Array => + source.Append(VariantUtils, ".CreateFromPackedVector3Array(", inputExpr, ")"), + MarshalType.ColorArray => + source.Append(VariantUtils, ".CreateFromPackedColorArray(", inputExpr, ")"), + MarshalType.GodotObjectOrDerivedArray => + source.Append(VariantUtils, ".CreateFromSystemArrayOfGodotObject(", inputExpr, ")"), + MarshalType.SystemArrayOfStringName => + source.Append(VariantUtils, ".CreateFromSystemArrayOfStringName(", inputExpr, ")"), + MarshalType.SystemArrayOfNodePath => + source.Append(VariantUtils, ".CreateFromSystemArrayOfNodePath(", inputExpr, ")"), + MarshalType.SystemArrayOfRID => + source.Append(VariantUtils, ".CreateFromSystemArrayOfRID(", inputExpr, ")"), + MarshalType.Variant => + source.Append(inputExpr, ".CopyNativeVariant()"), + MarshalType.GodotObjectOrDerived => + source.Append(VariantUtils, ".CreateFromGodotObject(", inputExpr, ")"), + MarshalType.StringName => + source.Append(VariantUtils, ".CreateFromStringName(", inputExpr, ")"), + MarshalType.NodePath => + source.Append(VariantUtils, ".CreateFromNodePath(", inputExpr, ")"), + MarshalType.RID => + source.Append(VariantUtils, ".CreateFromRID(", inputExpr, ")"), + MarshalType.GodotDictionary => + source.Append(VariantUtils, ".CreateFromDictionary(", inputExpr, ")"), + MarshalType.GodotArray => + source.Append(VariantUtils, ".CreateFromArray(", inputExpr, ")"), + MarshalType.GodotGenericDictionary => + source.Append(VariantUtils, ".CreateFromDictionary(", inputExpr, ")"), + MarshalType.GodotGenericArray => + source.Append(VariantUtils, ".CreateFromArray(", inputExpr, ")"), + _ => throw new ArgumentOutOfRangeException(nameof(marshalType), marshalType, + "Received unexpected marshal type") + }; + } + + public static StringBuilder AppendVariantToManagedExpr(this StringBuilder source, + string inputExpr, ITypeSymbol typeSymbol, MarshalType marshalType) + { + return marshalType switch + { + MarshalType.Boolean => source.Append(inputExpr, ".AsBool()"), + MarshalType.Char => source.Append(inputExpr, ".AsChar()"), + MarshalType.SByte => source.Append(inputExpr, ".AsSByte()"), + MarshalType.Int16 => source.Append(inputExpr, ".AsInt16()"), + MarshalType.Int32 => source.Append(inputExpr, ".AsInt32()"), + MarshalType.Int64 => source.Append(inputExpr, ".AsInt64()"), + MarshalType.Byte => source.Append(inputExpr, ".AsByte()"), + MarshalType.UInt16 => source.Append(inputExpr, ".AsUInt16()"), + MarshalType.UInt32 => source.Append(inputExpr, ".AsUInt32()"), + MarshalType.UInt64 => source.Append(inputExpr, ".AsUInt64()"), + MarshalType.Single => source.Append(inputExpr, ".AsSingle()"), + MarshalType.Double => source.Append(inputExpr, ".AsDouble()"), + MarshalType.String => source.Append(inputExpr, ".AsString()"), + MarshalType.Vector2 => source.Append(inputExpr, ".AsVector2()"), + MarshalType.Vector2i => source.Append(inputExpr, ".AsVector2i()"), + MarshalType.Rect2 => source.Append(inputExpr, ".AsRect2()"), + MarshalType.Rect2i => source.Append(inputExpr, ".AsRect2i()"), + MarshalType.Transform2D => source.Append(inputExpr, ".AsTransform2D()"), + MarshalType.Vector3 => source.Append(inputExpr, ".AsVector3()"), + MarshalType.Vector3i => source.Append(inputExpr, ".AsVector3i()"), + MarshalType.Basis => source.Append(inputExpr, ".AsBasis()"), + MarshalType.Quaternion => source.Append(inputExpr, ".AsQuaternion()"), + MarshalType.Transform3D => source.Append(inputExpr, ".AsTransform3D()"), + MarshalType.Vector4 => source.Append(inputExpr, ".AsVector4()"), + MarshalType.Vector4i => source.Append(inputExpr, ".AsVector4i()"), + MarshalType.Projection => source.Append(inputExpr, ".AsProjection()"), + MarshalType.AABB => source.Append(inputExpr, ".AsAABB()"), + MarshalType.Color => source.Append(inputExpr, ".AsColor()"), + MarshalType.Plane => source.Append(inputExpr, ".AsPlane()"), + MarshalType.Callable => source.Append(inputExpr, ".AsCallable()"), + MarshalType.SignalInfo => source.Append(inputExpr, ".AsSignalInfo()"), + MarshalType.Enum => + source.Append("(", typeSymbol.FullQualifiedName(), ")", inputExpr, ".AsInt64()"), + MarshalType.ByteArray => source.Append(inputExpr, ".AsByteArray()"), + MarshalType.Int32Array => source.Append(inputExpr, ".AsInt32Array()"), + MarshalType.Int64Array => source.Append(inputExpr, ".AsInt64Array()"), + MarshalType.Float32Array => source.Append(inputExpr, ".AsFloat32Array()"), + MarshalType.Float64Array => source.Append(inputExpr, ".AsFloat64Array()"), + MarshalType.StringArray => source.Append(inputExpr, ".AsStringArray()"), + MarshalType.Vector2Array => source.Append(inputExpr, ".AsVector2Array()"), + MarshalType.Vector3Array => source.Append(inputExpr, ".AsVector3Array()"), + MarshalType.ColorArray => source.Append(inputExpr, ".AsColorArray()"), + MarshalType.GodotObjectOrDerivedArray => source.Append(inputExpr, ".AsGodotObjectArray<", + ((IArrayTypeSymbol)typeSymbol).ElementType.FullQualifiedName(), ">()"), + MarshalType.SystemArrayOfStringName => source.Append(inputExpr, ".AsSystemArrayOfStringName()"), + MarshalType.SystemArrayOfNodePath => source.Append(inputExpr, ".AsSystemArrayOfNodePath()"), + MarshalType.SystemArrayOfRID => source.Append(inputExpr, ".AsSystemArrayOfRID()"), + MarshalType.Variant => source.Append(inputExpr), + MarshalType.GodotObjectOrDerived => source.Append("(", + typeSymbol.FullQualifiedName(), ")", inputExpr, ".AsGodotObject()"), + MarshalType.StringName => source.Append(inputExpr, ".AsStringName()"), + MarshalType.NodePath => source.Append(inputExpr, ".AsNodePath()"), + MarshalType.RID => source.Append(inputExpr, ".AsRID()"), + MarshalType.GodotDictionary => source.Append(inputExpr, ".AsGodotDictionary()"), + MarshalType.GodotArray => source.Append(inputExpr, ".AsGodotArray()"), + MarshalType.GodotGenericDictionary => source.Append(inputExpr, ".AsGodotDictionary<", + ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedName(), ", ", + ((INamedTypeSymbol)typeSymbol).TypeArguments[1].FullQualifiedName(), ">()"), + MarshalType.GodotGenericArray => source.Append(inputExpr, ".AsGodotArray<", + ((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedName(), ">()"), + _ => throw new ArgumentOutOfRangeException(nameof(marshalType), marshalType, + "Received unexpected marshal type") + }; + } + + public static StringBuilder AppendManagedToVariantExpr(this StringBuilder source, + string inputExpr, MarshalType marshalType) + { + switch (marshalType) + { + case MarshalType.Boolean: + case MarshalType.Char: + case MarshalType.SByte: + case MarshalType.Int16: + case MarshalType.Int32: + case MarshalType.Int64: + case MarshalType.Byte: + case MarshalType.UInt16: + case MarshalType.UInt32: + case MarshalType.UInt64: + case MarshalType.Single: + case MarshalType.Double: + case MarshalType.String: + case MarshalType.Vector2: + case MarshalType.Vector2i: + case MarshalType.Rect2: + case MarshalType.Rect2i: + case MarshalType.Transform2D: + case MarshalType.Vector3: + case MarshalType.Vector3i: + case MarshalType.Basis: + case MarshalType.Quaternion: + case MarshalType.Transform3D: + case MarshalType.Vector4: + case MarshalType.Vector4i: + case MarshalType.Projection: + case MarshalType.AABB: + case MarshalType.Color: + case MarshalType.Plane: + case MarshalType.Callable: + case MarshalType.SignalInfo: + case MarshalType.ByteArray: + case MarshalType.Int32Array: + case MarshalType.Int64Array: + case MarshalType.Float32Array: + case MarshalType.Float64Array: + case MarshalType.StringArray: + case MarshalType.Vector2Array: + case MarshalType.Vector3Array: + case MarshalType.ColorArray: + case MarshalType.GodotObjectOrDerivedArray: + case MarshalType.SystemArrayOfStringName: + case MarshalType.SystemArrayOfNodePath: + case MarshalType.SystemArrayOfRID: + case MarshalType.GodotObjectOrDerived: + case MarshalType.StringName: + case MarshalType.NodePath: + case MarshalType.RID: + case MarshalType.GodotDictionary: + case MarshalType.GodotArray: + case MarshalType.GodotGenericDictionary: + case MarshalType.GodotGenericArray: + return source.Append("Variant.CreateFrom(", inputExpr, ")"); + case MarshalType.Enum: + return source.Append("Variant.CreateFrom((long)", inputExpr, ")"); + case MarshalType.Variant: + return source.Append(inputExpr); + default: + throw new ArgumentOutOfRangeException(nameof(marshalType), marshalType, + "Received unexpected marshal type"); + } + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MethodInfo.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MethodInfo.cs new file mode 100644 index 0000000000..81c6f2b7d5 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MethodInfo.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Godot.SourceGenerators +{ + internal struct MethodInfo + { + public MethodInfo(string name, PropertyInfo returnVal, MethodFlags flags, + List<PropertyInfo>? arguments, + List<string?>? defaultArguments) + { + Name = name; + ReturnVal = returnVal; + Flags = flags; + Arguments = arguments; + DefaultArguments = defaultArguments; + } + + public string Name { get; } + public PropertyInfo ReturnVal { get; } + public MethodFlags Flags { get; } + public List<PropertyInfo>? Arguments { get; } + public List<string?>? DefaultArguments { get; } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs new file mode 100644 index 0000000000..7aaadb27be --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs @@ -0,0 +1,100 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Godot.SourceGenerators +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class MustBeVariantAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics + => ImmutableArray.Create( + Common.GenericTypeArgumentMustBeVariantRule, + Common.GenericTypeParameterMustBeVariantAnnotatedRule, + Common.TypeArgumentParentSymbolUnhandledRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.TypeArgumentList); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var typeArgListSyntax = (TypeArgumentListSyntax)context.Node; + + // Method invocation or variable declaration that contained the type arguments + var parentSyntax = context.Node.Parent; + Debug.Assert(parentSyntax != null); + + var sm = context.SemanticModel; + + var typeCache = new MarshalUtils.TypeCache(context.Compilation); + + for (int i = 0; i < typeArgListSyntax.Arguments.Count; i++) + { + var typeSyntax = typeArgListSyntax.Arguments[i]; + var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol; + Debug.Assert(typeSymbol != null); + + var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol; + + if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i)) + { + return; + } + + if (typeSymbol is ITypeParameterSymbol typeParamSymbol) + { + if (!typeParamSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false)) + { + Common.ReportGenericTypeParameterMustBeVariantAnnotated(context, typeSyntax, typeSymbol); + } + continue; + } + + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(typeSymbol, typeCache); + + if (marshalType == null) + { + Common.ReportGenericTypeArgumentMustBeVariant(context, typeSyntax, typeSymbol); + continue; + } + } + } + + /// <summary> + /// Check if the given type argument is being used in a type parameter that contains + /// the <c>MustBeVariantAttribute</c>; otherwise, we ignore the attribute. + /// </summary> + /// <param name="context">Context for a syntax node action.</param> + /// <param name="parentSyntax">The parent node syntax that contains the type node syntax.</param> + /// <param name="parentSymbol">The symbol retrieved for the parent node syntax.</param> + /// <param name="typeArgumentSyntax">The type node syntax of the argument type to check.</param> + /// <param name="typeArgumentSymbol">The symbol retrieved for the type node syntax.</param> + /// <returns><see langword="true"/> if the type must be variant and must be analyzed.</returns> + private bool ShouldCheckTypeArgument(SyntaxNodeAnalysisContext context, SyntaxNode parentSyntax, ISymbol parentSymbol, TypeSyntax typeArgumentSyntax, ITypeSymbol typeArgumentSymbol, int typeArgumentIndex) + { + var typeParamSymbol = parentSymbol switch + { + IMethodSymbol methodSymbol => methodSymbol.TypeParameters[typeArgumentIndex], + INamedTypeSymbol typeSymbol => typeSymbol.TypeParameters[typeArgumentIndex], + _ => null, + }; + + if (typeParamSymbol == null) + { + Common.ReportTypeArgumentParentSymbolUnhandled(context, typeArgumentSyntax, parentSymbol); + return false; + } + + return typeParamSymbol.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs new file mode 100644 index 0000000000..b345f5f84d --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/PropertyInfo.cs @@ -0,0 +1,23 @@ +namespace Godot.SourceGenerators +{ + internal struct PropertyInfo + { + public PropertyInfo(VariantType type, string name, PropertyHint hint, + string? hintString, PropertyUsageFlags usage, bool exported) + { + Type = type; + Name = name; + Hint = hint; + HintString = hintString; + Usage = usage; + Exported = exported; + } + + public VariantType Type { get; } + public string Name { get; } + public PropertyHint Hint { get; } + public string? HintString { get; } + public PropertyUsageFlags Usage { get; } + public bool Exported { 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 new file mode 100644 index 0000000000..5ac4f4a47e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -0,0 +1,408 @@ +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 ScriptMethodsGenerator : 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 class MethodOverloadEqualityComparer : IEqualityComparer<GodotMethodData> + { + public bool Equals(GodotMethodData x, GodotMethodData y) + => x.ParamTypes.Length == y.ParamTypes.Length && x.Method.Name == y.Method.Name; + + public int GetHashCode(GodotMethodData obj) + { + unchecked + { + return (obj.ParamTypes.Length.GetHashCode() * 397) ^ obj.Method.Name.GetHashCode(); + } + } + } + + private static void VisitGodotScriptClass( + GeneratorExecutionContext context, + MarshalUtils.TypeCache typeCache, + INamedTypeSymbol symbol + ) + { + INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; + string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? + namespaceSymbol.FullQualifiedName() : + string.Empty; + bool hasNamespace = classNs.Length != 0; + + bool isInnerClass = symbol.ContainingType != null; + + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptMethods_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 methodSymbols = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared) + .Cast<IMethodSymbol>() + .Where(m => m.MethodKind == MethodKind.Ordinary); + + var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache) + .Distinct(new MethodOverloadEqualityComparer()) + .ToArray(); + + source.Append(" private partial class GodotInternal {\n"); + + // Generate cached StringNames for methods and properties, for fast lookup + + var distinctMethodNames = godotClassMethods + .Select(m => m.Method.Name) + .Distinct() + .ToArray(); + + foreach (string methodName in distinctMethodNames) + { + source.Append(" public static readonly StringName MethodName_"); + source.Append(methodName); + source.Append(" = \""); + source.Append(methodName); + source.Append("\";\n"); + } + + source.Append(" }\n"); // class GodotInternal + + // Generate GetGodotMethodList + + if (godotClassMethods.Length > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + const string listType = "System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>"; + + source.Append(" internal new static ") + .Append(listType) + .Append(" GetGodotMethodList()\n {\n"); + + source.Append(" var methods = new ") + .Append(listType) + .Append("(") + .Append(godotClassMethods.Length) + .Append(");\n"); + + foreach (var method in godotClassMethods) + { + var methodInfo = DetermineMethodInfo(method); + AppendMethodInfo(source, methodInfo); + } + + source.Append(" return methods;\n"); + source.Append(" }\n"); + + source.Append("#pragma warning restore CS0109\n"); + } + + // Generate InvokeGodotClassMethod + + if (godotClassMethods.Length > 0) + { + source.Append(" protected override bool InvokeGodotClassMethod(in godot_string_name method, "); + source.Append("NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n {\n"); + + foreach (var method in godotClassMethods) + { + GenerateMethodInvoker(method, source); + } + + source.Append(" return base.InvokeGodotClassMethod(method, args, argCount, out ret);\n"); + + source.Append(" }\n"); + } + + // Generate HasGodotClassMethod + + if (distinctMethodNames.Length > 0) + { + source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n"); + + bool isFirstEntry = true; + foreach (string methodName in distinctMethodNames) + { + GenerateHasMethodEntry(methodName, source, isFirstEntry); + isFirstEntry = false; + } + + source.Append(" return base.HasGodotClassMethod(method);\n"); + + source.Append(" }\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 AppendMethodInfo(StringBuilder source, MethodInfo methodInfo) + { + source.Append(" methods.Add(new(name: GodotInternal.MethodName_") + .Append(methodInfo.Name) + .Append(", returnVal: "); + + AppendPropertyInfo(source, methodInfo.ReturnVal); + + source.Append(", flags: (Godot.MethodFlags)") + .Append((int)methodInfo.Flags) + .Append(", arguments: "); + + if (methodInfo.Arguments is { Count: > 0 }) + { + source.Append("new() { "); + + foreach (var param in methodInfo.Arguments) + { + AppendPropertyInfo(source, param); + + // C# allows colon after the last element + source.Append(", "); + } + + source.Append(" }"); + } + else + { + source.Append("null"); + } + + source.Append(", defaultArguments: null));\n"); + } + + private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) + { + source.Append("new(type: (Godot.Variant.Type)") + .Append((int)propertyInfo.Type) + .Append(", name: \"") + .Append(propertyInfo.Name) + .Append("\", hint: (Godot.PropertyHint)") + .Append((int)propertyInfo.Hint) + .Append(", hintString: \"") + .Append(propertyInfo.HintString) + .Append("\", usage: (Godot.PropertyUsageFlags)") + .Append((int)propertyInfo.Usage) + .Append(", exported: ") + .Append(propertyInfo.Exported ? "true" : "false") + .Append(")"); + } + + private static MethodInfo DetermineMethodInfo(GodotMethodData method) + { + PropertyInfo returnVal; + + if (method.RetType != null) + { + returnVal = DeterminePropertyInfo(method.RetType.Value, name: string.Empty); + } + else + { + returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None, + hintString: null, PropertyUsageFlags.Default, exported: false); + } + + int paramCount = method.ParamTypes.Length; + + List<PropertyInfo>? arguments; + + if (paramCount > 0) + { + arguments = new(capacity: paramCount); + + for (int i = 0; i < paramCount; i++) + { + arguments.Add(DeterminePropertyInfo(method.ParamTypes[i], + name: method.Method.Parameters[i].Name)); + } + } + else + { + arguments = null; + } + + return new MethodInfo(method.Method.Name, returnVal, MethodFlags.Default, arguments, + defaultArguments: null); + } + + private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, string name) + { + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + + var propUsage = PropertyUsageFlags.Default; + + if (memberVariantType == VariantType.Nil) + propUsage |= PropertyUsageFlags.NilIsVariant; + + return new PropertyInfo(memberVariantType, name, + PropertyHint.None, string.Empty, propUsage, exported: false); + } + + private static void GenerateHasMethodEntry( + string methodName, + StringBuilder source, + bool isFirstEntry + ) + { + source.Append(" "); + if (!isFirstEntry) + source.Append("else "); + source.Append("if (method == GodotInternal.MethodName_"); + source.Append(methodName); + source.Append(") {\n return true;\n }\n"); + } + + private static void GenerateMethodInvoker( + GodotMethodData method, + StringBuilder source + ) + { + string methodName = method.Method.Name; + + source.Append(" if (method == GodotInternal.MethodName_"); + source.Append(methodName); + source.Append(" && argCount == "); + source.Append(method.ParamTypes.Length); + source.Append(") {\n"); + + if (method.RetType != null) + source.Append(" var callRet = "); + else + source.Append(" "); + + source.Append(methodName); + source.Append("("); + + for (int i = 0; i < method.ParamTypes.Length; i++) + { + if (i != 0) + source.Append(", "); + + source.AppendNativeVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), + method.ParamTypeSymbols[i], method.ParamTypes[i]); + } + + source.Append(");\n"); + + if (method.RetType != null) + { + source.Append(" ret = "); + + source.AppendManagedToNativeVariantExpr("callRet", method.RetType.Value); + source.Append(";\n"); + + source.Append(" return true;\n"); + } + else + { + source.Append(" ret = default;\n"); + source.Append(" return true;\n"); + } + + source.Append(" }\n"); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index fa65595290..e8a9e28d0c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -14,13 +14,13 @@ namespace Godot.SourceGenerators { public void Execute(GeneratorExecutionContext context) { - if (context.TryGetGlobalAnalyzerProperty("GodotScriptPathAttributeGenerator", out string? toggle) - && toggle == "disabled") - { + if (context.AreGodotSourceGeneratorsDisabled()) + return; + + if (context.IsGodotToolsProject()) return; - } - // NOTE: IsNullOrEmpty doesn't work well with nullable checks + // NOTE: NotNullWhen diagnostics don't work on projects targeting .NET Standard 2.0 // ReSharper disable once ReplaceWithStringIsNullOrEmpty if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir) || godotProjectDir!.Length == 0) @@ -28,17 +28,18 @@ namespace Godot.SourceGenerators throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty."); } - var godotClasses = context.Compilation.SyntaxTrees + Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses = context + .Compilation.SyntaxTrees .SelectMany(tree => tree.GetRoot().DescendantNodes() .OfType<ClassDeclarationSyntax>() // Ignore inner classes - .Where(cds => !(cds.Parent is ClassDeclarationSyntax)) + .Where(cds => !cds.IsNested()) .SelectGodotScriptClasses(context.Compilation) // Report and skip non-partial classes .Where(x => { - if (x.cds.IsPartial() || x.symbol.HasDisableGeneratorsAttribute()) + if (x.cds.IsPartial()) return true; Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; @@ -89,21 +90,14 @@ namespace Godot.SourceGenerators attributes.Append(@""")]"); } - string className = symbol.Name; - INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? namespaceSymbol.FullQualifiedName() : string.Empty; bool hasNamespace = classNs.Length != 0; - var uniqueName = new StringBuilder(); - if (hasNamespace) - uniqueName.Append($"{classNs}."); - uniqueName.Append(className); - if (symbol.IsGenericType) - uniqueName.Append($"Of{string.Join(string.Empty, symbol.TypeParameters)}"); - uniqueName.Append("_ScriptPath_Generated"); + var uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptPath_Generated"; var source = new StringBuilder(); @@ -123,10 +117,8 @@ namespace Godot.SourceGenerators } source.Append(attributes); - source.Append("\n partial class "); - source.Append(className); - if (symbol.IsGenericType) - source.Append($"<{string.Join(", ", symbol.TypeParameters)}>"); + source.Append("\npartial class "); + source.Append(symbol.NameWithTypeParameters()); source.Append("\n{\n}\n"); if (hasNamespace) @@ -134,7 +126,7 @@ namespace Godot.SourceGenerators source.Append("\n}\n"); } - context.AddSource(uniqueName.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); + context.AddSource(uniqueHint.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); } private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context, diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs new file mode 100644 index 0000000000..fc46d82dff --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -0,0 +1,658 @@ +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.FullQualifiedName() : + string.Empty; + bool hasNamespace = classNs.Length != 0; + + bool isInnerClass = symbol.ContainingType != null; + + string uniqueHint = symbol.FullQualifiedName().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(" private partial class GodotInternal {\n"); + + // Generate cached StringNames for methods and properties, for fast lookup + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.Name; + source.Append(" public static readonly StringName PropName_"); + source.Append(propertyName); + source.Append(" = \""); + source.Append(propertyName); + source.Append("\";\n"); + } + + foreach (var field in godotClassFields) + { + string fieldName = field.FieldSymbol.Name; + source.Append(" public static readonly StringName PropName_"); + 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.Type, source, isFirstEntry); + isFirstEntry = false; + } + + foreach (var field in godotClassFields) + { + GeneratePropertyGetter(field.FieldSymbol.Name, + field.Type, source, isFirstEntry); + isFirstEntry = false; + } + + source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n"); + + source.Append(" }\n"); + + // Generate GetGodotPropertyList + + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + string dictionaryType = "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 == GodotInternal.PropName_") + .Append(propertyMemberName) + .Append(") {\n") + .Append(" ") + .Append(propertyMemberName) + .Append(" = ") + .AppendNativeVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType) + .Append(";\n") + .Append(" return true;\n") + .Append(" }\n"); + } + + private static void GeneratePropertyGetter( + string propertyMemberName, + MarshalType propertyMarshalType, + StringBuilder source, + bool isFirstEntry + ) + { + source.Append(" "); + + if (!isFirstEntry) + source.Append("else "); + + source.Append("if (name == GodotInternal.PropName_") + .Append(propertyMemberName) + .Append(") {\n") + .Append(" value = ") + .AppendManagedToNativeVariantExpr(propertyMemberName, 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: (Godot.Variant.Type)") + .Append((int)propertyInfo.Type) + .Append(", name: GodotInternal.PropName_") + .Append(propertyInfo.Name) + .Append(", hint: (Godot.PropertyHint)") + .Append((int)propertyInfo.Hint) + .Append(", hintString: \"") + .Append(propertyInfo.HintString) + .Append("\", usage: (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; + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs new file mode 100644 index 0000000000..c7745391d0 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs @@ -0,0 +1,298 @@ +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 ScriptPropertyDefValGenerator : 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.FullQualifiedName() : + string.Empty; + bool hasNamespace = classNs.Length != 0; + + bool isInnerClass = symbol.ContainingType != null; + + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptPropertyDefVal_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 exportedMembers = new List<ExportedPropertyMetadata>(); + + var members = symbol.GetMembers(); + + var exportedProperties = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) + .Cast<IPropertySymbol>() + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) + .ToArray(); + + var exportedFields = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast<IFieldSymbol>() + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) + .ToArray(); + + foreach (var property in exportedProperties) + { + if (property.IsStatic) + { + Common.ReportExportedMemberIsStatic(context, property); + continue; + } + + if (property.IsIndexer) + { + Common.ReportExportedMemberIsIndexer(context, property); + continue; + } + + // TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (property.IsWriteOnly) + { + Common.ReportExportedMemberIsWriteOnly(context, property); + continue; + } + + if (property.IsReadOnly) + { + Common.ReportExportedMemberIsReadOnly(context, property); + continue; + } + + var propertyType = property.Type; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(propertyType, typeCache); + + if (marshalType == null) + { + Common.ReportExportedMemberTypeNotSupported(context, property); + continue; + } + + // TODO: Detect default value from simple property getters (currently we only detect from initializers) + + EqualsValueClauseSyntax? initializer = property.DeclaringSyntaxReferences + .Select(r => r.GetSyntax() as PropertyDeclarationSyntax) + .Select(s => s?.Initializer ?? null) + .FirstOrDefault(); + + string? value = initializer?.Value.ToString(); + + exportedMembers.Add(new ExportedPropertyMetadata( + property.Name, marshalType.Value, propertyType, value)); + } + + foreach (var field in exportedFields) + { + if (field.IsStatic) + { + Common.ReportExportedMemberIsStatic(context, field); + continue; + } + + // TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (field.IsReadOnly) + { + Common.ReportExportedMemberIsReadOnly(context, field); + continue; + } + + var fieldType = field.Type; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(fieldType, typeCache); + + if (marshalType == null) + { + Common.ReportExportedMemberTypeNotSupported(context, field); + continue; + } + + EqualsValueClauseSyntax? initializer = field.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) + .OfType<VariableDeclaratorSyntax>() + .Select(s => s.Initializer) + .FirstOrDefault(i => i != null); + + string? value = initializer?.Value.ToString(); + + exportedMembers.Add(new ExportedPropertyMetadata( + field.Name, marshalType.Value, fieldType, value)); + } + + // Generate GetGodotExportedProperties + + if (exportedMembers.Count > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + string dictionaryType = "System.Collections.Generic.Dictionary<StringName, object>"; + + source.Append("#if TOOLS\n"); + source.Append(" internal new static "); + source.Append(dictionaryType); + source.Append(" GetGodotPropertyDefaultValues()\n {\n"); + + source.Append(" var values = new "); + source.Append(dictionaryType); + source.Append("("); + source.Append(exportedMembers.Count); + source.Append(");\n"); + + foreach (var exportedMember in exportedMembers) + { + string defaultValueLocalName = string.Concat("__", exportedMember.Name, "_default_value"); + + source.Append(" "); + source.Append(exportedMember.TypeSymbol.FullQualifiedName()); + source.Append(" "); + source.Append(defaultValueLocalName); + source.Append(" = "); + source.Append(exportedMember.Value ?? "default"); + source.Append(";\n"); + source.Append(" values.Add(GodotInternal.PropName_"); + source.Append(exportedMember.Name); + source.Append(", "); + source.Append(defaultValueLocalName); + source.Append(");\n"); + } + + source.Append(" return values;\n"); + source.Append(" }\n"); + source.Append("#endif\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 struct ExportedPropertyMetadata + { + public ExportedPropertyMetadata(string name, MarshalType type, ITypeSymbol typeSymbol, string? value) + { + Name = name; + Type = type; + TypeSymbol = typeSymbol; + Value = value; + } + + public string Name { get; } + public MarshalType Type { get; } + public ITypeSymbol TypeSymbol { get; } + public string? Value { get; } + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptRegistrarGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptRegistrarGenerator.cs new file mode 100644 index 0000000000..ec04a319e2 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptRegistrarGenerator.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + // Placeholder. Once we switch to native extensions this will act as the registrar for all + // user Godot classes in the assembly. Think of it as something similar to `register_types`. + public class ScriptRegistrarGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + throw new System.NotImplementedException(); + } + + public void Execute(GeneratorExecutionContext context) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs new file mode 100644 index 0000000000..39a99ff8ba --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs @@ -0,0 +1,284 @@ +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 ScriptSerializationGenerator : 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.FullQualifiedName() : + string.Empty; + bool hasNamespace = classNs.Length != 0; + + bool isInnerClass = symbol.ContainingType != null; + + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptSerialization_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(); + + var signalDelegateSymbols = members + .Where(s => s.Kind == SymbolKind.NamedType) + .Cast<INamedTypeSymbol>() + .Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate) + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false)); + + List<GodotSignalDelegateData> godotSignalDelegates = new(); + + foreach (var signalDelegateSymbol in signalDelegateSymbols) + { + if (!signalDelegateSymbol.Name.EndsWith(ScriptSignalsGenerator.SignalDelegateSuffix)) + continue; + + string signalName = signalDelegateSymbol.Name; + signalName = signalName.Substring(0, + signalName.Length - ScriptSignalsGenerator.SignalDelegateSuffix.Length); + + var invokeMethodData = signalDelegateSymbol + .DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache); + + if (invokeMethodData == null) + continue; + + godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value)); + } + + source.Append( + " protected override void SaveGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n {\n"); + source.Append(" base.SaveGodotObjectData(info);\n"); + + // Save properties + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.Name; + + source.Append(" info.AddProperty(GodotInternal.PropName_") + .Append(propertyName) + .Append(", ") + .AppendManagedToVariantExpr(string.Concat("this.", propertyName), property.Type) + .Append(");\n"); + } + + // Save fields + + foreach (var field in godotClassFields) + { + string fieldName = field.FieldSymbol.Name; + + source.Append(" info.AddProperty(GodotInternal.PropName_") + .Append(fieldName) + .Append(", ") + .AppendManagedToVariantExpr(string.Concat("this.", fieldName), field.Type) + .Append(");\n"); + } + + // Save signal events + + foreach (var signalDelegate in godotSignalDelegates) + { + string signalName = signalDelegate.Name; + + source.Append(" info.AddSignalEventDelegate(GodotInternal.SignalName_") + .Append(signalName) + .Append(", this.backing_") + .Append(signalName) + .Append(");\n"); + } + + source.Append(" }\n"); + + source.Append( + " protected override void RestoreGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n {\n"); + source.Append(" base.RestoreGodotObjectData(info);\n"); + + // Restore properties + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.Name; + + source.Append(" if (info.TryGetProperty(GodotInternal.PropName_") + .Append(propertyName) + .Append(", out var _value_") + .Append(propertyName) + .Append("))\n") + .Append(" this.") + .Append(propertyName) + .Append(" = ") + .AppendVariantToManagedExpr(string.Concat("_value_", propertyName), + property.PropertySymbol.Type, property.Type) + .Append(";\n"); + } + + // Restore fields + + foreach (var field in godotClassFields) + { + string fieldName = field.FieldSymbol.Name; + + source.Append(" if (info.TryGetProperty(GodotInternal.PropName_") + .Append(fieldName) + .Append(", out var _value_") + .Append(fieldName) + .Append("))\n") + .Append(" this.") + .Append(fieldName) + .Append(" = ") + .AppendVariantToManagedExpr(string.Concat("_value_", fieldName), + field.FieldSymbol.Type, field.Type) + .Append(";\n"); + } + + // Restore signal events + + foreach (var signalDelegate in godotSignalDelegates) + { + string signalName = signalDelegate.Name; + string signalDelegateQualifiedName = signalDelegate.DelegateSymbol.FullQualifiedName(); + + source.Append(" if (info.TryGetSignalEventDelegate<") + .Append(signalDelegateQualifiedName) + .Append(">(GodotInternal.SignalName_") + .Append(signalName) + .Append(", out var _value_") + .Append(signalName) + .Append("))\n") + .Append(" this.backing_") + .Append(signalName) + .Append(" = _value_") + .Append(signalName) + .Append(";\n"); + } + + source.Append(" }\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)); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs new file mode 100644 index 0000000000..6b06f10db1 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -0,0 +1,428 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +// TODO: +// Determine a proper way to emit the signal. +// 'Emit(nameof(TheEvent))' creates a StringName everytime and has the overhead of string marshaling. +// I haven't decided on the best option yet. Some possibilities: +// - Expose the generated StringName fields to the user, for use with 'Emit(...)'. +// - Generate a 'EmitSignalName' method for each event signal. + +namespace Godot.SourceGenerators +{ + [Generator] + public class ScriptSignalsGenerator : 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); + } + } + } + + internal static string SignalDelegateSuffix = "EventHandler"; + + private static void VisitGodotScriptClass( + GeneratorExecutionContext context, + MarshalUtils.TypeCache typeCache, + INamedTypeSymbol symbol + ) + { + INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; + string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? + namespaceSymbol.FullQualifiedName() : + string.Empty; + bool hasNamespace = classNs.Length != 0; + + bool isInnerClass = symbol.ContainingType != null; + + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptSignals_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 signalDelegateSymbols = members + .Where(s => s.Kind == SymbolKind.NamedType) + .Cast<INamedTypeSymbol>() + .Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate) + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false)); + + List<GodotSignalDelegateData> godotSignalDelegates = new(); + + foreach (var signalDelegateSymbol in signalDelegateSymbols) + { + if (!signalDelegateSymbol.Name.EndsWith(SignalDelegateSuffix)) + { + Common.ReportSignalDelegateMissingSuffix(context, signalDelegateSymbol); + continue; + } + + string signalName = signalDelegateSymbol.Name; + signalName = signalName.Substring(0, signalName.Length - SignalDelegateSuffix.Length); + + var invokeMethodData = signalDelegateSymbol + .DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache); + + if (invokeMethodData == null) + { + if (signalDelegateSymbol.DelegateInvokeMethod is IMethodSymbol methodSymbol) + { + foreach (var parameter in methodSymbol.Parameters) + { + if (parameter.RefKind != RefKind.None) + { + Common.ReportSignalParameterTypeNotSupported(context, parameter); + continue; + } + + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(parameter.Type, typeCache); + + if (marshalType == null) + { + Common.ReportSignalParameterTypeNotSupported(context, parameter); + } + } + + if (!methodSymbol.ReturnsVoid) + { + Common.ReportSignalDelegateSignatureMustReturnVoid(context, signalDelegateSymbol); + } + } + continue; + } + + godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value)); + } + + source.Append(" private partial class GodotInternal {\n"); + + // Generate cached StringNames for methods and properties, for fast lookup + + foreach (var signalDelegate in godotSignalDelegates) + { + string signalName = signalDelegate.Name; + source.Append(" public static readonly StringName SignalName_"); + source.Append(signalName); + source.Append(" = \""); + source.Append(signalName); + source.Append("\";\n"); + } + + source.Append(" }\n"); // class GodotInternal + + // Generate GetGodotSignalList + + if (godotSignalDelegates.Count > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + const string listType = "System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>"; + + source.Append(" internal new static ") + .Append(listType) + .Append(" GetGodotSignalList()\n {\n"); + + source.Append(" var signals = new ") + .Append(listType) + .Append("(") + .Append(godotSignalDelegates.Count) + .Append(");\n"); + + foreach (var signalDelegateData in godotSignalDelegates) + { + var methodInfo = DetermineMethodInfo(signalDelegateData); + AppendMethodInfo(source, methodInfo); + } + + source.Append(" return signals;\n"); + source.Append(" }\n"); + + source.Append("#pragma warning restore CS0109\n"); + } + + // Generate signal event + + foreach (var signalDelegate in godotSignalDelegates) + { + string signalName = signalDelegate.Name; + + // TODO: Hide backing event from code-completion and debugger + // The reason we have a backing field is to hide the invoke method from the event, + // as it doesn't emit the signal, only the event delegates. This can confuse users. + // Maybe we should directly connect the delegates, as we do with native signals? + source.Append(" private ") + .Append(signalDelegate.DelegateSymbol.FullQualifiedName()) + .Append(" backing_") + .Append(signalName) + .Append(";\n"); + + source.Append(" public event ") + .Append(signalDelegate.DelegateSymbol.FullQualifiedName()) + .Append(" ") + .Append(signalName) + .Append(" {\n") + .Append(" add => backing_") + .Append(signalName) + .Append(" += value;\n") + .Append(" remove => backing_") + .Append(signalName) + .Append(" -= value;\n") + .Append("}\n"); + } + + // Generate RaiseGodotClassSignalCallbacks + + if (godotSignalDelegates.Count > 0) + { + source.Append( + " protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, "); + source.Append("NativeVariantPtrArgs args, int argCount)\n {\n"); + + foreach (var signal in godotSignalDelegates) + { + GenerateSignalEventInvoker(signal, source); + } + + source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args, argCount);\n"); + + source.Append(" }\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 AppendMethodInfo(StringBuilder source, MethodInfo methodInfo) + { + source.Append(" signals.Add(new(name: GodotInternal.SignalName_") + .Append(methodInfo.Name) + .Append(", returnVal: "); + + AppendPropertyInfo(source, methodInfo.ReturnVal); + + source.Append(", flags: (Godot.MethodFlags)") + .Append((int)methodInfo.Flags) + .Append(", arguments: "); + + if (methodInfo.Arguments is { Count: > 0 }) + { + source.Append("new() { "); + + foreach (var param in methodInfo.Arguments) + { + AppendPropertyInfo(source, param); + + // C# allows colon after the last element + source.Append(", "); + } + + source.Append(" }"); + } + else + { + source.Append("null"); + } + + source.Append(", defaultArguments: null));\n"); + } + + private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) + { + source.Append("new(type: (Godot.Variant.Type)") + .Append((int)propertyInfo.Type) + .Append(", name: \"") + .Append(propertyInfo.Name) + .Append("\", hint: (Godot.PropertyHint)") + .Append((int)propertyInfo.Hint) + .Append(", hintString: \"") + .Append(propertyInfo.HintString) + .Append("\", usage: (Godot.PropertyUsageFlags)") + .Append((int)propertyInfo.Usage) + .Append(", exported: ") + .Append(propertyInfo.Exported ? "true" : "false") + .Append(")"); + } + + private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDelegateData) + { + var invokeMethodData = signalDelegateData.InvokeMethodData; + + PropertyInfo returnVal; + + if (invokeMethodData.RetType != null) + { + returnVal = DeterminePropertyInfo(invokeMethodData.RetType.Value, name: string.Empty); + } + else + { + returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None, + hintString: null, PropertyUsageFlags.Default, exported: false); + } + + int paramCount = invokeMethodData.ParamTypes.Length; + + List<PropertyInfo>? arguments; + + if (paramCount > 0) + { + arguments = new(capacity: paramCount); + + for (int i = 0; i < paramCount; i++) + { + arguments.Add(DeterminePropertyInfo(invokeMethodData.ParamTypes[i], + name: invokeMethodData.Method.Parameters[i].Name)); + } + } + else + { + arguments = null; + } + + return new MethodInfo(signalDelegateData.Name, returnVal, MethodFlags.Default, arguments, + defaultArguments: null); + } + + private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, string name) + { + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + + var propUsage = PropertyUsageFlags.Default; + + if (memberVariantType == VariantType.Nil) + propUsage |= PropertyUsageFlags.NilIsVariant; + + return new PropertyInfo(memberVariantType, name, + PropertyHint.None, string.Empty, propUsage, exported: false); + } + + private static void GenerateSignalEventInvoker( + GodotSignalDelegateData signal, + StringBuilder source + ) + { + string signalName = signal.Name; + var invokeMethodData = signal.InvokeMethodData; + + source.Append(" if (signal == GodotInternal.SignalName_"); + source.Append(signalName); + source.Append(" && argCount == "); + source.Append(invokeMethodData.ParamTypes.Length); + source.Append(") {\n"); + source.Append(" backing_"); + source.Append(signalName); + source.Append("?.Invoke("); + + for (int i = 0; i < invokeMethodData.ParamTypes.Length; i++) + { + if (i != 0) + source.Append(", "); + + source.AppendNativeVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), + invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]); + } + + source.Append(");\n"); + + source.Append(" return;\n"); + + source.Append(" }\n"); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index 2bf1cb7a18..01aa65bfc3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -7,8 +7,6 @@ namespace GodotTools.BuildLogger { public class GodotBuildLogger : ILogger { - public static readonly string AssemblyPath = Path.GetFullPath(typeof(GodotBuildLogger).Assembly.Location); - public string Parameters { get; set; } public LoggerVerbosity Verbosity { get; set; } diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index 0afec970c6..9e36497b06 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -5,6 +5,6 @@ <LangVersion>7.2</LangVersion> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" /> + <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" ExcludeAssets="runtime" /> </ItemGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index d6d8962f90..cfd5c88a58 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid> - <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <LangVersion>10</LangVersion> </PropertyGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 60a4f297c9..7c5502814f 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -34,7 +34,7 @@ namespace GodotTools.Core path = path.Replace('\\', '/'); path = path[path.Length - 1] == '/' ? path.Substring(0, path.Length - 1) : path; - string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); + string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim(); @@ -60,7 +60,7 @@ namespace GodotTools.Core public static string ToSafeDirName(this string dirName, bool allowDirSeparator = false) { - var invalidChars = new List<string> {":", "*", "?", "\"", "<", ">", "|"}; + var invalidChars = new List<string> { ":", "*", "?", "\"", "<", ">", "|" }; if (allowDirSeparator) { diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj index 303ca3a293..d2132115f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid> <OutputType>Exe</OutputType> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs index bc09e1ebf9..72e2a1fc0d 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs @@ -123,12 +123,16 @@ namespace GodotTools.IdeMessaging string projectMetadataDir = Path.Combine(godotProjectDir, ".godot", "mono", "metadata"); // FileSystemWatcher requires an existing directory - if (!Directory.Exists(projectMetadataDir)) { + if (!Directory.Exists(projectMetadataDir)) + { // Check if the non hidden version exists string nonHiddenProjectMetadataDir = Path.Combine(godotProjectDir, "godot", "mono", "metadata"); - if (Directory.Exists(nonHiddenProjectMetadataDir)) { + if (Directory.Exists(nonHiddenProjectMetadataDir)) + { projectMetadataDir = nonHiddenProjectMetadataDir; - } else { + } + else + { Directory.CreateDirectory(projectMetadataDir); } } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs index 686202e81e..2448a2953b 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs @@ -25,10 +25,7 @@ namespace GodotTools.IdeMessaging public override bool Equals(object obj) { - if (obj is GodotIdeMetadata metadata) - return metadata == this; - - return false; + return obj is GodotIdeMetadata metadata && metadata == this; } public bool Equals(GodotIdeMetadata other) diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs index 10d7e1898e..dd3913b4f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs @@ -78,7 +78,7 @@ namespace GodotTools.IdeMessaging clientStream.WriteTimeout = ClientWriteTimeout; clientReader = new StreamReader(clientStream, Encoding.UTF8); - clientWriter = new StreamWriter(clientStream, Encoding.UTF8) {NewLine = "\n"}; + clientWriter = new StreamWriter(clientStream, Encoding.UTF8) { NewLine = "\n" }; } public async Task Process() diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs index 548e7f06ee..a57c82b608 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs @@ -17,7 +17,7 @@ namespace GodotTools.IdeMessaging if (content.Status == MessageStatus.Ok) SetResult(JsonConvert.DeserializeObject<T>(content.Body)); else - SetResult(new T {Status = content.Status}); + SetResult(new T { Status = content.Status }); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs index d84a63c83c..a285d5fa97 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs @@ -21,14 +21,14 @@ namespace GodotTools.IdeMessaging.Utils public void OnCompleted(Action continuation) { if (this.continuation != null) - throw new InvalidOperationException("This awaiter has already been listened"); + throw new InvalidOperationException("This awaiter already has a continuation."); this.continuation = continuation; } public void SetResult(T result) { if (IsCompleted) - throw new InvalidOperationException("This awaiter is already completed"); + throw new InvalidOperationException("This awaiter is already completed."); IsCompleted = true; this.result = result; @@ -39,7 +39,7 @@ namespace GodotTools.IdeMessaging.Utils public void SetException(Exception exception) { if (IsCompleted) - throw new InvalidOperationException("This awaiter is already completed"); + throw new InvalidOperationException("This awaiter is already completed."); IsCompleted = true; this.exception = exception; diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj index 5b3ed0b1b7..c05096bdcc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid> <OutputType>Exe</OutputType> diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs index 7a4641dbbc..cc0deb6cc0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -249,8 +249,8 @@ namespace GodotTools.OpenVisualStudio // Thread call was rejected, so try again. int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) { + // flag = SERVERCALL_RETRYLATER if (dwRejectType == 2) - // flag = SERVERCALL_RETRYLATER { // Retry the thread call immediately if return >= 0 & < 100 return 99; diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 37123ba2b2..bde14b2b40 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -1,32 +1,16 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid> - <TargetFramework>net472</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <LangVersion>10</LangVersion> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Build" Version="16.5.0" /> + <PackageReference Include="Microsoft.Build" Version="15.1.548" ExcludeAssets="runtime" /> + <PackageReference Include="Microsoft.Build.Locator" Version="1.2.6" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> <ProjectReference Include="..\GodotTools.Shared\GodotTools.Shared.csproj" /> </ItemGroup> - <!-- - The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described - here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 - We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when - searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. - --> - <ItemGroup> - <None Include="MSBuild.exe" CopyToOutputDirectory="Always" /> - </ItemGroup> - <Target Name="CopyMSBuildStubWindows" AfterTargets="Build" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) "> - <PropertyGroup> - <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> - <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> - </PropertyGroup> - <!-- Need to copy it here as well on Windows --> - <Copy SourceFiles="MSBuild.exe" DestinationFiles="$(GodotOutputDataDir)\Mono\lib\mono\v4.0\MSBuild.exe" /> - </Target> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe deleted file mode 100644 index e69de29bb2..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe +++ /dev/null diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index 7d49d251dd..f3c8e89dff 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Text; using Microsoft.Build.Construction; @@ -14,14 +15,15 @@ namespace GodotTools.ProjectEditor public static ProjectRootElement GenGameProject(string name) { if (name.Length == 0) - throw new ArgumentException("Project name is empty", nameof(name)); + throw new ArgumentException("Project name is empty.", nameof(name)); var root = ProjectRootElement.Create(NewProjectFileOptions.None); root.Sdk = GodotSdkAttrValue; var mainGroup = root.AddPropertyGroup(); - mainGroup.AddProperty("TargetFramework", "netstandard2.1"); + mainGroup.AddProperty("TargetFramework", "net6.0"); + mainGroup.AddProperty("EnableDynamicLoading", "true"); string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); @@ -35,7 +37,7 @@ namespace GodotTools.ProjectEditor public static string GenAndSaveGameProject(string dir, string name) { if (name.Length == 0) - throw new ArgumentException("Project name is empty", nameof(name)); + throw new ArgumentException("Project name is empty.", nameof(name)); string path = Path.Combine(dir, name + ".csproj"); @@ -44,7 +46,7 @@ namespace GodotTools.ProjectEditor // Save (without BOM) root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - return Guid.NewGuid().ToString().ToUpper(); + return Guid.NewGuid().ToString().ToUpperInvariant(); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index cdac9acb25..7b1d5c228a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -19,6 +19,16 @@ namespace GodotTools.ProjectEditor public static class ProjectUtils { + public static void MSBuildLocatorRegisterDefaults(out Version version, out string path) + { + var instance = Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + version = instance.Version; + path = instance.MSBuildPath; + } + + public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath) + => Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath(msbuildPath); + public static MSBuildProject Open(string path) { var root = ProjectRootElement.Open(path); @@ -42,7 +52,8 @@ namespace GodotTools.ProjectEditor var root = project.Root; string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; - if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(root.Sdk) && + root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) return; root.Sdk = godotSdkAttrValue; diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets b/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets index aab2d73bdd..4baae77b34 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GenerateGodotNupkgsVersions.targets @@ -8,8 +8,8 @@ </Target> <Target Name="GenerateGodotNupkgsVersionsFile" - DependsOnTargets="PrepareForBuild;_GenerateGodotNupkgsVersionsFile" - BeforeTargets="BeforeCompile;CoreCompile"> + DependsOnTargets="_GenerateGodotNupkgsVersionsFile" + BeforeTargets="PrepareForBuild;CompileDesignTime;BeforeCompile;CoreCompile"> <ItemGroup> <Compile Include="$(GeneratedGodotNupkgsVersionsFile)" /> <FileWrites Include="$(GeneratedGodotNupkgsVersionsFile)" /> diff --git a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj index 3bc1698c15..d60e6343ea 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Shared/GodotTools.Shared.csproj @@ -1,6 +1,8 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> + <!-- Specify compile items manually to avoid including dangling generated items. --> + <EnableDefaultCompileItems>false</EnableDefaultCompileItems> </PropertyGroup> <Import Project="GenerateGodotNupkgsVersions.targets" /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index d3107a69db..564775635d 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.ProjectEditor", "GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj", "{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}" @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Shared", "GodotTools.Shared\GodotTools.Shared.csproj", "{2758FFAF-8237-4CF2-B569-66BF8B3587BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{D8C421B2-8911-41EB-B983-F675C7141EB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Internal", "..\..\glue\GodotSharp\Godot.SourceGenerators.Internal\Godot.SourceGenerators.Internal.csproj", "{55666071-BEC1-4A52-8A98-9A4A7A947DBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,5 +53,13 @@ Global {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2758FFAF-8237-4CF2-B569-66BF8B3587BB}.Release|Any CPU.Build.0 = Release|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8C421B2-8911-41EB-B983-F675C7141EB7}.Release|Any CPU.Build.0 = Release|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55666071-BEC1-4A52-8A98-9A4A7A947DBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index 28bf57dc21..edbf53a389 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -4,28 +4,36 @@ using Godot.Collections; using GodotTools.Internals; using Path = System.IO.Path; +#nullable enable + namespace GodotTools.Build { [Serializable] - public sealed class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization + public sealed partial class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization { - public string Solution { get; } - public string[] Targets { get; } - public string Configuration { get; } - public bool Restore { get; } + public string Solution { get; private set; } + public string Configuration { get; private set; } + public string? RuntimeIdentifier { get; private set; } + public string? PublishOutputDir { get; private set; } + public bool Restore { get; private set; } + public bool Rebuild { get; private set; } + public bool OnlyClean { get; private set; } + // TODO Use List once we have proper serialization - public Array<string> CustomProperties { get; } = new Array<string>(); + public Godot.Collections.Array CustomProperties { get; private set; } = new(); - public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); + public string LogsDirPath => + Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (obj is BuildInfo other) - return other.Solution == Solution && other.Targets == Targets && - other.Configuration == Configuration && other.Restore == Restore && - other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath; - - return false; + return obj is BuildInfo other && + other.Solution == Solution && + other.Configuration == Configuration && other.RuntimeIdentifier == RuntimeIdentifier && + other.PublishOutputDir == PublishOutputDir && other.Restore == Restore && + other.Rebuild == Rebuild && other.OnlyClean == OnlyClean && + other.CustomProperties == CustomProperties && + other.LogsDirPath == LogsDirPath; } public override int GetHashCode() @@ -34,25 +42,44 @@ namespace GodotTools.Build { int hash = 17; hash = (hash * 29) + Solution.GetHashCode(); - hash = (hash * 29) + Targets.GetHashCode(); hash = (hash * 29) + Configuration.GetHashCode(); + hash = (hash * 29) + (RuntimeIdentifier?.GetHashCode() ?? 0); + hash = (hash * 29) + (PublishOutputDir?.GetHashCode() ?? 0); hash = (hash * 29) + Restore.GetHashCode(); + hash = (hash * 29) + Rebuild.GetHashCode(); + hash = (hash * 29) + OnlyClean.GetHashCode(); hash = (hash * 29) + CustomProperties.GetHashCode(); hash = (hash * 29) + LogsDirPath.GetHashCode(); return hash; } } + // Needed for instantiation from Godot, after reloading assemblies private BuildInfo() { + Solution = string.Empty; + Configuration = string.Empty; + } + + public BuildInfo(string solution, string configuration, bool restore, bool rebuild, bool onlyClean) + { + Solution = solution; + Configuration = configuration; + Restore = restore; + Rebuild = rebuild; + OnlyClean = onlyClean; } - public BuildInfo(string solution, string[] targets, string configuration, bool restore) + public BuildInfo(string solution, string configuration, string runtimeIdentifier, + string publishOutputDir, bool restore, bool rebuild, bool onlyClean) { Solution = solution; - Targets = targets; Configuration = configuration; + RuntimeIdentifier = runtimeIdentifier; + PublishOutputDir = publishOutputDir; Restore = restore; + Rebuild = rebuild; + OnlyClean = onlyClean; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index 21bff70b15..993c6d9217 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -1,12 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; -using GodotTools.Ides.Rider; +using Godot; using GodotTools.Internals; -using JetBrains.Annotations; -using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; -using OS = GodotTools.Utils.OS; namespace GodotTools.Build { @@ -14,13 +12,8 @@ namespace GodotTools.Build { private static BuildInfo _buildInProgress; - public const string PropNameMSBuildMono = "MSBuild (Mono)"; - public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)"; - public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)"; - public const string PropNameDotnetCli = "dotnet CLI"; - public const string MsBuildIssuesFileName = "msbuild_issues.csv"; - public const string MsBuildLogFileName = "msbuild_log.txt"; + private const string MsBuildLogFileName = "msbuild_log.txt"; public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason); @@ -62,14 +55,14 @@ namespace GodotTools.Build private static void PrintVerbose(string text) { - if (Godot.OS.IsStdoutVerbose()) - Godot.GD.Print(text); + if (OS.IsStdoutVerbose()) + GD.Print(text); } - public static bool Build(BuildInfo buildInfo) + private static bool Build(BuildInfo buildInfo) { if (_buildInProgress != null) - throw new InvalidOperationException("A build is already in progress"); + throw new InvalidOperationException("A build is already in progress."); _buildInProgress = buildInfo; @@ -103,7 +96,8 @@ namespace GodotTools.Build } catch (Exception e) { - BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + BuildLaunchFailed?.Invoke(buildInfo, + $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); Console.Error.WriteLine(e); return false; } @@ -117,7 +111,7 @@ namespace GodotTools.Build public static async Task<bool> BuildAsync(BuildInfo buildInfo) { if (_buildInProgress != null) - throw new InvalidOperationException("A build is already in progress"); + throw new InvalidOperationException("A build is already in progress."); _buildInProgress = buildInfo; @@ -148,7 +142,8 @@ namespace GodotTools.Build } catch (Exception e) { - BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + BuildLaunchFailed?.Invoke(buildInfo, + $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); Console.Error.WriteLine(e); return false; } @@ -159,18 +154,54 @@ namespace GodotTools.Build } } - public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null) + private static bool Publish(BuildInfo buildInfo) { - var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true); + if (_buildInProgress != null) + throw new InvalidOperationException("A build is already in progress."); - // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. - if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform)) - buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + _buildInProgress = buildInfo; - if (Internal.GodotIsRealTDouble()) - buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + try + { + BuildStarted?.Invoke(buildInfo); + + // Required in order to update the build tasks list + Internal.GodotMainIteration(); - return BuildProjectBlocking(buildInfo); + try + { + RemoveOldIssuesFile(buildInfo); + } + catch (IOException e) + { + BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + Console.Error.WriteLine(e); + } + + try + { + int exitCode = BuildSystem.Publish(buildInfo, StdOutputReceived, StdErrorReceived); + + if (exitCode != 0) + PrintVerbose( + $"dotnet publish exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); + + BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error); + + return exitCode == 0; + } + catch (Exception e) + { + BuildLaunchFailed?.Invoke(buildInfo, + $"The publish method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + Console.Error.WriteLine(e); + return false; + } + } + finally + { + _buildInProgress = null; + } } private static bool BuildProjectBlocking(BuildInfo buildInfo) @@ -178,31 +209,109 @@ namespace GodotTools.Build if (!File.Exists(buildInfo.Solution)) return true; // No solution to build - // Make sure the API assemblies are up to date before building the project. - // We may not have had the chance to update the release API assemblies, and the debug ones - // may have been deleted by the user at some point after they were loaded by the Godot editor. - string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug"); + using var pr = new EditorProgress("dotnet_build_project", "Building .NET project...", 1); + + pr.Step("Building project solution", 0); - if (!string.IsNullOrEmpty(apiAssembliesUpdateError)) + if (!Build(buildInfo)) { - ShowBuildErrorDialog("Failed to update the Godot API assemblies"); + ShowBuildErrorDialog("Failed to build project solution"); return false; } - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) + return true; + } + + private static bool CleanProjectBlocking(BuildInfo buildInfo) + { + if (!File.Exists(buildInfo.Solution)) + return true; // No solution to clean + + using var pr = new EditorProgress("dotnet_clean_project", "Cleaning .NET project...", 1); + + pr.Step("Cleaning project solution", 0); + + if (!Build(buildInfo)) { - pr.Step("Building project solution", 0); + ShowBuildErrorDialog("Failed to clean project solution"); + return false; + } - if (!Build(buildInfo)) - { - ShowBuildErrorDialog("Failed to build project solution"); - return false; - } + return true; + } + + private static bool PublishProjectBlocking(BuildInfo buildInfo) + { + using var pr = new EditorProgress("dotnet_publish_project", "Publishing .NET project...", 1); + + pr.Step("Running dotnet publish", 0); + + if (!Publish(buildInfo)) + { + ShowBuildErrorDialog("Failed to publish .NET project"); + return false; } return true; } + private static BuildInfo CreateBuildInfo( + [DisallowNull] string configuration, + [AllowNull] string platform = null, + bool rebuild = false, + bool onlyClean = false + ) + { + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, configuration, + restore: true, rebuild, onlyClean); + + // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. + if (platform != null || Utils.OS.PlatformNameMap.TryGetValue(OS.GetName(), out platform)) + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return buildInfo; + } + + private static BuildInfo CreatePublishBuildInfo( + [DisallowNull] string configuration, + [DisallowNull] string platform, + [DisallowNull] string runtimeIdentifier, + [DisallowNull] string publishOutputDir + ) + { + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, configuration, + runtimeIdentifier, publishOutputDir, restore: true, rebuild: false, onlyClean: false); + + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); + + if (Internal.GodotIsRealTDouble()) + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); + + return buildInfo; + } + + public static bool BuildProjectBlocking( + [DisallowNull] string configuration, + [AllowNull] string platform = null, + bool rebuild = false + ) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild)); + + public static bool CleanProjectBlocking( + [DisallowNull] string configuration, + [AllowNull] string platform = null + ) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false)); + + public static bool PublishProjectBlocking( + [DisallowNull] string configuration, + [DisallowNull] string platform, + [DisallowNull] string runtimeIdentifier, + string publishOutputDir + ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration, + platform, runtimeIdentifier, publishOutputDir)); + public static bool EditorBuildCallback() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -215,7 +324,7 @@ namespace GodotTools.Build } catch (Exception e) { - Godot.GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); + GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); } if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) @@ -226,47 +335,6 @@ namespace GodotTools.Build public static void Initialize() { - // Build tool settings - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - - BuildTool msbuildDefault; - - if (OS.IsWindows) - { - if (RiderPathManager.IsExternalEditorSetToRider(editorSettings)) - msbuildDefault = BuildTool.JetBrainsMsBuild; - else - msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs; - } - else - { - msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono; - } - - EditorDef("mono/builds/build_tool", msbuildDefault); - - string hintString; - - if (OS.IsWindows) - { - hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + - $"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," + - $"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," + - $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; - } - else - { - hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + - $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; - } - - editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Godot.Variant.Type.Int, - ["name"] = "mono/builds/build_tool", - ["hint"] = Godot.PropertyHint.Enum, - ["hint_string"] = hintString - }); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs index ebdaca0ce8..180cc3cf14 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -1,17 +1,16 @@ using Godot; using System; -using Godot.Collections; +using System.Diagnostics.CodeAnalysis; using GodotTools.Internals; -using JetBrains.Annotations; using File = GodotTools.Utils.File; using Path = System.IO.Path; namespace GodotTools.Build { - public class BuildOutputView : VBoxContainer, ISerializationListener + public partial class BuildOutputView : VBoxContainer, ISerializationListener { [Serializable] - private class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization + private partial class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization { public bool Warning { get; set; } public string File { get; set; } @@ -22,7 +21,8 @@ namespace GodotTools.Build public string ProjectFile { get; set; } } - [Signal] public event Action BuildStateChanged; + [Signal] + public delegate void BuildStateChangedEventHandler(); public bool HasBuildExited { get; private set; } = false; @@ -58,7 +58,7 @@ namespace GodotTools.Build } // TODO Use List once we have proper serialization. - private readonly Array<BuildIssue> _issues = new Array<BuildIssue>(); + private Godot.Collections.Array<BuildIssue> _issues = new(); private ItemList _issuesList; private PopupMenu _issuesListContextMenu; private TextEdit _buildLog; @@ -120,20 +120,22 @@ namespace GodotTools.Build private void IssueActivated(int idx) { if (idx < 0 || idx >= _issuesList.ItemCount) - throw new IndexOutOfRangeException("Item list index out of range"); + throw new ArgumentOutOfRangeException(nameof(idx), "Item list index out of range."); // Get correct issue idx from issue list - int issueIndex = (int)(long)_issuesList.GetItemMetadata(idx); + int issueIndex = (int)_issuesList.GetItemMetadata(idx); if (issueIndex < 0 || issueIndex >= _issues.Count) - throw new IndexOutOfRangeException("Issue index out of range"); + throw new InvalidOperationException("Issue index out of range."); BuildIssue issue = _issues[issueIndex]; if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File)) return; - string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : _buildInfo.Solution.GetBaseDir(); + string projectDir = !string.IsNullOrEmpty(issue.ProjectFile) ? + issue.ProjectFile.GetBaseDir() : + _buildInfo.Solution.GetBaseDir(); string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); @@ -291,7 +293,7 @@ namespace GodotTools.Build public void RestartBuild() { if (!HasBuildExited) - throw new InvalidOperationException("Build already started"); + throw new InvalidOperationException("Build already started."); BuildManager.RestartBuild(this); } @@ -299,7 +301,7 @@ namespace GodotTools.Build public void StopBuild() { if (!HasBuildExited) - throw new InvalidOperationException("Build is not in progress"); + throw new InvalidOperationException("Build is not in progress."); BuildManager.StopBuild(this); } @@ -412,6 +414,16 @@ namespace GodotTools.Build { // In case it didn't update yet. We don't want to have to serialize any pending output. UpdateBuildLogText(); + + // NOTE: + // Currently, GodotTools is loaded in its own load context. This load context is not reloaded, but the script still are. + // Until that changes, we need workarounds like this one because events keep strong references to disposed objects. + BuildManager.BuildLaunchFailed -= BuildLaunchFailed; + BuildManager.BuildStarted -= BuildStarted; + BuildManager.BuildFinished -= BuildFinished; + // StdOutput/Error can be received from different threads, so we need to use CallDeferred + BuildManager.StdOutputReceived -= StdOutputReceived; + BuildManager.StdErrorReceived -= StdErrorReceived; } public void OnAfterDeserialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 02e9d98647..d0cd529d1f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -1,61 +1,93 @@ -using GodotTools.Core; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Text; using System.Threading.Tasks; using GodotTools.BuildLogger; -using GodotTools.Internals; using GodotTools.Utils; -using Directory = System.IO.Directory; namespace GodotTools.Build { public static class BuildSystem { - private static string MonoWindowsBinDir + private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - get - { - string monoWinBinDir = Path.Combine(Internal.MonoWindowsInstallRoot, "bin"); + string dotnetPath = DotNetFinder.FindDotNetExe(); - if (!Directory.Exists(monoWinBinDir)) - throw new FileNotFoundException("Cannot find the Windows Mono install bin directory."); + if (dotnetPath == null) + throw new FileNotFoundException("Cannot find the dotnet executable."); - return monoWinBinDir; - } + var startInfo = new ProcessStartInfo(dotnetPath); + + BuildArguments(buildInfo, startInfo.ArgumentList); + + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); + stdOutHandler?.Invoke(launchMessage); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine(launchMessage); + + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + + // Needed when running from Developer Command Prompt for VS + RemovePlatformVariable(startInfo.EnvironmentVariables); + + var process = new Process { StartInfo = startInfo }; + + if (stdOutHandler != null) + process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data); + if (stdErrHandler != null) + process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data); + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + return process; } - private static Godot.EditorSettings EditorSettings => - GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + { + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + { + process.WaitForExit(); + + return process.ExitCode; + } + } - private static bool UsingMonoMsBuildOnWindows + public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - get + using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { - if (OS.IsWindows) - { - return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") - == BuildTool.MsBuildMono; - } + await process.WaitForExitAsync(); - return false; + return process.ExitCode; } } - private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + private static Process LaunchPublish(BuildInfo buildInfo, Action<string> stdOutHandler, + Action<string> stdErrHandler) { - (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); + string dotnetPath = DotNetFinder.FindDotNetExe(); - if (msbuildPath == null) - throw new FileNotFoundException("Cannot find the MSBuild executable."); + if (dotnetPath == null) + throw new FileNotFoundException("Cannot find the dotnet executable."); - string compilerArgs = BuildArguments(buildTool, buildInfo); + var startInfo = new ProcessStartInfo(dotnetPath); - var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); + BuildPublishArguments(buildInfo, startInfo.ArgumentList); - string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"; + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); stdOutHandler?.Invoke(launchMessage); if (Godot.OS.IsStdoutVerbose()) Console.WriteLine(launchMessage); @@ -63,27 +95,16 @@ namespace GodotTools.Build startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - - if (UsingMonoMsBuildOnWindows) - { - // These environment variables are required for Mono's MSBuild to find the compilers. - // We use the batch files in Mono's bin directory to make sure the compilers are executed with mono. - string monoWinBinDir = MonoWindowsBinDir; - startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat")); - startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat")); - startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat")); - } // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); - var process = new Process {StartInfo = startInfo}; + var process = new Process { StartInfo = startInfo }; if (stdOutHandler != null) - process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data); + process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data); if (stdErrHandler != null) - process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data); + process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data); process.Start(); @@ -93,9 +114,9 @@ namespace GodotTools.Build return process; } - public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + public static int Publish(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) { - using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler)) { process.WaitForExit(); @@ -103,38 +124,102 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + private static void BuildArguments(BuildInfo buildInfo, Collection<string> arguments) { - using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) + // `dotnet clean` / `dotnet build` commands + arguments.Add(buildInfo.OnlyClean ? "clean" : "build"); + + // Solution + arguments.Add(buildInfo.Solution); + + // `dotnet clean` doesn't recognize these options + if (!buildInfo.OnlyClean) { - await process.WaitForExitAsync(); + // Restore + // `dotnet build` restores by default, unless requested not to + if (!buildInfo.Restore) + arguments.Add("--no-restore"); + + // Incremental or rebuild + if (buildInfo.Rebuild) + arguments.Add("--no-incremental"); + } - return process.ExitCode; + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); + + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Custom properties + foreach (var customProperty in buildInfo.CustomProperties) + { + arguments.Add("-p:" + (string)customProperty); } } - private static string BuildArguments(BuildTool buildTool, BuildInfo buildInfo) + private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string> arguments) { - string arguments = string.Empty; + arguments.Add("publish"); // `dotnet publish` command + + // Solution + arguments.Add(buildInfo.Solution); + + // Restore + // `dotnet publish` restores by default, unless requested not to + if (!buildInfo.Restore) + arguments.Add("--no-restore"); - if (buildTool == BuildTool.DotnetCli) - arguments += "msbuild"; // `dotnet msbuild` command + // Incremental or rebuild + // TODO: Not supported in `dotnet publish` (https://github.com/dotnet/sdk/issues/11099) + // if (buildInfo.Rebuild) + // arguments.Add("--no-incremental"); - arguments += $@" ""{buildInfo.Solution}"""; + // Configuration + arguments.Add("-c"); + arguments.Add(buildInfo.Configuration); - if (buildInfo.Restore) - arguments += " /restore"; + // Runtime Identifier + arguments.Add("-r"); + arguments.Add(buildInfo.RuntimeIdentifier!); - arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " + - $@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " + - $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{buildInfo.LogsDirPath}"""; + // Self-published + arguments.Add("--self-contained"); + arguments.Add("true"); - foreach (string customProperty in buildInfo.CustomProperties) + // Verbosity + arguments.Add("-v"); + arguments.Add("normal"); + + // Logger + AddLoggerArgument(buildInfo, arguments); + + // Custom properties + foreach (var customProperty in buildInfo.CustomProperties) + { + arguments.Add("-p:" + (string)customProperty); + } + + // Publish output directory + if (buildInfo.PublishOutputDir != null) { - arguments += " /p:" + customProperty; + arguments.Add("-o"); + arguments.Add(buildInfo.PublishOutputDir); } + } + + private static void AddLoggerArgument(BuildInfo buildInfo, Collection<string> arguments) + { + string buildLoggerPath = Path.Combine(Internals.GodotSharpDirs.DataEditorToolsDir, + "GodotTools.BuildLogger.dll"); - return arguments; + arguments.Add( + $"-l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"); } private static void RemovePlatformVariable(StringDictionary environmentVariables) @@ -145,7 +230,7 @@ namespace GodotTools.Build foreach (string env in environmentVariables.Keys) { - if (env.ToUpper() == "PLATFORM") + if (env.ToUpperInvariant() == "PLATFORM") platformEnvironmentVariables.Add(env); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs deleted file mode 100644 index 837c8adddb..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GodotTools.Build -{ - public enum BuildTool : long - { - MsBuildMono, - MsBuildVs, - JetBrainsMsBuild, - DotnetCli - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs new file mode 100644 index 0000000000..7bce53308c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using JetBrains.Annotations; +using OS = GodotTools.Utils.OS; + +namespace GodotTools.Build +{ + public static class DotNetFinder + { + [CanBeNull] + public static string FindDotNetExe() + { + // In the future, this method may do more than just search in PATH. We could look in + // known locations or use Godot's linked nethost to search from the hostfxr location. + + return OS.PathWhich("dotnet"); + } + + public static bool TryFindDotNetSdk( + Version expectedVersion, + [NotNullWhen(true)] out Version version, + [NotNullWhen(true)] out string path + ) + { + version = null; + path = null; + + string dotNetExe = FindDotNetExe(); + + if (string.IsNullOrEmpty(dotNetExe)) + return false; + + using Process process = new Process(); + process.StartInfo = new ProcessStartInfo(dotNetExe, "--list-sdks") + { + UseShellExecute = false, + RedirectStandardOutput = true + }; + + process.StartInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en-US"; + + var lines = new List<string>(); + + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrWhiteSpace(e.Data)) + lines.Add(e.Data); + }; + + try + { + process.Start(); + } + catch + { + return false; + } + + process.BeginOutputReadLine(); + process.WaitForExit(); + + Version latestVersionMatch = null; + string matchPath = null; + + foreach (var line in lines) + { + string[] sdkLineParts = line.Trim() + .Split(' ', 2, StringSplitOptions.TrimEntries); + + if (sdkLineParts.Length < 2) + continue; + + if (!Version.TryParse(sdkLineParts[0], out var lineVersion)) + continue; + + // We're looking for the exact same major version + if (lineVersion.Major != expectedVersion.Major) + continue; + + if (latestVersionMatch != null && lineVersion < latestVersionMatch) + continue; + + latestVersionMatch = lineVersion; + matchPath = sdkLineParts[1].TrimStart('[').TrimEnd(']'); + } + + if (latestVersionMatch == null) + return false; + + version = latestVersionMatch; + path = Path.Combine(matchPath!, version.ToString()); + + return true; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs index 3c020a2589..d05995c495 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -1,13 +1,12 @@ using System; using Godot; using GodotTools.Internals; -using JetBrains.Annotations; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; namespace GodotTools.Build { - public class MSBuildPanel : VBoxContainer + public partial class MSBuildPanel : VBoxContainer { public BuildOutputView BuildOutputView { get; private set; } @@ -28,7 +27,6 @@ namespace GodotTools.Build BuildOutputView.UpdateIssuesList(); } - [UsedImplicitly] public void BuildSolution() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -57,7 +55,6 @@ namespace GodotTools.Build Internal.ReloadAssemblies(softReload: false); } - [UsedImplicitly] private void RebuildSolution() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -73,7 +70,7 @@ namespace GodotTools.Build GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message); } - if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Rebuild" })) + if (!BuildManager.BuildProjectBlocking("Debug", rebuild: true)) return; // Build failed // Notify running game for hot-reload @@ -86,13 +83,12 @@ namespace GodotTools.Build Internal.ReloadAssemblies(softReload: false); } - [UsedImplicitly] private void CleanSolution() { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; // No solution to build - BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Clean" }); + _ = BuildManager.CleanProjectBlocking("Debug"); } private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed; @@ -143,7 +139,7 @@ namespace GodotTools.Build _errorsBtn = new Button { - HintTooltip = "Show Errors".TTR(), + TooltipText = "Show Errors".TTR(), Icon = GetThemeIcon("StatusError", "EditorIcons"), ExpandIcon = false, ToggleMode = true, @@ -155,7 +151,7 @@ namespace GodotTools.Build _warningsBtn = new Button { - HintTooltip = "Show Warnings".TTR(), + TooltipText = "Show Warnings".TTR(), Icon = GetThemeIcon("NodeWarning", "EditorIcons"), ExpandIcon = false, ToggleMode = true, diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs deleted file mode 100644 index a859c6f717..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Godot; -using GodotTools.Ides.Rider; -using GodotTools.Internals; -using Directory = System.IO.Directory; -using Environment = System.Environment; -using File = System.IO.File; -using Path = System.IO.Path; -using OS = GodotTools.Utils.OS; - -namespace GodotTools.Build -{ - public static class MsBuildFinder - { - private static string _msbuildToolsPath = string.Empty; - private static string _msbuildUnixPath = string.Empty; - - public static (string, BuildTool) FindMsBuild() - { - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - - if (OS.IsWindows) - { - switch (buildTool) - { - case BuildTool.DotnetCli: - { - string dotnetCliPath = OS.PathWhich("dotnet"); - if (!string.IsNullOrEmpty(dotnetCliPath)) - return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio."); - goto case BuildTool.MsBuildVs; - } - case BuildTool.MsBuildVs: - { - if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildToolsPath = FindMsBuildToolsPathOnWindows(); - - if (string.IsNullOrEmpty(_msbuildToolsPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'."); - } - - if (!_msbuildToolsPath.EndsWith("\\")) - _msbuildToolsPath += "\\"; - - return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs); - } - case BuildTool.MsBuildMono: - { - string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); - - if (!File.Exists(msbuildPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}"); - - return (msbuildPath, BuildTool.MsBuildMono); - } - case BuildTool.JetBrainsMsBuild: - { - string editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName); - - if (!File.Exists(editorPath)) - throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}"); - - var riderDir = new FileInfo(editorPath).Directory?.Parent; - - string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe"); - - if (!File.Exists(msbuildPath)) - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}"); - - return (msbuildPath, BuildTool.JetBrainsMsBuild); - } - default: - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); - } - } - - if (OS.IsUnixLike) - { - switch (buildTool) - { - case BuildTool.DotnetCli: - { - string dotnetCliPath = FindBuildEngineOnUnix("dotnet"); - if (!string.IsNullOrEmpty(dotnetCliPath)) - return (dotnetCliPath, BuildTool.DotnetCli); - GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono."); - goto case BuildTool.MsBuildMono; - } - case BuildTool.MsBuildMono: - { - if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildUnixPath = FindBuildEngineOnUnix("msbuild"); - } - - if (string.IsNullOrEmpty(_msbuildUnixPath)) - throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'"); - - return (_msbuildUnixPath, BuildTool.MsBuildMono); - } - default: - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); - } - } - - throw new PlatformNotSupportedException(); - } - - private static IEnumerable<string> MsBuildHintDirs - { - get - { - var result = new List<string>(); - - if (OS.IsMacOS) - { - result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/"); - result.Add("/opt/local/bin/"); - result.Add("/usr/local/var/homebrew/linked/mono/bin/"); - result.Add("/usr/local/bin/"); - result.Add("/usr/local/bin/dotnet/"); - result.Add("/usr/local/share/dotnet/"); - } - - result.Add("/opt/novell/mono/bin/"); - - return result; - } - } - - private static string FindBuildEngineOnUnix(string name) - { - string ret = OS.PathWhich(name); - - if (!string.IsNullOrEmpty(ret)) - return ret; - - string retFallback = OS.PathWhich($"{name}.exe"); - - if (!string.IsNullOrEmpty(retFallback)) - return retFallback; - - foreach (string hintDir in MsBuildHintDirs) - { - string hintPath = Path.Combine(hintDir, name); - - if (File.Exists(hintPath)) - return hintPath; - } - - return string.Empty; - } - - private static string FindMsBuildToolsPathOnWindows() - { - if (!OS.IsWindows) - throw new PlatformNotSupportedException(); - - // Try to find 15.0 with vswhere - - string[] envNames = Internal.GodotIs32Bits() ? - envNames = new[] { "ProgramFiles", "ProgramW6432" } : - envNames = new[] { "ProgramFiles(x86)", "ProgramFiles" }; - - string vsWherePath = null; - foreach (var envName in envNames) - { - vsWherePath = Environment.GetEnvironmentVariable(envName); - if (!string.IsNullOrEmpty(vsWherePath)) - { - vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe"; - if (File.Exists(vsWherePath)) - break; - } - - vsWherePath = null; - } - - var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"}; - - var outputArray = new Godot.Collections.Array<string>(); - int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs, - output: (Godot.Collections.Array)outputArray); - - if (exitCode != 0) - return string.Empty; - - if (outputArray.Count == 0) - return string.Empty; - - var lines = outputArray[0].Split('\n'); - - foreach (string line in lines) - { - int sepIdx = line.IndexOf(':'); - - if (sepIdx <= 0) - continue; - - string key = line.Substring(0, sepIdx); // No need to trim - - if (key != "installationPath") - continue; - - string value = line.Substring(sepIdx + 1).StripEdges(); - - if (string.IsNullOrEmpty(value)) - throw new FormatException("installationPath value is empty"); - - if (!value.EndsWith("\\")) - value += "\\"; - - // Since VS2019, the directory is simply named "Current" - string msbuildDir = Path.Combine(value, "MSBuild\\Current\\Bin"); - - if (Directory.Exists(msbuildDir)) - return msbuildDir; - - // Directory name "15.0" is used in VS 2017 - return Path.Combine(value, "MSBuild\\15.0\\Bin"); - } - - return string.Empty; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs index 63b97e981e..d2e0e128b5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/NuGetUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; @@ -39,7 +40,8 @@ namespace GodotTools.Build // Since this can be considered pretty much a new NuGet.Config, add the default nuget.org source as well XmlElement nugetOrgSourceEntry = xmlDoc.CreateElement("add"); nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("key")).Value = "nuget.org"; - nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = "https://api.nuget.org/v3/index.json"; + nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = + "https://api.nuget.org/v3/index.json"; nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("protocolVersion")).Value = "3"; rootNode.AppendChild(xmlDoc.CreateElement("packageSources")).AppendChild(nugetOrgSourceEntry); } @@ -47,7 +49,7 @@ namespace GodotTools.Build { // Check that the root node is the expected one if (rootNode.Name != nuGetConfigRootName) - throw new Exception("Invalid root Xml node for NuGet.Config. " + + throw new FormatException("Invalid root Xml node for NuGet.Config. " + $"Expected '{nuGetConfigRootName}' got '{rootNode.Name}'."); } @@ -181,8 +183,8 @@ namespace GodotTools.Build // - The sha512 of the nupkg is base64 encoded. // - We can get the nuspec from the nupkg which is a Zip file. - string packageIdLower = packageId.ToLower(); - string packageVersionLower = packageVersion.ToLower(); + string packageIdLower = packageId.ToLowerInvariant(); + string packageVersionLower = packageVersion.ToLowerInvariant(); string destDir = Path.Combine(fallbackFolder, packageIdLower, packageVersionLower); string nupkgDestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg"); @@ -227,9 +229,11 @@ namespace GodotTools.Build var nuspecEntry = archive.GetEntry(packageId + ".nuspec"); if (nuspecEntry == null) - throw new InvalidOperationException($"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); + throw new InvalidOperationException( + $"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file."); - nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name.ToLower().SimplifyGodotPath())); + nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name + .ToLowerInvariant().SimplifyGodotPath())); // Extract the other package files diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs index e9718cc82c..2184cae6d6 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -75,8 +75,24 @@ namespace GodotTools.Export } else { - string bits = features.Contains("64") ? "64" : features.Contains("32") ? "32" : null; - CompileAssembliesForDesktop(exporter, platform, isDebug, bits, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir); + string arch = ""; + if (features.Contains("x86_64")) + { + arch = "x86_64"; + } + else if (features.Contains("x86_32")) + { + arch = "x86_32"; + } + else if (features.Contains("arm64")) + { + arch = "arm64"; + } + else if (features.Contains("arm32")) + { + arch = "arm32"; + } + CompileAssembliesForDesktop(exporter, platform, isDebug, arch, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir); } } @@ -112,7 +128,7 @@ namespace GodotTools.Export } } - public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string bits, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir) + public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string arch, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir) { foreach (var assembly in assemblies) { @@ -126,9 +142,9 @@ namespace GodotTools.Export string outputFileName = assemblyName + ".dll" + outputFileExtension; string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); - var compilerArgs = GetAotCompilerArgs(platform, isDebug, bits, aotOpts, assemblyPath, tempOutputFilePath); + var compilerArgs = GetAotCompilerArgs(platform, isDebug, arch, aotOpts, assemblyPath, tempOutputFilePath); - string compilerDirPath = GetMonoCrossDesktopDirName(platform, bits); + string compilerDirPath = GetMonoCrossDesktopDirName(platform, arch); ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); @@ -203,7 +219,7 @@ namespace GodotTools.Export int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs); if (clangExitCode != 0) - throw new Exception($"Command 'clang' exited with code: {clangExitCode}"); + throw new InvalidOperationException($"Command 'clang' exited with code: {clangExitCode}."); objFilePathsForiOSArch[arch].Add(objFilePath); } @@ -289,7 +305,7 @@ MONO_AOT_MODE_LAST = 1000, // Archive the AOT object files into a static library var arFilePathsForAllArchs = new List<string>(); - string projectAssemblyName = GodotSharpEditor.ProjectAssemblyName; + string projectAssemblyName = GodotSharpDirs.ProjectAssemblyName; foreach (var archPathsPair in objFilePathsForiOSArch) { @@ -309,7 +325,7 @@ MONO_AOT_MODE_LAST = 1000, int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs); if (arExitCode != 0) - throw new Exception($"Command 'ar' exited with code: {arExitCode}"); + throw new InvalidOperationException($"Command 'ar' exited with code: {arExitCode}."); arFilePathsForAllArchs.Add(arOutputFilePath); } @@ -327,7 +343,7 @@ MONO_AOT_MODE_LAST = 1000, int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs); if (lipoExitCode != 0) - throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}"); + throw new InvalidOperationException($"Command 'lipo' exited with code: {lipoExitCode}."); // TODO: Add the AOT lib and interpreter libs as device only to suppress warnings when targeting the simulator @@ -427,14 +443,14 @@ MONO_AOT_MODE_LAST = 1000, } else if (!Directory.Exists(androidToolchain)) { - throw new FileNotFoundException("Android toolchain not found: " + androidToolchain); + throw new FileNotFoundException($"Android toolchain not found: '{androidToolchain}'."); } var androidToolPrefixes = new Dictionary<string, string> { - ["armeabi-v7a"] = "arm-linux-androideabi-", - ["arm64-v8a"] = "aarch64-linux-android-", - ["x86"] = "i686-linux-android-", + ["arm32"] = "arm-linux-androideabi-", + ["arm64"] = "aarch64-linux-android-", + ["x86_32"] = "i686-linux-android-", ["x86_64"] = "x86_64-linux-android-" }; @@ -524,12 +540,12 @@ MONO_AOT_MODE_LAST = 1000, Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}"); if (!process.Start()) - throw new Exception("Failed to start process for Mono AOT compiler"); + throw new InvalidOperationException("Failed to start process for Mono AOT compiler."); process.WaitForExit(); if (process.ExitCode != 0) - throw new Exception($"Mono AOT compiler exited with code: {process.ExitCode}"); + throw new InvalidOperationException($"Mono AOT compiler exited with code: {process.ExitCode}."); } } @@ -547,9 +563,9 @@ MONO_AOT_MODE_LAST = 1000, { var androidAbis = new[] { - "armeabi-v7a", - "arm64-v8a", - "x86", + "arm32", + "arm64", + "x86_32", "x86_64" }; @@ -560,9 +576,9 @@ MONO_AOT_MODE_LAST = 1000, { var abiArchs = new Dictionary<string, string> { - ["armeabi-v7a"] = "armv7", - ["arm64-v8a"] = "aarch64-v8a", - ["x86"] = "i686", + ["arm32"] = "armv7", + ["arm64"] = "aarch64-v8a", + ["x86_32"] = "i686", ["x86_64"] = "x86_64" }; @@ -571,31 +587,25 @@ MONO_AOT_MODE_LAST = 1000, return $"{arch}-linux-android"; } - private static string GetMonoCrossDesktopDirName(string platform, string bits) + private static string GetMonoCrossDesktopDirName(string platform, string arch) { switch (platform) { case OS.Platforms.Windows: case OS.Platforms.UWP: { - string arch = bits == "64" ? "x86_64" : "i686"; return $"windows-{arch}"; } case OS.Platforms.MacOS: { - Debug.Assert(bits == null || bits == "64"); - string arch = "x86_64"; return $"{platform}-{arch}"; } case OS.Platforms.LinuxBSD: - case OS.Platforms.Server: { - string arch = bits == "64" ? "x86_64" : "i686"; return $"linux-{arch}"; } case OS.Platforms.Haiku: { - string arch = bits == "64" ? "x86_64" : "i686"; return $"{platform}-{arch}"; } default: diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index cca18a2a1f..851a8b3689 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -4,11 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using GodotTools.Build; using GodotTools.Core; using GodotTools.Internals; -using JetBrains.Annotations; using static GodotTools.Internals.Globals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; @@ -17,61 +15,13 @@ using Path = System.IO.Path; namespace GodotTools.Export { - public class ExportPlugin : EditorExportPlugin + public partial class ExportPlugin : EditorExportPlugin { - [Flags] - private enum I18NCodesets : long - { - None = 0, - CJK = 1, - MidEast = 2, - Other = 4, - Rare = 8, - West = 16, - All = CJK | MidEast | Other | Rare | West - } - - private string _maybeLastExportError; - - private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir) - { - var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets"); - - if (codesets == I18NCodesets.None) - return; - - void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll")); - - AddI18NAssembly("I18N"); - - if ((codesets & I18NCodesets.CJK) != 0) - AddI18NAssembly("I18N.CJK"); - if ((codesets & I18NCodesets.MidEast) != 0) - AddI18NAssembly("I18N.MidEast"); - if ((codesets & I18NCodesets.Other) != 0) - AddI18NAssembly("I18N.Other"); - if ((codesets & I18NCodesets.Rare) != 0) - AddI18NAssembly("I18N.Rare"); - if ((codesets & I18NCodesets.West) != 0) - AddI18NAssembly("I18N.West"); - } - public void RegisterExportSettings() { // TODO: These would be better as export preset options, but that doesn't seem to be supported yet GlobalDef("mono/export/include_scripts_content", false); - GlobalDef("mono/export/export_assemblies_inside_pck", true); - - GlobalDef("mono/export/i18n_codesets", I18NCodesets.All); - - ProjectSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Variant.Type.Int, - ["name"] = "mono/export/i18n_codesets", - ["hint"] = PropertyHint.Flags, - ["hint_string"] = "CJK,MidEast,Other,Rare,West" - }); GlobalDef("mono/export/aot/enabled", false); GlobalDef("mono/export/aot/full_aot", false); @@ -85,11 +35,7 @@ namespace GodotTools.Export GlobalDef("mono/export/aot/android_toolchain_path", ""); } - private void AddFile(string srcPath, string dstPath, bool remap = false) - { - // Add file to the PCK - AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap); - } + private string _maybeLastExportError; // With this method we can override how a file is exported in the PCK public override void _ExportFile(string path, string type, string[] features) @@ -100,7 +46,9 @@ namespace GodotTools.Export return; if (Path.GetExtension(path) != Internal.CSharpLanguageExtension) - throw new ArgumentException($"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path)); + throw new ArgumentException( + $"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", + nameof(path)); // TODO What if the source file is not part of the game's C# project @@ -150,161 +98,95 @@ namespace GodotTools.Export return; if (!DeterminePlatformFromFeatures(features, out string platform)) - throw new NotSupportedException("Target platform not supported"); + throw new NotSupportedException("Target platform not supported."); + + if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS } + .Contains(platform)) + { + throw new NotImplementedException("Target platform not yet implemented."); + } string outputDir = new FileInfo(path).Directory?.FullName ?? - throw new FileNotFoundException("Base directory not found"); + throw new FileNotFoundException("Output base directory not found."); string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; - if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform)) - throw new Exception("Failed to build project"); + // TODO: This works for now, as we only implemented support for x86 family desktop so far, but it needs to be fixed + string arch = features.Contains("x86_64") ? "x86_64" : "x86"; - // Add dependency assemblies + string ridOS = DetermineRuntimeIdentifierOS(platform); + string ridArch = DetermineRuntimeIdentifierArch(arch); + string runtimeIdentifier = $"{ridOS}-{ridArch}"; - var assemblies = new Godot.Collections.Dictionary<string, string>(); + // Create temporary publish output directory - string projectDllName = GodotSharpEditor.ProjectAssemblyName; - string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig); - string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll"); + string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", + $"{Process.GetCurrentProcess().Id}-{buildConfig}-{runtimeIdentifier}"); - assemblies[projectDllName] = projectDllSrcPath; + if (!Directory.Exists(publishOutputTempDir)) + Directory.CreateDirectory(publishOutputTempDir); - string bclDir = DeterminePlatformBclDir(platform); + // Execute dotnet publish - if (platform == OS.Platforms.Android) + if (!BuildManager.PublishProjectBlocking(buildConfig, platform, + runtimeIdentifier, publishOutputTempDir)) { - string godotAndroidExtProfileDir = GetBclProfileDir("godot_android_ext"); - string monoAndroidAssemblyPath = Path.Combine(godotAndroidExtProfileDir, "Mono.Android.dll"); + throw new InvalidOperationException("Failed to build project."); + } - if (!File.Exists(monoAndroidAssemblyPath)) - throw new FileNotFoundException("Assembly not found: 'Mono.Android'", monoAndroidAssemblyPath); + string soExt = ridOS switch + { + OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", + OS.DotNetOS.OSX or OS.DotNetOS.iOS => "dylib", + _ => "so" + }; - assemblies["Mono.Android"] = monoAndroidAssemblyPath; - } - else if (platform == OS.Platforms.HTML5) + if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll")) + // NativeAOT shared library output + && !File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}"))) { - // Ideally these would be added automatically since they're referenced by the wasm BCL assemblies. - // However, at least in the case of 'WebAssembly.Net.Http' for some reason the BCL assemblies - // reference a different version even though the assembly is the same, for some weird reason. - - var wasmFrameworkAssemblies = new[] { "WebAssembly.Bindings", "WebAssembly.Net.WebSockets" }; - - foreach (string thisWasmFrameworkAssemblyName in wasmFrameworkAssemblies) - { - string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - throw new FileNotFoundException($"Assembly not found: '{thisWasmFrameworkAssemblyName}'", thisWasmFrameworkAssemblyPath); - assemblies[thisWasmFrameworkAssemblyName] = thisWasmFrameworkAssemblyPath; - } - - // Assemblies that can have a different name in a newer version. Newer version must come first and it has priority. - (string newName, string oldName)[] wasmFrameworkAssembliesOneOf = new[] - { - ("System.Net.Http.WebAssemblyHttpHandler", "WebAssembly.Net.Http") - }; - - foreach (var thisWasmFrameworkAssemblyName in wasmFrameworkAssembliesOneOf) - { - string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName.newName + ".dll"); - if (File.Exists(thisWasmFrameworkAssemblyPath)) - { - assemblies[thisWasmFrameworkAssemblyName.newName] = thisWasmFrameworkAssemblyPath; - } - else - { - thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName.oldName + ".dll"); - if (!File.Exists(thisWasmFrameworkAssemblyPath)) - { - throw new FileNotFoundException("Expected one of the following assemblies but none were found: " + - $"'{thisWasmFrameworkAssemblyName.newName}' / '{thisWasmFrameworkAssemblyName.oldName}'", - thisWasmFrameworkAssemblyPath); - } - - assemblies[thisWasmFrameworkAssemblyName.oldName] = thisWasmFrameworkAssemblyPath; - } - } + throw new NotSupportedException( + "Publish succeeded but project assembly not found in the output directory"); } - var initialAssemblies = assemblies.Duplicate(); - internal_GetExportedAssemblyDependencies(initialAssemblies, buildConfig, bclDir, assemblies); - - AddI18NAssemblies(assemblies, bclDir); + // Copy all files from the dotnet publish output directory to + // a data directory next to the Godot output executable. - string outputDataDir = null; - - if (PlatformHasTemplateDir(platform)) - outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir); + string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - string apiConfig = isDebug ? "Debug" : "Release"; - string resAssembliesDir = Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig); + if (Directory.Exists(outputDataDir)) + Directory.Delete(outputDataDir, recursive: true); // Clean first - bool assembliesInsidePck = (bool)ProjectSettings.GetSetting("mono/export/export_assemblies_inside_pck") || outputDataDir == null; + Directory.CreateDirectory(outputDataDir); - if (!assembliesInsidePck) + foreach (string dir in Directory.GetDirectories(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - string outputDataGameAssembliesDir = Path.Combine(outputDataDir, "Assemblies"); - if (!Directory.Exists(outputDataGameAssembliesDir)) - Directory.CreateDirectory(outputDataGameAssembliesDir); + Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(publishOutputTempDir.Length + 1))); } - foreach (var assembly in assemblies) + foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - void AddToAssembliesDir(string fileSrcPath) - { - if (assembliesInsidePck) - { - string fileDstPath = Path.Combine(resAssembliesDir, fileSrcPath.GetFile()); - AddFile(fileSrcPath, fileDstPath); - } - else - { - Debug.Assert(outputDataDir != null); - string fileDstPath = Path.Combine(outputDataDir, "Assemblies", fileSrcPath.GetFile()); - File.Copy(fileSrcPath, fileDstPath); - } - } - - string assemblySrcPath = assembly.Value; - - string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null); - string pdbSrcPath = assemblyPathWithoutExtension + ".pdb"; - - AddToAssembliesDir(assemblySrcPath); - - if (File.Exists(pdbSrcPath)) - AddToAssembliesDir(pdbSrcPath); + File.Copy(file, Path.Combine(outputDataDir, file.Substring(publishOutputTempDir.Length + 1))); } + } - // AOT compilation - bool aotEnabled = platform == OS.Platforms.iOS || (bool)ProjectSettings.GetSetting("mono/export/aot/enabled"); + private string DetermineRuntimeIdentifierOS(string platform) + => OS.DotNetOSPlatformMap[platform]; - if (aotEnabled) + private string DetermineRuntimeIdentifierArch(string arch) + { + return arch switch { - string aotToolchainPath = null; - - if (platform == OS.Platforms.Android) - aotToolchainPath = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path"); - - if (aotToolchainPath == string.Empty) - aotToolchainPath = null; // Don't risk it being used as current working dir - - // TODO: LLVM settings are hard-coded and disabled for now - var aotOpts = new AotOptions - { - EnableLLVM = false, - LLVMOnly = false, - LLVMPath = "", - LLVMOutputPath = "", - FullAot = platform == OS.Platforms.iOS || (bool)(ProjectSettings.GetSetting("mono/export/aot/full_aot") ?? false), - UseInterpreter = (bool)ProjectSettings.GetSetting("mono/export/aot/use_interpreter"), - ExtraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options") ?? Array.Empty<string>(), - ExtraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? Array.Empty<string>(), - ToolchainPath = aotToolchainPath - }; - - AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, assemblies); - } + "x86" => "x86", + "x86_32" => "x86", + "x64" => "x64", + "x86_64" => "x64", + "armeabi-v7a" => "arm", + "arm64-v8a" => "arm64", + "armv7" => "arm", + "arm64" => "arm64", + _ => throw new ArgumentOutOfRangeException(nameof(arch), arch, "Unexpected architecture") + }; } public override void _ExportEnd() @@ -316,8 +198,10 @@ namespace GodotTools.Export if (Directory.Exists(aotTempDir)) Directory.Delete(aotTempDir, recursive: true); - // TODO: Just a workaround until the export plugins can be made to abort with errors - if (!string.IsNullOrEmpty(_maybeLastExportError)) // Check empty as well, because it's set to empty after hot-reloading + // TODO: The following is just a workaround until the export plugins can be made to abort with errors + + // We check for empty as well, because it's set to empty after hot-reloading + if (!string.IsNullOrEmpty(_maybeLastExportError)) { string lastExportError = _maybeLastExportError; _maybeLastExportError = null; @@ -326,69 +210,11 @@ namespace GodotTools.Export } } - [NotNull] - private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir) - { - string target = isDebug ? "release_debug" : "release"; - - // NOTE: Bits is ok for now as all platforms with a data directory only have one or two architectures. - // However, this may change in the future if we add arm linux or windows desktop templates. - string bits = features.Contains("64") ? "64" : "32"; - - string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}"; - - string templateDirPath = Path.Combine(Internal.FullExportTemplatesDir, TemplateDirName()); - bool validTemplatePathFound = true; - - if (!Directory.Exists(templateDirPath)) - { - validTemplatePathFound = false; - - if (isDebug) - { - target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name - templateDirPath = Path.Combine(Internal.FullExportTemplatesDir, TemplateDirName()); - validTemplatePathFound = true; - - if (!Directory.Exists(templateDirPath)) - validTemplatePathFound = false; - } - } - - if (!validTemplatePathFound) - throw new FileNotFoundException("Data template directory not found", templateDirPath); - - string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - - if (Directory.Exists(outputDataDir)) - Directory.Delete(outputDataDir, recursive: true); // Clean first - - Directory.CreateDirectory(outputDataDir); - - foreach (string dir in Directory.GetDirectories(templateDirPath, "*", SearchOption.AllDirectories)) - { - Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(templateDirPath.Length + 1))); - } - - foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories)) - { - File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1))); - } - - return outputDataDir; - } - - private static bool PlatformHasTemplateDir(string platform) - { - // macOS export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest. - return !new[] { OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform); - } - private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) { foreach (var feature in features) { - if (OS.PlatformNameMap.TryGetValue(feature, out platform)) + if (OS.PlatformFeatureMap.TryGetValue(feature, out platform)) return true; } @@ -396,87 +222,11 @@ namespace GodotTools.Export return false; } - private static string GetBclProfileDir(string profile) - { - string templatesDir = Internal.FullExportTemplatesDir; - return Path.Combine(templatesDir, "bcl", profile); - } - - private static string DeterminePlatformBclDir(string platform) - { - string templatesDir = Internal.FullExportTemplatesDir; - string platformBclDir = Path.Combine(templatesDir, "bcl", platform); - - if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll"))) - { - string profile = DeterminePlatformBclProfile(platform); - platformBclDir = Path.Combine(templatesDir, "bcl", profile); - - if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll"))) - { - if (PlatformRequiresCustomBcl(platform)) - throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}"); - - platformBclDir = typeof(object).Assembly.Location.GetBaseDir(); // Use the one we're running on - } - } - - return platformBclDir; - } - - /// <summary> - /// Determines whether the BCL bundled with the Godot editor can be used for the target platform, - /// or if it requires a custom BCL that must be distributed with the export templates. - /// </summary> - private static bool PlatformRequiresCustomBcl(string platform) - { - if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform)) - return true; - - // The 'net_4_x' BCL is not compatible between Windows and the other platforms. - // We use the names 'net_4_x_win' and 'net_4_x' to differentiate between the two. - - bool isWinOrUwp = new[] - { - OS.Platforms.Windows, - OS.Platforms.UWP - }.Contains(platform); - - return OS.IsWindows ? !isWinOrUwp : isWinOrUwp; - } - - private static string DeterminePlatformBclProfile(string platform) - { - switch (platform) - { - case OS.Platforms.Windows: - case OS.Platforms.UWP: - return "net_4_x_win"; - case OS.Platforms.MacOS: - case OS.Platforms.LinuxBSD: - case OS.Platforms.Server: - case OS.Platforms.Haiku: - return "net_4_x"; - case OS.Platforms.Android: - return "monodroid"; - case OS.Platforms.iOS: - return "monotouch"; - case OS.Platforms.HTML5: - return "wasm"; - default: - throw new NotSupportedException($"Platform not supported: {platform}"); - } - } - private static string DetermineDataDirNameForProject() { string appName = (string)ProjectSettings.GetSetting("application/config/name"); string appNameSafe = appName.ToSafeDirName(); return $"data_{appNameSafe}"; } - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_GetExportedAssemblyDependencies(Godot.Collections.Dictionary<string, string> initialAssemblies, - string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencyAssemblies); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs index 93ef837a83..4f5bebfb42 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs @@ -16,7 +16,7 @@ namespace GodotTools.Export _XcodePath = FindXcode(); if (_XcodePath == null) - throw new Exception("Could not find Xcode"); + throw new FileNotFoundException("Could not find Xcode."); } return _XcodePath; diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index b39c3d1c0d..994c11fa72 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -13,13 +13,14 @@ using GodotTools.Internals; using GodotTools.ProjectEditor; using JetBrains.Annotations; using static GodotTools.Internals.Globals; +using Environment = System.Environment; using File = GodotTools.Utils.File; using OS = GodotTools.Utils.OS; using Path = System.IO.Path; namespace GodotTools { - public class GodotSharpEditor : EditorPlugin, ISerializationListener + public partial class GodotSharpEditor : EditorPlugin, ISerializationListener { private EditorSettings _editorSettings; @@ -39,28 +40,27 @@ namespace GodotTools public bool SkipBuildBeforePlaying { get; set; } = false; - public static string ProjectAssemblyName + [UsedImplicitly] + private bool CreateProjectSolutionIfNeeded() { - get + if (!File.Exists(GodotSharpDirs.ProjectSlnPath) || !File.Exists(GodotSharpDirs.ProjectCsProjPath)) { - string projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); - projectAssemblyName = projectAssemblyName.ToSafeDirName(); - if (string.IsNullOrEmpty(projectAssemblyName)) - projectAssemblyName = "UnnamedProject"; - return projectAssemblyName; + return CreateProjectSolution(); } + + return true; } private bool CreateProjectSolution() { - using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 3)) + using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 2)) { pr.Step("Generating C# project...".TTR()); string resourceDir = ProjectSettings.GlobalizePath("res://"); string path = resourceDir; - string name = ProjectAssemblyName; + string name = GodotSharpDirs.ProjectAssemblyName; string guid = CsProjOperations.GenerateGameProject(path, name); @@ -75,7 +75,7 @@ namespace GodotTools { Guid = guid, PathRelativeToSolution = name + ".csproj", - Configs = new List<string> {"Debug", "ExportDebug", "ExportRelease"} + Configs = new List<string> { "Debug", "ExportDebug", "ExportRelease" } }; solution.AddNewProject(name, projectInfo); @@ -90,24 +90,6 @@ namespace GodotTools return false; } - pr.Step("Updating Godot API assemblies...".TTR()); - - string debugApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Debug"); - - if (!string.IsNullOrEmpty(debugApiAssembliesError)) - { - ShowErrorDialog("Failed to update the Godot API assemblies: " + debugApiAssembliesError); - return false; - } - - string releaseApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Release"); - - if (!string.IsNullOrEmpty(releaseApiAssembliesError)) - { - ShowErrorDialog("Failed to update the Godot API assemblies: " + releaseApiAssembliesError); - return false; - } - pr.Step("Done".TTR()); // Here, after all calls to progress_task_step @@ -141,7 +123,8 @@ namespace GodotTools try { string fallbackFolder = NuGetUtils.GodotFallbackFolderPath; - NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, fallbackFolder); + NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, + fallbackFolder); NuGetUtils.AddBundledPackagesToFallbackFolder(fallbackFolder); } catch (Exception e) @@ -167,13 +150,6 @@ namespace GodotTools Instance.MSBuildPanel.BuildSolution(); } - public override void _Ready() - { - base._Ready(); - - MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; - } - private enum MenuOptions { CreateSln, @@ -197,7 +173,7 @@ namespace GodotTools [UsedImplicitly] public Error OpenInExternalEditor(Script script, int line, int col) { - var editorId = (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor"); + var editorId = (ExternalEditorId)(int)_editorSettings.GetSetting("mono/editor/external_editor"); switch (editorId) { @@ -219,13 +195,15 @@ namespace GodotTools try { if (Godot.OS.IsStdoutVerbose()) - Console.WriteLine($"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}"); + Console.WriteLine( + $"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}"); OS.RunProcess(command, args); } catch (Exception e) { - GD.PushError($"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'"); + GD.PushError( + $"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'"); } break; @@ -347,7 +325,8 @@ namespace GodotTools [UsedImplicitly] public bool OverridesExternalEditor() { - return (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditorId.None; + return (ExternalEditorId)(int)_editorSettings.GetSetting("mono/editor/external_editor") != + ExternalEditorId.None; } public override bool _Build() @@ -363,12 +342,12 @@ namespace GodotTools DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) - ?? throw new Exception("Cannot open C# project"); + ?? throw new InvalidOperationException("Cannot open C# project."); // NOTE: The order in which changes are made to the project is important // Migrate to MSBuild project Sdks style if using the old style - ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, ProjectAssemblyName); + ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, GodotSharpDirs.ProjectAssemblyName); ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject); @@ -400,18 +379,49 @@ namespace GodotTools throw new InvalidOperationException(); Instance = this; + var dotNetSdkSearchVersion = Environment.Version; + + // First we try to find the .NET Sdk ourselves to make sure we get the + // correct version first (`RegisterDefaults` always picks the latest). + if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string sdkPath)) + { + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); + + ProjectUtils.MSBuildLocatorRegisterMSBuildPath(sdkPath); + } + else + { + try + { + ProjectUtils.MSBuildLocatorRegisterDefaults(out sdkVersion, out sdkPath); + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); + } + catch (InvalidOperationException e) + { + if (Godot.OS.IsStdoutVerbose()) + GD.PrintErr(e.ToString()); + GD.PushError($".NET Sdk not found. The required version is '{dotNetSdkSearchVersion}'."); + } + } + var editorInterface = GetEditorInterface(); var editorBaseControl = editorInterface.GetBaseControl(); _editorSettings = editorInterface.GetEditorSettings(); + GodotSharpDirs.RegisterProjectSettings(); + _errorDialog = new AcceptDialog(); editorBaseControl.AddChild(_errorDialog); MSBuildPanel = new MSBuildPanel(); + MSBuildPanel.Ready += () => + MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged; _bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR()); - AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"}); + AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" }); _menuPopup = new PopupMenu(); _menuPopup.Hide(); @@ -423,7 +433,7 @@ namespace GodotTools _toolBarBuildButton = new Button { Text = "Build", - HintTooltip = "Build Solution".TTR(), + TooltipText = "Build Solution".TTR(), FocusMode = Control.FocusModeEnum.None, Shortcut = buildSolutionShortcut, ShortcutInTooltip = true @@ -472,9 +482,9 @@ namespace GodotTools _editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary { - ["type"] = Variant.Type.Int, + ["type"] = (int)Variant.Type.Int, ["name"] = "mono/editor/external_editor", - ["hint"] = PropertyHint.Enum, + ["hint"] = (int)PropertyHint.Enum, ["hint_string"] = settingsHintStr }); @@ -487,7 +497,8 @@ namespace GodotTools try { // At startup we make sure NuGet.Config files have our Godot NuGet fallback folder included - NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, NuGetUtils.GodotFallbackFolderPath); + NuGetUtils.AddFallbackFolderToUserNuGetConfigs(NuGetUtils.GodotFallbackFolderName, + NuGetUtils.GodotFallbackFolderPath); } catch (Exception e) { @@ -503,20 +514,23 @@ namespace GodotTools protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_exportPluginWeak != null) + if (disposing) { - // We need to dispose our export plugin before the editor destroys EditorSettings. - // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid - // will be freed after EditorSettings already was, and its device polling thread - // will try to access the EditorSettings singleton, resulting in null dereferencing. - (_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); + if (IsInstanceValid(_exportPluginWeak)) + { + // We need to dispose our export plugin before the editor destroys EditorSettings. + // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid + // will be freed after EditorSettings already was, and its device polling thread + // will try to access the EditorSettings singleton, resulting in null dereferencing. + (_exportPluginWeak.GetRef().AsGodotObject() as ExportPlugin)?.Dispose(); + + _exportPluginWeak.Dispose(); + } - _exportPluginWeak.Dispose(); + GodotIdeManager?.Dispose(); } - GodotIdeManager?.Dispose(); + base.Dispose(disposing); } public void OnBeforeSerialize() @@ -533,8 +547,10 @@ namespace GodotTools public static GodotSharpEditor Instance { get; private set; } [UsedImplicitly] - private GodotSharpEditor() + private static IntPtr InternalCreateInstance(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) { + Internal.Initialize(unmanagedCallbacks, unmanagedCallbacksSize); + return new GodotSharpEditor().NativeInstance; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index f1d45463c5..30525ba04a 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -1,14 +1,24 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid> - <TargetFramework>net472</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <EnableDynamicLoading>true</EnableDynamicLoading> + <LangVersion>10</LangVersion> <!-- The Godot editor uses the Debug Godot API assemblies --> <GodotApiConfiguration>Debug</GodotApiConfiguration> <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir> + <ProduceReferenceAssembly>false</ProduceReferenceAssembly> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> + <!-- Needed for our source generators to work despite this not being a Godot game project --> + <PropertyGroup> + <IsGodotToolsProject>true</IsGodotToolsProject> + </PropertyGroup> + <ItemGroup> + <CompilerVisibleProperty Include="IsGodotToolsProject" /> + </ItemGroup> <PropertyGroup Condition=" Exists('$(GodotApiAssembliesDir)/GodotSharp.dll') "> <!-- The project is part of the Godot source tree --> <!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' --> @@ -20,6 +30,8 @@ <PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <!-- For RiderPathLocator --> + <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <Reference Include="GodotSharp"> <HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath> <Private>False</Private> @@ -30,6 +42,10 @@ </Reference> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\..\Godot.NET.Sdk\Godot.SourceGenerators\Godot.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + <ProjectReference Include="..\..\..\glue\GodotSharp\Godot.SourceGenerators.Internal\Godot.SourceGenerators.Internal.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + </ItemGroup> + <ItemGroup> <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" /> <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" /> <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" /> diff --git a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs index dd05f28af0..260d13a714 100644 --- a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs +++ b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs @@ -1,10 +1,11 @@ using Godot; using GodotTools.Internals; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; namespace GodotTools { - public class HotReloadAssemblyWatcher : Node + public partial class HotReloadAssemblyWatcher : Node { private Timer _watchTimer; @@ -25,6 +26,7 @@ namespace GodotTools Internal.ReloadAssemblies(softReload: false); } + [UsedImplicitly] public void RestartTimer() { _watchTimer.Stop(); @@ -38,7 +40,7 @@ namespace GodotTools _watchTimer = new Timer { OneShot = false, - WaitTime = (float)EditorDef("mono/assembly_watch_interval_sec", 0.5) + WaitTime = 0.5f }; _watchTimer.Timeout += TimerTimeout; AddChild(_watchTimer); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 23339fe50b..9df90ac608 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -8,7 +8,7 @@ using GodotTools.Internals; namespace GodotTools.Ides { - public sealed class GodotIdeManager : Node, ISerializationListener + public sealed partial class GodotIdeManager : Node, ISerializationListener { private MessagingServer _messagingServer; @@ -76,7 +76,7 @@ namespace GodotTools.Ides public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000) { - var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface() + var editorId = (ExternalEditorId)(int)GodotSharpEditor.Instance.GetEditorInterface() .GetEditorSettings().GetSetting("mono/editor/external_editor"); string editorIdentity = GetExternalEditorIdentity(editorId); @@ -153,7 +153,7 @@ namespace GodotTools.Ides } default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(editorId)); } } @@ -180,17 +180,17 @@ namespace GodotTools.Ides public void SendOpenFile(string file) { - SendRequest<OpenFileResponse>(new OpenFileRequest {File = file}); + SendRequest<OpenFileResponse>(new OpenFileRequest { File = file }); } public void SendOpenFile(string file, int line) { - SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line}); + SendRequest<OpenFileResponse>(new OpenFileRequest { File = file, Line = line }); } public void SendOpenFile(string file, int line, int column) { - SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column}); + SendRequest<OpenFileResponse>(new OpenFileRequest { File = file, Line = line, Column = column }); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 6f11831b80..62db6e3af5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -385,7 +385,7 @@ namespace GodotTools.Ides // However, it doesn't fix resource loading if the rest of the path is also case insensitive. string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile); - var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile}; + var response = new CodeCompletionResponse { Kind = request.Kind, ScriptFile = request.ScriptFile }; response.Suggestions = await Task.Run(() => Internal.CodeCompletionRequest(response.Kind, scriptFileLocalized ?? request.ScriptFile)); return response; diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs index 71055f0125..dad6e35344 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Runtime.Versioning; using Godot; -using JetBrains.Annotations; using Microsoft.Win32; using Newtonsoft.Json; using Directory = System.IO.Directory; @@ -41,7 +42,7 @@ namespace GodotTools.Ides.Rider { return CollectAllRiderPathsLinux(); } - throw new Exception("Unexpected OS."); + throw new InvalidOperationException("Unexpected OS."); } catch (Exception e) { @@ -113,6 +114,7 @@ namespace GodotTools.Ides.Rider return installInfos.ToArray(); } + [SupportedOSPlatform("windows")] private static RiderInfo[] CollectRiderInfosWindows() { var installInfos = new List<RiderInfo>(); @@ -214,9 +216,10 @@ namespace GodotTools.Ides.Rider return "../../build.txt"; if (OS.IsMacOS) return "Contents/Resources/build.txt"; - throw new Exception("Unknown OS."); + throw new InvalidOperationException("Unknown OS."); } + [SupportedOSPlatform("windows")] private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths) { using (var key = Registry.CurrentUser.OpenSubKey(registryKey)) @@ -229,6 +232,7 @@ namespace GodotTools.Ides.Rider } } + [SupportedOSPlatform("windows")] private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key) { if (key == null) return; @@ -324,7 +328,7 @@ namespace GodotTools.Ides.Rider { public string install_location; - [CanBeNull] + [return: MaybeNull] public static string GetInstallLocationFromJson(string json) { try @@ -378,7 +382,7 @@ namespace GodotTools.Ides.Rider public string version; public string versionSuffix; - [CanBeNull] + [return: MaybeNull] internal static ProductInfo GetProductInfo(string json) { try @@ -402,7 +406,7 @@ namespace GodotTools.Ides.Rider // ReSharper disable once InconsistentNaming public ActiveApplication active_application; - [CanBeNull] + [return: MaybeNull] public static string GetLatestBuildFromJson(string json) { try diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs index 3440eb701c..60602a5847 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs @@ -22,7 +22,7 @@ namespace GodotTools.Ides.Rider public static void Initialize() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor"); + var editor = (ExternalEditorId)(int)editorSettings.GetSetting("mono/editor/external_editor"); if (editor == ExternalEditorId.Rider) { if (!editorSettings.HasSetting(EditorPathSettingName)) @@ -30,9 +30,9 @@ namespace GodotTools.Ides.Rider Globals.EditorDef(EditorPathSettingName, "Optional"); editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary { - ["type"] = Variant.Type.String, + ["type"] = (int)Variant.Type.String, ["name"] = EditorPathSettingName, - ["hint"] = PropertyHint.File, + ["hint"] = (int)PropertyHint.File, ["hint_string"] = "" }); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs index 70ba7c733a..8f39ad063e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/EditorProgress.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using Godot; +using Godot.NativeInterop; namespace GodotTools.Internals { @@ -8,19 +9,12 @@ namespace GodotTools.Internals { public string Task { get; } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_Create(string task, string label, int amount, bool canCancel); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_Dispose(string task); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_Step(string task, string state, int step, bool forceRefresh); - public EditorProgress(string task, string label, int amount, bool canCancel = false) { Task = task; - internal_Create(task, label, amount, canCancel); + using godot_string taskIn = Marshaling.ConvertStringToNative(task); + using godot_string labelIn = Marshaling.ConvertStringToNative(label); + Internal.godot_icall_EditorProgress_Create(taskIn, labelIn, amount, canCancel); } ~EditorProgress() @@ -33,18 +27,23 @@ namespace GodotTools.Internals public void Dispose() { - internal_Dispose(Task); + using godot_string taskIn = Marshaling.ConvertStringToNative(Task); + Internal.godot_icall_EditorProgress_Dispose(taskIn); GC.SuppressFinalize(this); } public void Step(string state, int step = -1, bool forceRefresh = true) { - internal_Step(Task, state, step, forceRefresh); + using godot_string taskIn = Marshaling.ConvertStringToNative(Task); + using godot_string stateIn = Marshaling.ConvertStringToNative(state); + Internal.godot_icall_EditorProgress_Step(taskIn, stateIn, step, forceRefresh); } public bool TryStep(string state, int step = -1, bool forceRefresh = true) { - return internal_Step(Task, state, step, forceRefresh); + using godot_string taskIn = Marshaling.ConvertStringToNative(Task); + using godot_string stateIn = Marshaling.ConvertStringToNative(state); + return Internal.godot_icall_EditorProgress_Step(taskIn, stateIn, step, forceRefresh); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs index 5c5ced8c29..acb7cc3ab0 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Globals.cs @@ -1,3 +1,4 @@ +using Godot.NativeInterop; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -5,35 +6,41 @@ namespace GodotTools.Internals { public static class Globals { - public static float EditorScale => internal_EditorScale(); - - public static object GlobalDef(string setting, object defaultValue, bool restartIfChanged = false) => - internal_GlobalDef(setting, defaultValue, restartIfChanged); - - public static object EditorDef(string setting, object defaultValue, bool restartIfChanged = false) => - internal_EditorDef(setting, defaultValue, restartIfChanged); - - public static object EditorShortcut(string setting) => - internal_EditorShortcut(setting); + public static float EditorScale => Internal.godot_icall_Globals_EditorScale(); + + public static unsafe object GlobalDef(string setting, object defaultValue, bool restartIfChanged = false) + { + using godot_string settingIn = Marshaling.ConvertStringToNative(setting); + using godot_variant defaultValueIn = Marshaling.ConvertManagedObjectToVariant(defaultValue); + Internal.godot_icall_Globals_GlobalDef(settingIn, defaultValueIn, restartIfChanged, out godot_variant result); + using (result) + return Marshaling.ConvertVariantToManagedObject(result); + } + + public static unsafe object EditorDef(string setting, object defaultValue, bool restartIfChanged = false) + { + using godot_string settingIn = Marshaling.ConvertStringToNative(setting); + using godot_variant defaultValueIn = Marshaling.ConvertManagedObjectToVariant(defaultValue); + Internal.godot_icall_Globals_EditorDef(settingIn, defaultValueIn, restartIfChanged, out godot_variant result); + using (result) + return Marshaling.ConvertVariantToManagedObject(result); + } + + public static object EditorShortcut(string setting) + { + using godot_string settingIn = Marshaling.ConvertStringToNative(setting); + Internal.godot_icall_Globals_EditorShortcut(settingIn, out godot_variant result); + using (result) + return Marshaling.ConvertVariantToManagedObject(result); + } [SuppressMessage("ReSharper", "InconsistentNaming")] - public static string TTR(this string text) => internal_TTR(text); - - // Internal Calls - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern float internal_EditorScale(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern object internal_GlobalDef(string setting, object defaultValue, bool restartIfChanged); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern object internal_EditorDef(string setting, object defaultValue, bool restartIfChanged); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern object internal_EditorShortcut(string setting); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_TTR(string text); + public static string TTR(this string text) + { + using godot_string textIn = Marshaling.ConvertStringToNative(text); + Internal.godot_icall_Globals_TTR(textIn, out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index 5e70c399b2..14285cc0f1 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -1,103 +1,125 @@ -using System.Runtime.CompilerServices; +using System.IO; +using Godot; +using Godot.NativeInterop; +using GodotTools.Core; +using static GodotTools.Internals.Globals; namespace GodotTools.Internals { public static class GodotSharpDirs { - public static string ResDataDir => internal_ResDataDir(); - public static string ResMetadataDir => internal_ResMetadataDir(); - public static string ResAssembliesBaseDir => internal_ResAssembliesBaseDir(); - public static string ResAssembliesDir => internal_ResAssembliesDir(); - public static string ResConfigDir => internal_ResConfigDir(); - public static string ResTempDir => internal_ResTempDir(); - public static string ResTempAssembliesBaseDir => internal_ResTempAssembliesBaseDir(); - public static string ResTempAssembliesDir => internal_ResTempAssembliesDir(); - - public static string MonoUserDir => internal_MonoUserDir(); - public static string MonoLogsDir => internal_MonoLogsDir(); - - #region Tools-only - public static string MonoSolutionsDir => internal_MonoSolutionsDir(); - public static string BuildLogsDirs => internal_BuildLogsDirs(); - - public static string ProjectSlnPath => internal_ProjectSlnPath(); - public static string ProjectCsProjPath => internal_ProjectCsProjPath(); - - public static string DataEditorToolsDir => internal_DataEditorToolsDir(); - public static string DataEditorPrebuiltApiDir => internal_DataEditorPrebuiltApiDir(); - #endregion - - public static string DataMonoEtcDir => internal_DataMonoEtcDir(); - public static string DataMonoLibDir => internal_DataMonoLibDir(); - - #region Windows-only - public static string DataMonoBinDir => internal_DataMonoBinDir(); - #endregion - - - #region Internal - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResDataDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResMetadataDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResAssembliesBaseDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResAssembliesDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResConfigDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResTempDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResTempAssembliesBaseDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ResTempAssembliesDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoUserDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoLogsDir(); - - #region Tools-only - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoSolutionsDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_BuildLogsDirs(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ProjectSlnPath(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_ProjectCsProjPath(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataEditorToolsDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataEditorPrebuiltApiDir(); - #endregion - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataMonoEtcDir(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataMonoLibDir(); - - #region Windows-only - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_DataMonoBinDir(); - #endregion - - #endregion + public static string ResMetadataDir + { + get + { + Internal.godot_icall_GodotSharpDirs_ResMetadataDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static string MonoUserDir + { + get + { + Internal.godot_icall_GodotSharpDirs_MonoUserDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static string BuildLogsDirs + { + get + { + Internal.godot_icall_GodotSharpDirs_BuildLogsDirs(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static string DataEditorToolsDir + { + get + { + Internal.godot_icall_GodotSharpDirs_DataEditorToolsDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } + + public static void RegisterProjectSettings() + { + GlobalDef("dotnet/project/assembly_name", ""); + GlobalDef("dotnet/project/solution_directory", ""); + GlobalDef("dotnet/project/c#_project_directory", ""); + } + + private static void DetermineProjectLocation() + { + static string DetermineProjectName() + { + string projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); + projectAssemblyName = projectAssemblyName.ToSafeDirName(); + if (string.IsNullOrEmpty(projectAssemblyName)) + projectAssemblyName = "UnnamedProject"; + return projectAssemblyName; + } + + _projectAssemblyName = (string)ProjectSettings.GetSetting("dotnet/project/assembly_name"); + if (string.IsNullOrEmpty(_projectAssemblyName)) + { + _projectAssemblyName = DetermineProjectName(); + ProjectSettings.SetSetting("dotnet/project/assembly_name", _projectAssemblyName); + } + + string slnParentDir = (string)ProjectSettings.GetSetting("dotnet/project/solution_directory"); + if (string.IsNullOrEmpty(slnParentDir)) + slnParentDir = "res://"; + + string csprojParentDir = (string)ProjectSettings.GetSetting("dotnet/project/c#_project_directory"); + if (string.IsNullOrEmpty(csprojParentDir)) + csprojParentDir = "res://"; + + _projectSlnPath = Path.Combine(ProjectSettings.GlobalizePath(slnParentDir), + string.Concat(_projectAssemblyName, ".sln")); + + _projectCsProjPath = Path.Combine(ProjectSettings.GlobalizePath(csprojParentDir), + string.Concat(_projectAssemblyName, ".csproj")); + } + + private static string _projectAssemblyName; + private static string _projectSlnPath; + private static string _projectCsProjPath; + + public static string ProjectAssemblyName + { + get + { + if (_projectAssemblyName == null) + DetermineProjectLocation(); + return _projectAssemblyName; + } + } + + public static string ProjectSlnPath + { + get + { + if (_projectSlnPath == null) + DetermineProjectLocation(); + return _projectSlnPath; + } + } + + public static string ProjectCsProjPath + { + get + { + if (_projectCsProjPath == null) + DetermineProjectLocation(); + return _projectCsProjPath; + } + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 12c90178c9..fd810996f7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -1,114 +1,161 @@ +using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Godot; +using Godot.NativeInterop; +using Godot.SourceGenerators.Internal; using GodotTools.IdeMessaging.Requests; namespace GodotTools.Internals { - public static class Internal + [SuppressMessage("ReSharper", "InconsistentNaming")] + [GenerateUnmanagedCallbacks(typeof(InternalUnmanagedCallbacks))] + internal static partial class Internal { public const string CSharpLanguageType = "CSharpScript"; public const string CSharpLanguageExtension = ".cs"; - public static string UpdateApiAssembliesFromPrebuilt(string config) => - internal_UpdateApiAssembliesFromPrebuilt(config); + public static string FullExportTemplatesDir + { + get + { + godot_icall_Internal_FullExportTemplatesDir(out godot_string dest); + using (dest) + return Marshaling.ConvertStringToManaged(dest); + } + } - public static string FullExportTemplatesDir => - internal_FullExportTemplatesDir(); + public static string SimplifyGodotPath(this string path) => Godot.StringExtensions.SimplifyPath(path); - public static string SimplifyGodotPath(this string path) => internal_SimplifyGodotPath(path); + public static bool IsMacOSAppBundleInstalled(string bundleId) + { + using godot_string bundleIdIn = Marshaling.ConvertStringToNative(bundleId); + return godot_icall_Internal_IsMacOSAppBundleInstalled(bundleIdIn); + } - public static bool IsMacOSAppBundleInstalled(string bundleId) => internal_IsMacOSAppBundleInstalled(bundleId); + public static bool GodotIs32Bits() => godot_icall_Internal_GodotIs32Bits(); - public static bool GodotIs32Bits() => internal_GodotIs32Bits(); + public static bool GodotIsRealTDouble() => godot_icall_Internal_GodotIsRealTDouble(); - public static bool GodotIsRealTDouble() => internal_GodotIsRealTDouble(); + public static void GodotMainIteration() => godot_icall_Internal_GodotMainIteration(); - public static void GodotMainIteration() => internal_GodotMainIteration(); + public static bool IsAssembliesReloadingNeeded() => godot_icall_Internal_IsAssembliesReloadingNeeded(); - public static ulong GetCoreApiHash() => internal_GetCoreApiHash(); + public static void ReloadAssemblies(bool softReload) => godot_icall_Internal_ReloadAssemblies(softReload); - public static ulong GetEditorApiHash() => internal_GetEditorApiHash(); + public static void EditorDebuggerNodeReloadScripts() => godot_icall_Internal_EditorDebuggerNodeReloadScripts(); - public static bool IsAssembliesReloadingNeeded() => internal_IsAssembliesReloadingNeeded(); + public static bool ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus = true) => + godot_icall_Internal_ScriptEditorEdit(resource.NativeInstance, line, col, grabFocus); - public static void ReloadAssemblies(bool softReload) => internal_ReloadAssemblies(softReload); + public static void EditorNodeShowScriptScreen() => godot_icall_Internal_EditorNodeShowScriptScreen(); - public static void EditorDebuggerNodeReloadScripts() => internal_EditorDebuggerNodeReloadScripts(); + public static void EditorRunPlay() => godot_icall_Internal_EditorRunPlay(); - public static bool ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus = true) => - internal_ScriptEditorEdit(resource, line, col, grabFocus); + public static void EditorRunStop() => godot_icall_Internal_EditorRunStop(); - public static void EditorNodeShowScriptScreen() => internal_EditorNodeShowScriptScreen(); + public static void ScriptEditorDebugger_ReloadScripts() => + godot_icall_Internal_ScriptEditorDebugger_ReloadScripts(); - public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot(); + public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, + string scriptFile) + { + using godot_string scriptFileIn = Marshaling.ConvertStringToNative(scriptFile); + godot_icall_Internal_CodeCompletionRequest((int)kind, scriptFileIn, out godot_packed_string_array res); + using (res) + return Marshaling.ConvertNativePackedStringArrayToSystemArray(res); + } - public static void EditorRunPlay() => internal_EditorRunPlay(); + #region Internal - public static void EditorRunStop() => internal_EditorRunStop(); + private static bool initialized = false; - public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts(); + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Global + internal static unsafe void Initialize(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) + { + if (initialized) + throw new InvalidOperationException("Already initialized."); + initialized = true; - public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) => - internal_CodeCompletionRequest((int)kind, scriptFile); + if (unmanagedCallbacksSize != sizeof(InternalUnmanagedCallbacks)) + throw new ArgumentException("Unmanaged callbacks size mismatch.", nameof(unmanagedCallbacksSize)); - #region Internal + _unmanagedCallbacks = Unsafe.AsRef<InternalUnmanagedCallbacks>((void*)unmanagedCallbacks); + } + + private partial struct InternalUnmanagedCallbacks + { + } + + /* + * IMPORTANT: + * The order of the methods defined in NativeFuncs must match the order + * in the array defined at the bottom of 'editor/editor_internal_calls.cpp'. + */ + + public static partial void godot_icall_GodotSharpDirs_ResMetadataDir(out godot_string r_dest); + + public static partial void godot_icall_GodotSharpDirs_MonoUserDir(out godot_string r_dest); + + public static partial void godot_icall_GodotSharpDirs_BuildLogsDirs(out godot_string r_dest); + + public static partial void godot_icall_GodotSharpDirs_DataEditorToolsDir(out godot_string r_dest); + + public static partial void godot_icall_EditorProgress_Create(in godot_string task, in godot_string label, + int amount, bool canCancel); + + public static partial void godot_icall_EditorProgress_Dispose(in godot_string task); + + public static partial bool godot_icall_EditorProgress_Step(in godot_string task, in godot_string state, + int step, + bool forceRefresh); + + private static partial void godot_icall_Internal_FullExportTemplatesDir(out godot_string dest); + + private static partial bool godot_icall_Internal_IsMacOSAppBundleInstalled(in godot_string bundleId); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_UpdateApiAssembliesFromPrebuilt(string config); + private static partial bool godot_icall_Internal_GodotIs32Bits(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_FullExportTemplatesDir(); + private static partial bool godot_icall_Internal_GodotIsRealTDouble(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_SimplifyGodotPath(this string path); + private static partial void godot_icall_Internal_GodotMainIteration(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_IsMacOSAppBundleInstalled(string bundleId); + private static partial bool godot_icall_Internal_IsAssembliesReloadingNeeded(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_GodotIs32Bits(); + private static partial void godot_icall_Internal_ReloadAssemblies(bool softReload); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_GodotIsRealTDouble(); + private static partial void godot_icall_Internal_EditorDebuggerNodeReloadScripts(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_GodotMainIteration(); + private static partial bool godot_icall_Internal_ScriptEditorEdit(IntPtr resource, int line, int col, + bool grabFocus); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern ulong internal_GetCoreApiHash(); + private static partial void godot_icall_Internal_EditorNodeShowScriptScreen(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern ulong internal_GetEditorApiHash(); + private static partial void godot_icall_Internal_EditorRunPlay(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_IsAssembliesReloadingNeeded(); + private static partial void godot_icall_Internal_EditorRunStop(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_ReloadAssemblies(bool softReload); + private static partial void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorDebuggerNodeReloadScripts(); + private static partial void godot_icall_Internal_CodeCompletionRequest(int kind, in godot_string scriptFile, + out godot_packed_string_array res); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool internal_ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus); + public static partial float godot_icall_Globals_EditorScale(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorNodeShowScriptScreen(); + public static partial void godot_icall_Globals_GlobalDef(in godot_string setting, in godot_variant defaultValue, + bool restartIfChanged, out godot_variant result); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string internal_MonoWindowsInstallRoot(); + public static partial void godot_icall_Globals_EditorDef(in godot_string setting, in godot_variant defaultValue, + bool restartIfChanged, out godot_variant result); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorRunPlay(); + public static partial void + godot_icall_Globals_EditorShortcut(in godot_string setting, out godot_variant result); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_EditorRunStop(); + public static partial void godot_icall_Globals_TTR(in godot_string text, out godot_string dest); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void internal_ScriptEditorDebugger_ReloadScripts(); + public static partial void godot_icall_Utils_OS_GetPlatformName(out godot_string dest); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile); + public static partial bool godot_icall_Utils_OS_UnixFileHasExecutableAccess(in godot_string filePath); #endregion } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs index 05499339b1..89bda704bb 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs @@ -1,14 +1,14 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using Godot; using GodotTools.Core; -using JetBrains.Annotations; namespace GodotTools.Utils { public static class FsPathUtils { - private static readonly string _resourcePath = ProjectSettings.GlobalizePath("res://"); + private static readonly string ResourcePath = ProjectSettings.GlobalizePath("res://"); private static bool PathStartsWithAlreadyNorm(this string childPath, string parentPath) { @@ -30,11 +30,11 @@ namespace GodotTools.Utils return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm); } - [CanBeNull] + [return: MaybeNull] public static string LocalizePathWithCaseChecked(string path) { string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar; - string resourcePathNorm = _resourcePath.NormalizePath() + Path.DirectorySeparatorChar; + string resourcePathNorm = ResourcePath.NormalizePath() + Path.DirectorySeparatorChar; if (!pathNorm.PathStartsWithAlreadyNorm(resourcePathNorm)) return null; diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index 5cef6e5c3c..c16f803226 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -1,24 +1,24 @@ +using Godot.NativeInterop; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; -using JetBrains.Annotations; +using System.Runtime.Versioning; +using System.Text; +using GodotTools.Internals; namespace GodotTools.Utils { [SuppressMessage("ReSharper", "InconsistentNaming")] public static class OS { - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string GetPlatformName(); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool UnixFileHasExecutableAccess(string filePath); - - public static class Names + /// <summary> + /// Display names for the OS platforms. + /// </summary> + private static class Names { public const string Windows = "Windows"; public const string MacOS = "macOS"; @@ -26,27 +26,58 @@ namespace GodotTools.Utils public const string FreeBSD = "FreeBSD"; public const string NetBSD = "NetBSD"; public const string BSD = "BSD"; - public const string Server = "Server"; public const string UWP = "UWP"; public const string Haiku = "Haiku"; public const string Android = "Android"; public const string iOS = "iOS"; - public const string HTML5 = "HTML5"; + public const string Web = "Web"; } + /// <summary> + /// Godot platform identifiers. + /// </summary> public static class Platforms { public const string Windows = "windows"; public const string MacOS = "macos"; public const string LinuxBSD = "linuxbsd"; - public const string Server = "server"; public const string UWP = "uwp"; public const string Haiku = "haiku"; public const string Android = "android"; public const string iOS = "ios"; - public const string HTML5 = "javascript"; + public const string Web = "web"; + } + + /// <summary> + /// OS name part of the .NET runtime identifier (RID). + /// See https://docs.microsoft.com/en-us/dotnet/core/rid-catalog. + /// </summary> + public static class DotNetOS + { + public const string Win = "win"; + public const string OSX = "osx"; + public const string Linux = "linux"; + public const string Win10 = "win10"; + public const string Android = "android"; + public const string iOS = "ios"; + public const string Browser = "browser"; } + public static readonly Dictionary<string, string> PlatformFeatureMap = new Dictionary<string, string>( + // Export `features` may be in lower case + StringComparer.InvariantCultureIgnoreCase + ) + { + ["Windows"] = Platforms.Windows, + ["macOS"] = Platforms.MacOS, + ["Linux"] = Platforms.LinuxBSD, + ["UWP"] = Platforms.UWP, + ["Haiku"] = Platforms.Haiku, + ["Android"] = Platforms.Android, + ["iOS"] = Platforms.iOS, + ["Web"] = Platforms.Web + }; + public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string> { [Names.Windows] = Platforms.Windows, @@ -55,55 +86,85 @@ namespace GodotTools.Utils [Names.FreeBSD] = Platforms.LinuxBSD, [Names.NetBSD] = Platforms.LinuxBSD, [Names.BSD] = Platforms.LinuxBSD, - [Names.Server] = Platforms.Server, [Names.UWP] = Platforms.UWP, [Names.Haiku] = Platforms.Haiku, [Names.Android] = Platforms.Android, [Names.iOS] = Platforms.iOS, - [Names.HTML5] = Platforms.HTML5 + [Names.Web] = Platforms.Web + }; + + public static readonly Dictionary<string, string> DotNetOSPlatformMap = new Dictionary<string, string> + { + [Platforms.Windows] = DotNetOS.Win, + [Platforms.MacOS] = DotNetOS.OSX, + // TODO: + // Does .NET 6 support BSD variants? If it does, it may need the name `unix` + // instead of `linux` in the runtime identifier. This would be a problem as + // Godot has a single export profile for both, named LinuxBSD. + [Platforms.LinuxBSD] = DotNetOS.Linux, + [Platforms.UWP] = DotNetOS.Win10, + [Platforms.Android] = DotNetOS.Android, + [Platforms.iOS] = DotNetOS.iOS, + [Platforms.Web] = DotNetOS.Browser }; private static bool IsOS(string name) { - return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase); + Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); + using (dest) + { + string platformName = Marshaling.ConvertStringToManaged(dest); + return name.Equals(platformName, StringComparison.OrdinalIgnoreCase); + } } private static bool IsAnyOS(IEnumerable<string> names) { - return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase)); + Internal.godot_icall_Utils_OS_GetPlatformName(out godot_string dest); + using (dest) + { + string platformName = Marshaling.ConvertStringToManaged(dest); + return names.Any(p => p.Equals(platformName, StringComparison.OrdinalIgnoreCase)); + } } private static readonly IEnumerable<string> LinuxBSDPlatforms = new[] { Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD }; private static readonly IEnumerable<string> UnixLikePlatforms = - new[] { Names.MacOS, Names.Server, Names.Haiku, Names.Android, Names.iOS } + new[] { Names.MacOS, Names.Haiku, Names.Android, Names.iOS } .Concat(LinuxBSDPlatforms).ToArray(); - private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows)); - private static readonly Lazy<bool> _isMacOS = new Lazy<bool>(() => IsOS(Names.MacOS)); - private static readonly Lazy<bool> _isLinuxBSD = new Lazy<bool>(() => IsAnyOS(LinuxBSDPlatforms)); - private static readonly Lazy<bool> _isServer = new Lazy<bool>(() => IsOS(Names.Server)); - private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP)); - private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku)); - private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android)); - private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS)); - private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5)); - private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms)); - - public static bool IsWindows => _isWindows.Value || IsUWP; - public static bool IsMacOS => _isMacOS.Value; - public static bool IsLinuxBSD => _isLinuxBSD.Value; - public static bool IsServer => _isServer.Value; - public static bool IsUWP => _isUWP.Value; + private static readonly Lazy<bool> _isWindows = new(() => IsOS(Names.Windows)); + private static readonly Lazy<bool> _isMacOS = new(() => IsOS(Names.MacOS)); + private static readonly Lazy<bool> _isLinuxBSD = new(() => IsAnyOS(LinuxBSDPlatforms)); + private static readonly Lazy<bool> _isUWP = new(() => IsOS(Names.UWP)); + private static readonly Lazy<bool> _isHaiku = new(() => IsOS(Names.Haiku)); + private static readonly Lazy<bool> _isAndroid = new(() => IsOS(Names.Android)); + private static readonly Lazy<bool> _isiOS = new(() => IsOS(Names.iOS)); + private static readonly Lazy<bool> _isWeb = new(() => IsOS(Names.Web)); + private static readonly Lazy<bool> _isUnixLike = new(() => IsAnyOS(UnixLikePlatforms)); + + [SupportedOSPlatformGuard("windows")] public static bool IsWindows => _isWindows.Value || IsUWP; + + [SupportedOSPlatformGuard("osx")] public static bool IsMacOS => _isMacOS.Value; + + [SupportedOSPlatformGuard("linux")] public static bool IsLinuxBSD => _isLinuxBSD.Value; + + [SupportedOSPlatformGuard("windows")] public static bool IsUWP => _isUWP.Value; + public static bool IsHaiku => _isHaiku.Value; - public static bool IsAndroid => _isAndroid.Value; - public static bool IsiOS => _isiOS.Value; - public static bool IsHTML5 => _isHTML5.Value; + + [SupportedOSPlatformGuard("android")] public static bool IsAndroid => _isAndroid.Value; + + [SupportedOSPlatformGuard("ios")] public static bool IsiOS => _isiOS.Value; + + [SupportedOSPlatformGuard("browser")] public static bool IsWeb => _isWeb.Value; public static bool IsUnixLike => _isUnixLike.Value; public static char PathSep => IsWindows ? ';' : ':'; + [return: MaybeNull] public static string PathWhich([NotNull] string name) { if (IsWindows) @@ -112,9 +173,11 @@ namespace GodotTools.Utils return PathWhichUnix(name); } + [return: MaybeNull] private static string PathWhichWindows([NotNull] string name) { - string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>(); + string[] windowsExts = + Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>(); string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); char[] invalidPathChars = Path.GetInvalidPathChars(); @@ -133,7 +196,7 @@ namespace GodotTools.Utils string nameExt = Path.GetExtension(name); bool hasPathExt = !string.IsNullOrEmpty(nameExt) && - windowsExts.Contains(nameExt, StringComparer.OrdinalIgnoreCase); + windowsExts.Contains(nameExt, StringComparer.OrdinalIgnoreCase); searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list @@ -147,6 +210,7 @@ namespace GodotTools.Utils select path + ext).FirstOrDefault(File.Exists); } + [return: MaybeNull] private static string PathWhichUnix([NotNull] string name) { string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); @@ -168,19 +232,16 @@ namespace GodotTools.Utils searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list return searchDirs.Select(dir => Path.Combine(dir, name)) - .FirstOrDefault(path => File.Exists(path) && UnixFileHasExecutableAccess(path)); + .FirstOrDefault(path => + { + using godot_string pathIn = Marshaling.ConvertStringToNative(path); + return File.Exists(path) && Internal.godot_icall_Utils_OS_UnixFileHasExecutableAccess(pathIn); + }); } public static void RunProcess(string command, IEnumerable<string> arguments) { - // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead - string CmdLineArgsToString(IEnumerable<string> args) - { - // Not perfect, but as long as we are careful... - return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); - } - - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)) + var startInfo = new ProcessStartInfo(command) { RedirectStandardOutput = true, RedirectStandardError = true, @@ -188,44 +249,104 @@ namespace GodotTools.Utils CreateNoWindow = true }; - using (Process process = Process.Start(startInfo)) - { - if (process == null) - throw new Exception("No process was started"); + foreach (string arg in arguments) + startInfo.ArgumentList.Add(arg); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - if (IsWindows && process.Id > 0) - User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself - } + using Process process = Process.Start(startInfo); + + if (process == null) + throw new InvalidOperationException("No process was started."); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (IsWindows && process.Id > 0) + User32Dll.AllowSetForegroundWindow(process.Id); // Allows application to focus itself } public static int ExecuteCommand(string command, IEnumerable<string> arguments) { - // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead - string CmdLineArgsToString(IEnumerable<string> args) + var startInfo = new ProcessStartInfo(command) { - // Not perfect, but as long as we are careful... - return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); - } + // Print the output + RedirectStandardOutput = false, + RedirectStandardError = false, + UseShellExecute = false + }; + + foreach (string arg in arguments) + startInfo.ArgumentList.Add(arg); - var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments)); + Console.WriteLine(startInfo.GetCommandLineDisplay(new StringBuilder("Executing: ")).ToString()); - Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}"); + using var process = new Process { StartInfo = startInfo }; + process.Start(); + process.WaitForExit(); + + return process.ExitCode; + } - // Print the output - startInfo.RedirectStandardOutput = false; - startInfo.RedirectStandardError = false; + private static void AppendProcessFileNameForDisplay(this StringBuilder builder, string fileName) + { + if (builder.Length > 0) + builder.Append(' '); + + if (fileName.Contains(' ')) + { + builder.Append('"'); + builder.Append(fileName); + builder.Append('"'); + } + else + { + builder.Append(fileName); + } + } - startInfo.UseShellExecute = false; + private static void AppendProcessArgumentsForDisplay(this StringBuilder builder, + Collection<string> argumentList) + { + // This is intended just for reading. It doesn't need to be a valid command line. + // E.g.: We don't handle escaping of quotes. - using (var process = new Process { StartInfo = startInfo }) + foreach (string argument in argumentList) { - process.Start(); - process.WaitForExit(); + if (builder.Length > 0) + builder.Append(' '); - return process.ExitCode; + if (argument.Contains(' ')) + { + builder.Append('"'); + builder.Append(argument); + builder.Append('"'); + } + else + { + builder.Append(argument); + } } } + + public static StringBuilder GetCommandLineDisplay( + this ProcessStartInfo startInfo, + StringBuilder optionalBuilder = null + ) + { + var builder = optionalBuilder ?? new StringBuilder(); + + builder.AppendProcessFileNameForDisplay(startInfo.FileName); + + if (startInfo.ArgumentList.Count == 0) + { + builder.Append(' '); + builder.Append(startInfo.Arguments); + } + else + { + builder.AppendProcessArgumentsForDisplay(startInfo.ArgumentList); + } + + return builder; + } } } diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 2e628cb576..0e496454a1 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -41,69 +41,99 @@ #include "core/string/ucaps.h" #include "main/main.h" -#include "../glue/cs_glue_version.gen.h" #include "../godotsharp_defs.h" -#include "../mono_gd/gd_mono_marshal.h" #include "../utils/path_utils.h" #include "../utils/string_utils.h" +StringBuilder &operator<<(StringBuilder &r_sb, const String &p_string) { + r_sb.append(p_string); + return r_sb; +} + +StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) { + r_sb.append(p_cstring); + return r_sb; +} + #define CS_INDENT " " // 4 whitespaces #define INDENT1 CS_INDENT #define INDENT2 INDENT1 INDENT1 #define INDENT3 INDENT2 INDENT1 #define INDENT4 INDENT3 INDENT1 -#define INDENT5 INDENT4 INDENT1 -#define MEMBER_BEGIN "\n" INDENT2 +#define MEMBER_BEGIN "\n" INDENT1 #define OPEN_BLOCK "{\n" #define CLOSE_BLOCK "}\n" -#define OPEN_BLOCK_L2 INDENT2 OPEN_BLOCK INDENT3 -#define OPEN_BLOCK_L3 INDENT3 OPEN_BLOCK INDENT4 +#define OPEN_BLOCK_L1 INDENT1 OPEN_BLOCK +#define OPEN_BLOCK_L2 INDENT2 OPEN_BLOCK +#define CLOSE_BLOCK_L1 INDENT1 CLOSE_BLOCK #define CLOSE_BLOCK_L2 INDENT2 CLOSE_BLOCK #define CLOSE_BLOCK_L3 INDENT3 CLOSE_BLOCK -#define CS_FIELD_MEMORYOWN "memoryOwn" +#define BINDINGS_GLOBAL_SCOPE_CLASS "GD" +#define BINDINGS_NATIVE_NAME_FIELD "NativeName" + +#define CS_PARAM_MEMORYOWN "memoryOwn" #define CS_PARAM_METHODBIND "method" #define CS_PARAM_INSTANCE "ptr" -#define CS_SMETHOD_GETINSTANCE "GetPtr" +#define CS_STATIC_METHOD_GETINSTANCE "GetPtr" #define CS_METHOD_CALL "Call" +#define CS_PROPERTY_SINGLETON "Singleton" +#define CS_METHOD_INVOKE_GODOT_CLASS_METHOD "InvokeGodotClassMethod" +#define CS_METHOD_HAS_GODOT_CLASS_METHOD "HasGodotClassMethod" + +#define CS_STATIC_FIELD_NATIVE_CTOR "NativeCtor" +#define CS_STATIC_FIELD_METHOD_BIND_PREFIX "MethodBind" +#define CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX "MethodProxyName_" -#define GLUE_HEADER_FILE "glue_header.h" #define ICALL_PREFIX "godot_icall_" -#define SINGLETON_ICALL_SUFFIX "_get_singleton" -#define ICALL_GET_METHODBIND "__ClassDB_get_method" +#define ICALL_CLASSDB_GET_METHOD "ClassDB_get_method" +#define ICALL_CLASSDB_GET_CONSTRUCTOR "ClassDB_get_constructor" #define C_LOCAL_RET "ret" #define C_LOCAL_VARARG_RET "vararg_ret" #define C_LOCAL_PTRCALL_ARGS "call_args" -#define C_MACRO_OBJECT_CONSTRUCT "GODOTSHARP_INSTANCE_OBJECT" - -#define C_NS_MONOUTILS "GDMonoUtils" -#define C_NS_MONOINTERNALS "GDMonoInternals" -#define C_METHOD_TIE_MANAGED_TO_UNMANAGED C_NS_MONOINTERNALS "::tie_managed_to_unmanaged" -#define C_METHOD_UNMANAGED_GET_MANAGED C_NS_MONOUTILS "::unmanaged_get_managed" - -#define C_NS_MONOMARSHAL "GDMonoMarshal" -#define C_METHOD_MANAGED_TO_VARIANT C_NS_MONOMARSHAL "::mono_object_to_variant" -#define C_METHOD_MANAGED_FROM_VARIANT C_NS_MONOMARSHAL "::variant_to_mono_object" -#define C_METHOD_MONOSTR_TO_GODOT C_NS_MONOMARSHAL "::mono_string_to_godot" -#define C_METHOD_MONOSTR_FROM_GODOT C_NS_MONOMARSHAL "::mono_string_from_godot" -#define C_METHOD_MONOARRAY_TO(m_type) C_NS_MONOMARSHAL "::mono_array_to_" #m_type -#define C_METHOD_MONOARRAY_FROM(m_type) C_NS_MONOMARSHAL "::" #m_type "_to_mono_array" -#define C_METHOD_MANAGED_TO_CALLABLE C_NS_MONOMARSHAL "::managed_to_callable" -#define C_METHOD_MANAGED_FROM_CALLABLE C_NS_MONOMARSHAL "::callable_to_managed" -#define C_METHOD_MANAGED_TO_SIGNAL C_NS_MONOMARSHAL "::signal_info_to_callable" -#define C_METHOD_MANAGED_FROM_SIGNAL C_NS_MONOMARSHAL "::callable_to_signal_info" - -#define BINDINGS_GENERATOR_VERSION UINT32_C(13) + +#define C_CLASS_NATIVE_FUNCS "NativeFuncs" +#define C_NS_MONOUTILS "InteropUtils" +#define C_METHOD_UNMANAGED_GET_MANAGED C_NS_MONOUTILS ".UnmanagedGetManaged" +#define C_METHOD_ENGINE_GET_SINGLETON C_NS_MONOUTILS ".EngineGetSingleton" + +#define C_NS_MONOMARSHAL "Marshaling" +#define C_METHOD_MONOSTR_TO_GODOT C_NS_MONOMARSHAL ".ConvertStringToNative" +#define C_METHOD_MONOSTR_FROM_GODOT C_NS_MONOMARSHAL ".ConvertStringToManaged" +#define C_METHOD_MONOARRAY_TO(m_type) C_NS_MONOMARSHAL ".ConvertSystemArrayToNative" #m_type +#define C_METHOD_MONOARRAY_FROM(m_type) C_NS_MONOMARSHAL ".ConvertNative" #m_type "ToSystemArray" +#define C_METHOD_MANAGED_TO_CALLABLE C_NS_MONOMARSHAL ".ConvertCallableToNative" +#define C_METHOD_MANAGED_FROM_CALLABLE C_NS_MONOMARSHAL ".ConvertCallableToManaged" +#define C_METHOD_MANAGED_TO_SIGNAL C_NS_MONOMARSHAL ".ConvertSignalToNative" +#define C_METHOD_MANAGED_FROM_SIGNAL C_NS_MONOMARSHAL ".ConvertSignalToManaged" // Types that will be ignored by the generator and won't be available in C#. const Vector<String> ignored_types = { "PhysicsServer3DExtension" }; -const char *BindingsGenerator::TypeInterface::DEFAULT_VARARG_C_IN("\t%0 %1_in = %1;\n"); +void BindingsGenerator::TypeInterface::postsetup_enum_type(BindingsGenerator::TypeInterface &r_enum_itype) { + // C interface for enums is the same as that of 'uint32_t'. Remember to apply + // any of the changes done here to the 'uint32_t' type interface as well. + + r_enum_itype.cs_type = r_enum_itype.proxy_name; + r_enum_itype.cs_in_expr = "(int)%0"; + r_enum_itype.cs_out = "%5return (%2)%0(%1);"; + + { + // The expected types for parameters and return value in ptrcall are 'int64_t' or 'uint64_t'. + r_enum_itype.c_in = "%5%0 %1_in = %1;\n"; + r_enum_itype.c_out = "%5return (%0)%1;\n"; + r_enum_itype.c_type = "long"; + r_enum_itype.c_arg_in = "&%s_in"; + } + r_enum_itype.c_type_in = "int"; + r_enum_itype.c_type_out = r_enum_itype.c_type_in; + r_enum_itype.class_doc = &EditorHelp::get_doc_data()->class_list[r_enum_itype.proxy_name]; +} static String fix_doc_description(const String &p_bbcode) { // This seems to be the correct way to do this. It's the same EditorHelp does. @@ -359,23 +389,23 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf xml_output.append(tag); xml_output.append("</c>"); } else if (tag == "PackedByteArray") { - xml_output.append("<see cref=\"T:byte[]\"/>"); + xml_output.append("<see cref=\"byte\"/>[]"); } else if (tag == "PackedInt32Array") { - xml_output.append("<see cref=\"T:int[]\"/>"); + xml_output.append("<see cref=\"int\"/>[]"); } else if (tag == "PackedInt64Array") { - xml_output.append("<see cref=\"T:long[]\"/>"); + xml_output.append("<see cref=\"long\"/>[]"); } else if (tag == "PackedFloat32Array") { - xml_output.append("<see cref=\"T:float[]\"/>"); + xml_output.append("<see cref=\"float\"/>[]"); } else if (tag == "PackedFloat64Array") { - xml_output.append("<see cref=\"T:double[]\"/>"); + xml_output.append("<see cref=\"double\"/>[]"); } else if (tag == "PackedStringArray") { - xml_output.append("<see cref=\"T:string[]\"/>"); + xml_output.append("<see cref=\"string\"/>[]"); } else if (tag == "PackedVector2Array") { - xml_output.append("<see cref=\"T:" BINDINGS_NAMESPACE ".Vector2[]\"/>"); + xml_output.append("<see cref=\"" BINDINGS_NAMESPACE ".Vector2\"/>[]"); } else if (tag == "PackedVector3Array") { - xml_output.append("<see cref=\"T:" BINDINGS_NAMESPACE ".Vector3[]\"/>"); + xml_output.append("<see cref=\"" BINDINGS_NAMESPACE ".Vector3\"/>[]"); } else if (tag == "PackedColorArray") { - xml_output.append("<see cref=\"T:" BINDINGS_NAMESPACE ".Color[]\"/>"); + xml_output.append("<see cref=\"" BINDINGS_NAMESPACE ".Color\"/>[]"); } else { const TypeInterface *target_itype = _get_type_or_null(TypeReference(tag)); @@ -794,49 +824,28 @@ void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumI } } -void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) { +Error BindingsGenerator::_populate_method_icalls_table(const TypeInterface &p_itype) { for (const MethodInterface &imethod : p_itype.methods) { if (imethod.is_virtual) { continue; } - const TypeInterface *return_type = _get_type_or_placeholder(imethod.return_type); + const TypeInterface *return_type = _get_type_or_null(imethod.return_type); + ERR_FAIL_NULL_V(return_type, ERR_BUG); // Return type not found - String im_sig = "IntPtr " CS_PARAM_METHODBIND; - String im_unique_sig = imethod.return_type.cname.operator String() + ",IntPtr"; + String im_unique_sig = get_ret_unique_sig(return_type) + ",CallMethodBind"; if (!imethod.is_static) { - im_sig += ", IntPtr " CS_PARAM_INSTANCE; - im_unique_sig += ",IntPtr"; + im_unique_sig += ",CallInstance"; } // Get arguments information - int i = 0; for (const ArgumentInterface &iarg : imethod.arguments) { - const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type); - - im_sig += ", "; - im_sig += arg_type->im_type_in; - im_sig += " arg"; - im_sig += itos(i + 1); + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found im_unique_sig += ","; - im_unique_sig += get_unique_sig(*arg_type); - - i++; - } - - String im_type_out = return_type->im_type_out; - - if (return_type->ret_as_byref_arg) { - // Doesn't affect the unique signature - im_type_out = "void"; - - im_sig += ", "; - im_sig += return_type->im_type_out; - im_sig += " argRet"; - - i++; + im_unique_sig += get_arg_unique_sig(*arg_type); } // godot_icall_{argc}_{icallcount} @@ -845,7 +854,15 @@ void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) { icall_method += "_"; icall_method += itos(method_icalls.size()); - InternalCall im_icall = InternalCall(p_itype.api_type, icall_method, im_type_out, im_sig, im_unique_sig); + InternalCall im_icall = InternalCall(p_itype.api_type, icall_method, im_unique_sig); + + im_icall.is_vararg = imethod.is_vararg; + im_icall.is_static = imethod.is_static; + im_icall.return_type = imethod.return_type; + + for (const List<ArgumentInterface>::Element *F = imethod.arguments.front(); F; F = F->next()) { + im_icall.argument_types.push_back(F->get().type); + } List<InternalCall>::Element *match = method_icalls.find(im_icall); @@ -859,47 +876,49 @@ void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) { method_icalls_map.insert(&imethod, &added->get()); } } + + return OK; } void BindingsGenerator::_generate_array_extensions(StringBuilder &p_output) { + p_output.append("namespace " BINDINGS_NAMESPACE ";\n\n"); p_output.append("using System;\n\n"); - p_output.append("namespace " BINDINGS_NAMESPACE "\n" OPEN_BLOCK); // The class where we put the extensions doesn't matter, so just use "GD". - p_output.append(INDENT1 "public static partial class " BINDINGS_GLOBAL_SCOPE_CLASS "\n" INDENT1 "{"); + p_output.append("public static partial class " BINDINGS_GLOBAL_SCOPE_CLASS "\n{"); #define ARRAY_IS_EMPTY(m_type) \ - p_output.append("\n" INDENT2 "/// <summary>\n"); \ - p_output.append(INDENT2 "/// Returns true if this " #m_type " array is empty or doesn't exist.\n"); \ - p_output.append(INDENT2 "/// </summary>\n"); \ - p_output.append(INDENT2 "/// <param name=\"instance\">The " #m_type " array check.</param>\n"); \ - p_output.append(INDENT2 "/// <returns>Whether or not the array is empty.</returns>\n"); \ - p_output.append(INDENT2 "public static bool IsEmpty(this " #m_type "[] instance)\n"); \ - p_output.append(INDENT2 OPEN_BLOCK); \ - p_output.append(INDENT3 "return instance == null || instance.Length == 0;\n"); \ - p_output.append(INDENT2 CLOSE_BLOCK); + p_output.append("\n" INDENT1 "/// <summary>\n"); \ + p_output.append(INDENT1 "/// Returns true if this " #m_type " array is empty or doesn't exist.\n"); \ + p_output.append(INDENT1 "/// </summary>\n"); \ + p_output.append(INDENT1 "/// <param name=\"instance\">The " #m_type " array check.</param>\n"); \ + p_output.append(INDENT1 "/// <returns>Whether or not the array is empty.</returns>\n"); \ + p_output.append(INDENT1 "public static bool IsEmpty(this " #m_type "[] instance)\n"); \ + p_output.append(OPEN_BLOCK_L1); \ + p_output.append(INDENT2 "return instance == null || instance.Length == 0;\n"); \ + p_output.append(INDENT1 CLOSE_BLOCK); #define ARRAY_JOIN(m_type) \ - p_output.append("\n" INDENT2 "/// <summary>\n"); \ - p_output.append(INDENT2 "/// Converts this " #m_type " array to a string delimited by the given string.\n"); \ - p_output.append(INDENT2 "/// </summary>\n"); \ - p_output.append(INDENT2 "/// <param name=\"instance\">The " #m_type " array to convert.</param>\n"); \ - p_output.append(INDENT2 "/// <param name=\"delimiter\">The delimiter to use between items.</param>\n"); \ - p_output.append(INDENT2 "/// <returns>A single string with all items.</returns>\n"); \ - p_output.append(INDENT2 "public static string Join(this " #m_type "[] instance, string delimiter = \", \")\n"); \ - p_output.append(INDENT2 OPEN_BLOCK); \ - p_output.append(INDENT3 "return String.Join(delimiter, instance);\n"); \ - p_output.append(INDENT2 CLOSE_BLOCK); + p_output.append("\n" INDENT1 "/// <summary>\n"); \ + p_output.append(INDENT1 "/// Converts this " #m_type " array to a string delimited by the given string.\n"); \ + p_output.append(INDENT1 "/// </summary>\n"); \ + p_output.append(INDENT1 "/// <param name=\"instance\">The " #m_type " array to convert.</param>\n"); \ + p_output.append(INDENT1 "/// <param name=\"delimiter\">The delimiter to use between items.</param>\n"); \ + p_output.append(INDENT1 "/// <returns>A single string with all items.</returns>\n"); \ + p_output.append(INDENT1 "public static string Join(this " #m_type "[] instance, string delimiter = \", \")\n"); \ + p_output.append(OPEN_BLOCK_L1); \ + p_output.append(INDENT2 "return String.Join(delimiter, instance);\n"); \ + p_output.append(INDENT1 CLOSE_BLOCK); #define ARRAY_STRINGIFY(m_type) \ - p_output.append("\n" INDENT2 "/// <summary>\n"); \ - p_output.append(INDENT2 "/// Converts this " #m_type " array to a string with brackets.\n"); \ - p_output.append(INDENT2 "/// </summary>\n"); \ - p_output.append(INDENT2 "/// <param name=\"instance\">The " #m_type " array to convert.</param>\n"); \ - p_output.append(INDENT2 "/// <returns>A single string with all items.</returns>\n"); \ - p_output.append(INDENT2 "public static string Stringify(this " #m_type "[] instance)\n"); \ - p_output.append(INDENT2 OPEN_BLOCK); \ - p_output.append(INDENT3 "return \"[\" + instance.Join() + \"]\";\n"); \ - p_output.append(INDENT2 CLOSE_BLOCK); + p_output.append("\n" INDENT1 "/// <summary>\n"); \ + p_output.append(INDENT1 "/// Converts this " #m_type " array to a string with brackets.\n"); \ + p_output.append(INDENT1 "/// </summary>\n"); \ + p_output.append(INDENT1 "/// <param name=\"instance\">The " #m_type " array to convert.</param>\n"); \ + p_output.append(INDENT1 "/// <returns>A single string with all items.</returns>\n"); \ + p_output.append(INDENT1 "public static string Stringify(this " #m_type "[] instance)\n"); \ + p_output.append(OPEN_BLOCK_L1); \ + p_output.append(INDENT2 "return \"[\" + instance.Join() + \"]\";\n"); \ + p_output.append(INDENT1 CLOSE_BLOCK); #define ARRAY_ALL(m_type) \ ARRAY_IS_EMPTY(m_type) \ @@ -925,18 +944,18 @@ void BindingsGenerator::_generate_array_extensions(StringBuilder &p_output) { #undef ARRAY_JOIN #undef ARRAY_STRINGIFY - p_output.append(INDENT1 CLOSE_BLOCK); // End of GD class. - p_output.append(CLOSE_BLOCK); // End of namespace. + p_output.append(CLOSE_BLOCK); // End of GD class. } void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { // Constants (in partial GD class) + p_output.append("namespace " BINDINGS_NAMESPACE ";\n\n"); + p_output.append("\n#pragma warning disable CS1591 // Disable warning: " "'Missing XML comment for publicly visible type or member'\n"); - p_output.append("namespace " BINDINGS_NAMESPACE "\n" OPEN_BLOCK); - p_output.append(INDENT1 "public static partial class " BINDINGS_GLOBAL_SCOPE_CLASS "\n" INDENT1 "{"); + p_output.append("public static partial class " BINDINGS_GLOBAL_SCOPE_CLASS "\n{"); for (const ConstantInterface &iconstant : global_constants) { if (iconstant.const_doc && iconstant.const_doc->description.size()) { @@ -947,12 +966,12 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { p_output.append(MEMBER_BEGIN "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - p_output.append(INDENT2 "/// "); + p_output.append(INDENT1 "/// "); p_output.append(summary_lines[i]); p_output.append("\n"); } - p_output.append(INDENT2 "/// </summary>"); + p_output.append(INDENT1 "/// </summary>"); } } @@ -967,7 +986,7 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { p_output.append("\n"); } - p_output.append(INDENT1 CLOSE_BLOCK); // end of GD class + p_output.append(CLOSE_BLOCK); // end of GD class // Enums @@ -985,21 +1004,21 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { CRASH_COND(enum_class_name != "Variant"); // Hard-coded... - _log("Declaring global enum '%s' inside static class '%s'\n", enum_proxy_name.utf8().get_data(), enum_class_name.utf8().get_data()); + _log("Declaring global enum '%s' inside struct '%s'\n", enum_proxy_name.utf8().get_data(), enum_class_name.utf8().get_data()); - p_output.append("\n" INDENT1 "public static partial class "); + p_output.append("\npublic partial struct "); p_output.append(enum_class_name); - p_output.append("\n" INDENT1 OPEN_BLOCK); + p_output.append("\n" OPEN_BLOCK); } if (ienum.is_flags) { - p_output.append("\n" INDENT1 "[System.Flags]"); + p_output.append("\n[System.Flags]"); } - p_output.append("\n" INDENT1 "public enum "); + p_output.append("\npublic enum "); p_output.append(enum_proxy_name); p_output.append(" : long"); - p_output.append("\n" INDENT1 OPEN_BLOCK); + p_output.append("\n" OPEN_BLOCK); const ConstantInterface &last = ienum.constants.back()->get(); for (const ConstantInterface &iconstant : ienum.constants) { @@ -1008,34 +1027,32 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); if (summary_lines.size()) { - p_output.append(INDENT2 "/// <summary>\n"); + p_output.append(INDENT1 "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - p_output.append(INDENT2 "/// "); + p_output.append(INDENT1 "/// "); p_output.append(summary_lines[i]); p_output.append("\n"); } - p_output.append(INDENT2 "/// </summary>\n"); + p_output.append(INDENT1 "/// </summary>\n"); } } - p_output.append(INDENT2); + p_output.append(INDENT1); p_output.append(iconstant.proxy_name); p_output.append(" = "); p_output.append(itos(iconstant.value)); p_output.append(&iconstant != &last ? ",\n" : "\n"); } - p_output.append(INDENT1 CLOSE_BLOCK); + p_output.append(CLOSE_BLOCK); if (enum_in_static_class) { - p_output.append(INDENT1 CLOSE_BLOCK); + p_output.append(CLOSE_BLOCK); } } - p_output.append(CLOSE_BLOCK); // end of namespace - p_output.append("\n#pragma warning restore CS1591\n"); } @@ -1106,42 +1123,38 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { compile_items.push_back(output_file); } - // Generate sources from compressed files + // Generate native calls StringBuilder cs_icalls_content; + cs_icalls_content.append("namespace " BINDINGS_NAMESPACE ";\n\n"); cs_icalls_content.append("using System;\n" - "using System.Runtime.CompilerServices;\n" + "using System.Diagnostics.CodeAnalysis;\n" + "using System.Runtime.InteropServices;\n" + "using Godot.NativeInterop;\n" "\n"); - cs_icalls_content.append("namespace " BINDINGS_NAMESPACE "\n" OPEN_BLOCK); - cs_icalls_content.append(INDENT1 "internal static class " BINDINGS_CLASS_NATIVECALLS "\n" INDENT1 "{"); + cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"InconsistentNaming\")]\n"); + cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantUnsafeContext\")]\n"); + cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantNameQualifier\")]\n"); + cs_icalls_content.append("[System.Runtime.CompilerServices.SkipLocalsInit]\n"); + cs_icalls_content.append("internal static class " BINDINGS_CLASS_NATIVECALLS "\n{"); cs_icalls_content.append(MEMBER_BEGIN "internal static ulong godot_api_hash = "); - cs_icalls_content.append(String::num_uint64(GDMono::get_singleton()->get_api_core_hash()) + ";\n"); - cs_icalls_content.append(MEMBER_BEGIN "internal static uint bindings_version = "); - cs_icalls_content.append(String::num_uint64(BINDINGS_GENERATOR_VERSION) + ";\n"); - cs_icalls_content.append(MEMBER_BEGIN "internal static uint cs_glue_version = "); - cs_icalls_content.append(String::num_uint64(CS_GLUE_VERSION) + ";\n"); + cs_icalls_content.append(String::num_uint64(ClassDB::get_api_hash(ClassDB::API_CORE)) + ";\n"); -#define ADD_INTERNAL_CALL(m_icall) \ - if (!m_icall.editor_only) { \ - cs_icalls_content.append(MEMBER_BEGIN "[MethodImpl(MethodImplOptions.InternalCall)]\n"); \ - cs_icalls_content.append(INDENT2 "internal static extern "); \ - cs_icalls_content.append(m_icall.im_type_out + " "); \ - cs_icalls_content.append(m_icall.name + "("); \ - cs_icalls_content.append(m_icall.im_sig + ");\n"); \ - } + cs_icalls_content.append(MEMBER_BEGIN "private const int VarArgsSpanThreshold = 10;\n"); - for (const InternalCall &internal_call : core_custom_icalls) { - ADD_INTERNAL_CALL(internal_call); - } - for (const InternalCall &internal_call : method_icalls) { - ADD_INTERNAL_CALL(internal_call); + for (const InternalCall &icall : method_icalls) { + if (icall.editor_only) { + continue; + } + Error err = _generate_cs_native_calls(icall, cs_icalls_content); + if (err != OK) { + return err; + } } -#undef ADD_INTERNAL_CALL - - cs_icalls_content.append(INDENT1 CLOSE_BLOCK CLOSE_BLOCK); + cs_icalls_content.append(CLOSE_BLOCK); String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS ".cs"); @@ -1152,6 +1165,8 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { compile_items.push_back(internal_methods_file); + // Generate GeneratedIncludes.props + StringBuilder includes_props_content; includes_props_content.append("<Project>\n" " <ItemGroup>\n"); @@ -1215,41 +1230,40 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { compile_items.push_back(output_file); } + // Generate native calls + StringBuilder cs_icalls_content; + cs_icalls_content.append("namespace " BINDINGS_NAMESPACE ";\n\n"); cs_icalls_content.append("using System;\n" - "using System.Runtime.CompilerServices;\n" + "using System.Diagnostics.CodeAnalysis;\n" + "using System.Runtime.InteropServices;\n" + "using Godot.NativeInterop;\n" "\n"); - cs_icalls_content.append("namespace " BINDINGS_NAMESPACE "\n" OPEN_BLOCK); - cs_icalls_content.append(INDENT1 "internal static class " BINDINGS_CLASS_NATIVECALLS_EDITOR "\n" INDENT1 OPEN_BLOCK); - - cs_icalls_content.append(INDENT2 "internal static ulong godot_api_hash = "); - cs_icalls_content.append(String::num_uint64(GDMono::get_singleton()->get_api_editor_hash()) + ";\n"); - cs_icalls_content.append(INDENT2 "internal static uint bindings_version = "); - cs_icalls_content.append(String::num_uint64(BINDINGS_GENERATOR_VERSION) + ";\n"); - cs_icalls_content.append(INDENT2 "internal static uint cs_glue_version = "); - cs_icalls_content.append(String::num_uint64(CS_GLUE_VERSION) + ";\n"); - cs_icalls_content.append("\n"); + cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"InconsistentNaming\")]\n"); + cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantUnsafeContext\")]\n"); + cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantNameQualifier\")]\n"); + cs_icalls_content.append("[System.Runtime.CompilerServices.SkipLocalsInit]\n"); + cs_icalls_content.append("internal static class " BINDINGS_CLASS_NATIVECALLS_EDITOR "\n" OPEN_BLOCK); -#define ADD_INTERNAL_CALL(m_icall) \ - if (m_icall.editor_only) { \ - cs_icalls_content.append(INDENT2 "[MethodImpl(MethodImplOptions.InternalCall)]\n"); \ - cs_icalls_content.append(INDENT2 "internal static extern "); \ - cs_icalls_content.append(m_icall.im_type_out + " "); \ - cs_icalls_content.append(m_icall.name + "("); \ - cs_icalls_content.append(m_icall.im_sig + ");\n"); \ - } + cs_icalls_content.append(INDENT1 "internal static ulong godot_api_hash = "); + cs_icalls_content.append(String::num_uint64(ClassDB::get_api_hash(ClassDB::API_EDITOR)) + ";\n"); - for (const InternalCall &internal_call : editor_custom_icalls) { - ADD_INTERNAL_CALL(internal_call); - } - for (const InternalCall &internal_call : method_icalls) { - ADD_INTERNAL_CALL(internal_call); - } + cs_icalls_content.append(MEMBER_BEGIN "private const int VarArgsSpanThreshold = 10;\n"); -#undef ADD_INTERNAL_CALL + cs_icalls_content.append("\n"); + + for (const InternalCall &icall : method_icalls) { + if (!icall.editor_only) { + continue; + } + Error err = _generate_cs_native_calls(icall, cs_icalls_content); + if (err != OK) { + return err; + } + } - cs_icalls_content.append(INDENT1 CLOSE_BLOCK CLOSE_BLOCK); + cs_icalls_content.append(CLOSE_BLOCK); String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS_EDITOR ".cs"); @@ -1260,6 +1274,8 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { compile_items.push_back(internal_methods_file); + // Generate GeneratedIncludes.props + StringBuilder includes_props_content; includes_props_content.append("<Project>\n" " <ItemGroup>\n"); @@ -1299,7 +1315,7 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { // Generate GodotSharp source files - String core_proj_dir = output_dir.plus_file(CORE_API_ASSEMBLY_NAME); + String core_proj_dir = output_dir.path_join(CORE_API_ASSEMBLY_NAME); proj_err = generate_cs_core_project(core_proj_dir); if (proj_err != OK) { @@ -1309,7 +1325,7 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { // Generate GodotSharpEditor source files - String editor_proj_dir = output_dir.plus_file(EDITOR_API_ASSEMBLY_NAME); + String editor_proj_dir = output_dir.path_join(EDITOR_API_ASSEMBLY_NAME); proj_err = generate_cs_editor_project(editor_proj_dir); if (proj_err != OK) { @@ -1343,16 +1359,15 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str CRASH_COND(itype.is_singleton); } - List<InternalCall> &custom_icalls = itype.api_type == ClassDB::API_EDITOR ? editor_custom_icalls : core_custom_icalls; - _log("Generating %s.cs...\n", itype.proxy_name.utf8().get_data()); - String ctor_method(ICALL_PREFIX + itype.proxy_name + "_Ctor"); // Used only for derived types - StringBuilder output; + output.append("namespace " BINDINGS_NAMESPACE ";\n\n"); + output.append("using System;\n"); // IntPtr output.append("using System.Diagnostics;\n"); // DebuggerBrowsable + output.append("using Godot.NativeInterop;\n"); output.append("\n" "#pragma warning disable CS1591 // Disable warning: " @@ -1360,7 +1375,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str "#pragma warning disable CS1573 // Disable warning: " "'Parameter has no matching param tag in the XML comment'\n"); - output.append("\nnamespace " BINDINGS_NAMESPACE "\n" OPEN_BLOCK); + output.append("\n#nullable disable\n"); const DocData::ClassDoc *class_doc = itype.class_doc; @@ -1369,40 +1384,48 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); if (summary_lines.size()) { - output.append(INDENT1 "/// <summary>\n"); + output.append("/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - output.append(INDENT1 "/// "); + output.append("/// "); output.append(summary_lines[i]); output.append("\n"); } - output.append(INDENT1 "/// </summary>\n"); + output.append("/// </summary>\n"); } } - output.append(INDENT1 "public "); + // We generate a `GodotClassName` attribute if the engine class name is not the same as the + // generated C# class name. This allows introspection code to find the name associated with + // the class. If the attribute is not present, the C# class name can be used instead. + if (itype.name != itype.proxy_name) { + output << "[GodotClassName(\"" << itype.name << "\")]\n"; + } + + output.append("public "); if (itype.is_singleton) { output.append("static partial class "); } else { - output.append(itype.is_instantiable ? "partial class " : "abstract partial class "); + // Even if the class is not instantiable, we can't declare it abstract because + // the engine can still instantiate them and return them via the scripting API. + // Example: `SceneTreeTimer` returned from `SceneTree.create_timer`. + // See the reverted commit: ef5672d3f94a7321ed779c922088bb72adbb1521 + output.append("partial class "); } output.append(itype.proxy_name); - if (itype.is_singleton) { - output.append("\n"); - } else if (is_derived_type) { + if (is_derived_type && !itype.is_singleton) { if (obj_types.has(itype.base_name)) { output.append(" : "); output.append(obj_types[itype.base_name].proxy_name); - output.append("\n"); } else { ERR_PRINT("Base type '" + itype.base_name.operator String() + "' does not exist, for class '" + itype.name + "'."); return ERR_INVALID_DATA; } } - output.append(INDENT1 "{"); + output.append("\n{"); // Add constants @@ -1415,12 +1438,12 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output.append(MEMBER_BEGIN "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - output.append(INDENT2 "/// "); + output.append(INDENT1 "/// "); output.append(summary_lines[i]); output.append("\n"); } - output.append(INDENT2 "/// </summary>"); + output.append(INDENT1 "/// </summary>"); } } @@ -1456,26 +1479,26 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); if (summary_lines.size()) { - output.append(INDENT3 "/// <summary>\n"); + output.append(INDENT2 "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - output.append(INDENT3 "/// "); + output.append(INDENT2 "/// "); output.append(summary_lines[i]); output.append("\n"); } - output.append(INDENT3 "/// </summary>\n"); + output.append(INDENT2 "/// </summary>\n"); } } - output.append(INDENT3); + output.append(INDENT2); output.append(iconstant.proxy_name); output.append(" = "); output.append(itos(iconstant.value)); output.append(&iconstant != &last ? ",\n" : "\n"); } - output.append(INDENT2 CLOSE_BLOCK); + output.append(INDENT1 CLOSE_BLOCK); } // Add properties @@ -1491,55 +1514,68 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str // Add the type name and the singleton pointer as static fields output.append(MEMBER_BEGIN "private static Godot.Object singleton;\n"); - output.append(MEMBER_BEGIN "public static Godot.Object Singleton\n" INDENT2 "{\n" INDENT3 - "get\n" INDENT3 "{\n" INDENT4 "if (singleton == null)\n" INDENT5 - "singleton = Engine.GetSingleton(typeof("); - output.append(itype.proxy_name); - output.append(").Name);\n" INDENT4 "return singleton;\n" INDENT3 "}\n" INDENT2 "}\n"); - output.append(MEMBER_BEGIN "private static StringName " BINDINGS_NATIVE_NAME_FIELD " = \""); + output << MEMBER_BEGIN "public static Godot.Object " CS_PROPERTY_SINGLETON "\n" INDENT1 "{\n" + << INDENT2 "get\n" INDENT2 "{\n" INDENT3 "if (singleton == null)\n" + << INDENT4 "singleton = " C_METHOD_ENGINE_GET_SINGLETON "(typeof(" + << itype.proxy_name + << ").Name);\n" INDENT3 "return singleton;\n" INDENT2 "}\n" INDENT1 "}\n"; + + output.append(MEMBER_BEGIN "private static readonly StringName " BINDINGS_NATIVE_NAME_FIELD " = \""); output.append(itype.name); output.append("\";\n"); + } else { + // IMPORTANT: We also generate the static fields for Godot.Object instead of declaring + // them manually in the `Object.base.cs` partial class declaration, because they're + // required by other static fields in this generated partial class declaration. + // Static fields are initialized in order of declaration, but when they're in different + // partial class declarations then it becomes harder to tell (Rider warns about this). - output.append(INDENT2 "internal static IntPtr " BINDINGS_PTR_FIELD " = "); - output.append(itype.api_type == ClassDB::API_EDITOR ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS); - output.append("." ICALL_PREFIX); - output.append(itype.name); - output.append(SINGLETON_ICALL_SUFFIX "();\n"); - } else if (is_derived_type) { - // Add member fields + // Add native name static field + + if (is_derived_type) { + output << MEMBER_BEGIN "private static readonly System.Type CachedType = typeof(" << itype.proxy_name << ");\n"; + } - output.append(MEMBER_BEGIN "private static StringName " BINDINGS_NATIVE_NAME_FIELD " = \""); + output.append(MEMBER_BEGIN "private static readonly StringName " BINDINGS_NATIVE_NAME_FIELD " = \""); output.append(itype.name); output.append("\";\n"); - // Add default constructor if (itype.is_instantiable) { - output.append(MEMBER_BEGIN "public "); - output.append(itype.proxy_name); - output.append("() : this("); - output.append(itype.memory_own ? "true" : "false"); - - // The default constructor may also be called by the engine when instancing existing native objects - // The engine will initialize the pointer field of the managed side before calling the constructor - // This is why we only allocate a new native object from the constructor if the pointer field is not set - output.append(")\n" OPEN_BLOCK_L2 "if (" BINDINGS_PTR_FIELD " == IntPtr.Zero)\n" INDENT4 BINDINGS_PTR_FIELD " = "); - output.append(itype.api_type == ClassDB::API_EDITOR ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS); - output.append("." + ctor_method); - output.append("(this);\n" INDENT3 "_InitializeGodotScriptInstanceInternals();\n" CLOSE_BLOCK_L2); - } else { - // Hide the constructor + // Add native constructor static field + + output << MEMBER_BEGIN << "[DebuggerBrowsable(DebuggerBrowsableState.Never)]\n" + << INDENT1 "private static readonly unsafe delegate* unmanaged<IntPtr> " + << CS_STATIC_FIELD_NATIVE_CTOR " = " ICALL_CLASSDB_GET_CONSTRUCTOR + << "(" BINDINGS_NATIVE_NAME_FIELD ");\n"; + } + + if (is_derived_type) { + // Add default constructor + if (itype.is_instantiable) { + output << MEMBER_BEGIN "public " << itype.proxy_name << "() : this(" + << (itype.memory_own ? "true" : "false") << ")\n" OPEN_BLOCK_L1 + << INDENT2 "unsafe\n" INDENT2 OPEN_BLOCK + << INDENT3 "_ConstructAndInitialize(" CS_STATIC_FIELD_NATIVE_CTOR ", " + << BINDINGS_NATIVE_NAME_FIELD ", CachedType, refCounted: " + << (itype.is_ref_counted ? "true" : "false") << ");\n" + << CLOSE_BLOCK_L2 CLOSE_BLOCK_L1; + } else { + // Hide the constructor + output.append(MEMBER_BEGIN "internal "); + output.append(itype.proxy_name); + output.append("() {}\n"); + } + + // Add.. em.. trick constructor. Sort of. output.append(MEMBER_BEGIN "internal "); output.append(itype.proxy_name); - output.append("() {}\n"); + output.append("(bool " CS_PARAM_MEMORYOWN ") : base(" CS_PARAM_MEMORYOWN ") {}\n"); } - - // Add.. em.. trick constructor. Sort of. - output.append(MEMBER_BEGIN "internal "); - output.append(itype.proxy_name); - output.append("(bool " CS_FIELD_MEMORYOWN ") : base(" CS_FIELD_MEMORYOWN ") {}\n"); } + // Methods + int method_bind_count = 0; for (const MethodInterface &imethod : itype.methods) { Error method_err = _generate_cs_method(itype, imethod, method_bind_count, output); @@ -1547,30 +1583,186 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str "Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'."); } + // Signals + for (const SignalInterface &isignal : itype.signals_) { Error method_err = _generate_cs_signal(itype, isignal, output); ERR_FAIL_COND_V_MSG(method_err != OK, method_err, "Failed to generate signal '" + isignal.name + "' for class '" + itype.name + "'."); } - if (itype.is_singleton) { - InternalCall singleton_icall = InternalCall(itype.api_type, ICALL_PREFIX + itype.name + SINGLETON_ICALL_SUFFIX, "IntPtr"); + // Script members look-up - if (!find_icall_by_name(singleton_icall.name, custom_icalls)) { - custom_icalls.push_back(singleton_icall); + if (!itype.is_singleton && (is_derived_type || itype.has_virtual_methods)) { + // Generate method names cache fields + + for (const MethodInterface &imethod : itype.methods) { + if (!imethod.is_virtual) { + continue; + } + + output << MEMBER_BEGIN "// ReSharper disable once InconsistentNaming\n" + << INDENT1 "[DebuggerBrowsable(DebuggerBrowsableState.Never)]\n" + << INDENT1 "private static readonly StringName " + << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name + << " = \"" << imethod.proxy_name << "\";\n"; } - } - if (is_derived_type && itype.is_instantiable) { - InternalCall ctor_icall = InternalCall(itype.api_type, ctor_method, "IntPtr", itype.proxy_name + " obj"); + // TODO: Only generate HasGodotClassMethod and InvokeGodotClassMethod if there's any method + + // Generate InvokeGodotClassMethod + + output << MEMBER_BEGIN "protected internal " << (is_derived_type ? "override" : "virtual") + << " bool " CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(in godot_string_name method, " + << "NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n" + << INDENT1 "{\n"; + + for (const MethodInterface &imethod : itype.methods) { + if (!imethod.is_virtual) { + continue; + } - if (!find_icall_by_name(ctor_icall.name, custom_icalls)) { - custom_icalls.push_back(ctor_icall); + // We also call HasGodotClassMethod to ensure the method is overridden and avoid calling + // the stub implementation. This solution adds some extra overhead to calls, but it's + // much simpler than other solutions. This won't be a problem once we move to function + // pointers of generated wrappers for each method, as lookup will only happen once. + + // We check both native names (snake_case) and proxy names (PascalCase) + output << INDENT2 "if ((method == " << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name + << " || method == MethodName." << imethod.proxy_name + << ") && argCount == " << itos(imethod.arguments.size()) + << " && " << CS_METHOD_HAS_GODOT_CLASS_METHOD << "((godot_string_name)" + << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << ".NativeValue))\n" + << INDENT2 "{\n"; + + if (imethod.return_type.cname != name_cache.type_void) { + output << INDENT3 "var callRet = "; + } else { + output << INDENT3; + } + + output << imethod.proxy_name << "("; + + for (int i = 0; i < imethod.arguments.size(); i++) { + const ArgumentInterface &iarg = imethod.arguments[i]; + + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found + + if (i != 0) { + output << ", "; + } + + if (arg_type->cname == name_cache.type_Array_generic || arg_type->cname == name_cache.type_Dictionary_generic) { + String arg_cs_type = arg_type->cs_type + _get_generic_type_parameters(*arg_type, iarg.type.generic_type_parameters); + + output << "new " << arg_cs_type << "(" << sformat(arg_type->cs_variant_to_managed, "args[" + itos(i) + "]", arg_type->cs_type, arg_type->name) << ")"; + } else { + output << sformat(arg_type->cs_variant_to_managed, + "args[" + itos(i) + "]", arg_type->cs_type, arg_type->name); + } + } + + output << ");\n"; + + if (imethod.return_type.cname != name_cache.type_void) { + const TypeInterface *return_type = _get_type_or_null(imethod.return_type); + ERR_FAIL_NULL_V(return_type, ERR_BUG); // Return type not found + + output << INDENT3 "ret = " + << sformat(return_type->cs_managed_to_variant, "callRet", return_type->cs_type, return_type->name) + << ";\n" + << INDENT3 "return true;\n"; + } else { + output << INDENT3 "ret = default;\n" + << INDENT3 "return true;\n"; + } + + output << INDENT2 "}\n"; + } + + if (is_derived_type) { + output << INDENT2 "return base." CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(method, args, argCount, out ret);\n"; + } else { + output << INDENT2 "ret = default;\n" + << INDENT2 "return false;\n"; + } + + output << INDENT1 "}\n"; + + // Generate HasGodotClassMethod + + output << MEMBER_BEGIN "protected internal " << (is_derived_type ? "override" : "virtual") + << " bool " CS_METHOD_HAS_GODOT_CLASS_METHOD "(in godot_string_name method)\n" + << INDENT1 "{\n"; + + for (const MethodInterface &imethod : itype.methods) { + if (!imethod.is_virtual) { + continue; + } + + // We check for native names (snake_case). If we detect one, we call HasGodotClassMethod + // again, but this time with the respective proxy name (PascalCase). It's the job of + // user derived classes to override the method and check for those. Our C# source + // generators take care of generating those override methods. + output << INDENT2 "if (method == MethodName." << imethod.proxy_name + << ")\n" INDENT2 "{\n" + << INDENT3 "if (" CS_METHOD_HAS_GODOT_CLASS_METHOD "(" + << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name + << ".NativeValue.DangerousSelfRef))\n" INDENT3 "{\n" + << INDENT4 "return true;\n" + << INDENT3 "}\n" INDENT2 "}\n"; } + + if (is_derived_type) { + output << INDENT2 "return base." CS_METHOD_HAS_GODOT_CLASS_METHOD "(method);\n"; + } else { + output << INDENT2 "return false;\n"; + } + + output << INDENT1 "}\n"; } - output.append(INDENT1 CLOSE_BLOCK /* class */ - CLOSE_BLOCK /* namespace */); + //Generate StringName for all class members + bool is_inherit = !itype.is_singleton && obj_types.has(itype.base_name); + //PropertyName + if (is_inherit) { + output << MEMBER_BEGIN "public new class PropertyName : " << obj_types[itype.base_name].proxy_name << ".PropertyName"; + } else { + output << MEMBER_BEGIN "public class PropertyName"; + } + output << "\n" + << INDENT1 "{\n"; + for (const PropertyInterface &iprop : itype.properties) { + output << INDENT2 "public static readonly StringName " << iprop.proxy_name << " = \"" << iprop.cname << "\";\n"; + } + output << INDENT1 "}\n"; + //MethodName + if (is_inherit) { + output << MEMBER_BEGIN "public new class MethodName : " << obj_types[itype.base_name].proxy_name << ".MethodName"; + } else { + output << MEMBER_BEGIN "public class MethodName"; + } + output << "\n" + << INDENT1 "{\n"; + for (const MethodInterface &imethod : itype.methods) { + output << INDENT2 "public static readonly StringName " << imethod.proxy_name << " = \"" << imethod.cname << "\";\n"; + } + output << INDENT1 "}\n"; + //SignalName + if (is_inherit) { + output << MEMBER_BEGIN "public new class SignalName : " << obj_types[itype.base_name].proxy_name << ".SignalName"; + } else { + output << MEMBER_BEGIN "public class SignalName"; + } + output << "\n" + << INDENT1 "{\n"; + for (const SignalInterface &isignal : itype.signals_) { + output << INDENT2 "public static readonly StringName " << isignal.proxy_name << " = \"" << isignal.cname << "\";\n"; + } + output << INDENT1 "}\n"; + + output.append(CLOSE_BLOCK /* class */); output.append("\n" "#pragma warning restore CS1591\n" @@ -1649,12 +1841,12 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte p_output.append(MEMBER_BEGIN "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - p_output.append(INDENT2 "/// "); + p_output.append(INDENT1 "/// "); p_output.append(summary_lines[i]); p_output.append("\n"); } - p_output.append(INDENT2 "/// </summary>"); + p_output.append(INDENT1 "/// </summary>"); } } @@ -1669,15 +1861,15 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte p_output.append(prop_cs_type); p_output.append(" "); p_output.append(p_iprop.proxy_name); - p_output.append("\n" INDENT2 OPEN_BLOCK); + p_output.append("\n" OPEN_BLOCK_L1); if (getter) { - p_output.append(INDENT3 "get\n" + p_output.append(INDENT2 "get\n" // TODO Remove this once we make accessor methods private/internal (they will no longer be marked as obsolete after that) "#pragma warning disable CS0618 // Disable warning about obsolete method\n" - OPEN_BLOCK_L3); + OPEN_BLOCK_L2 INDENT3); p_output.append("return "); p_output.append(getter->proxy_name + "("); @@ -1694,19 +1886,19 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte } p_output.append(");\n" - CLOSE_BLOCK_L3 + CLOSE_BLOCK_L2 // TODO Remove this once we make accessor methods private/internal (they will no longer be marked as obsolete after that) "#pragma warning restore CS0618\n"); } if (setter) { - p_output.append(INDENT3 "set\n" + p_output.append(INDENT2 "set\n" // TODO Remove this once we make accessor methods private/internal (they will no longer be marked as obsolete after that) "#pragma warning disable CS0618 // Disable warning about obsolete method\n" - OPEN_BLOCK_L3); + OPEN_BLOCK_L2 INDENT3); p_output.append(setter->proxy_name + "("); if (p_iprop.index != -1) { @@ -1722,19 +1914,20 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte } p_output.append("value);\n" - CLOSE_BLOCK_L3 + CLOSE_BLOCK_L2 // TODO Remove this once we make accessor methods private/internal (they will no longer be marked as obsolete after that) "#pragma warning restore CS0618\n"); } - p_output.append(CLOSE_BLOCK_L2); + p_output.append(CLOSE_BLOCK_L1); return OK; } Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, int &p_method_bind_count, StringBuilder &p_output) { - const TypeInterface *return_type = _get_type_or_placeholder(p_imethod.return_type); + const TypeInterface *return_type = _get_type_or_null(p_imethod.return_type); + ERR_FAIL_NULL_V(return_type, ERR_BUG); // Return type not found ERR_FAIL_COND_V_MSG(return_type->is_singleton, ERR_BUG, "Method return type is a singleton: '" + p_itype.name + "." + p_imethod.name + "'."); @@ -1745,14 +1938,21 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf "' from the editor API. Core API cannot have dependencies on the editor API."); } - String method_bind_field = "__method_bind_" + itos(p_method_bind_count); + String method_bind_field = CS_STATIC_FIELD_METHOD_BIND_PREFIX + itos(p_method_bind_count); String arguments_sig; - String cs_in_statements; + StringBuilder cs_in_statements; + bool cs_in_expr_is_unsafe = false; String icall_params = method_bind_field; + if (!p_imethod.is_static) { - icall_params += ", " + sformat(p_itype.cs_in, "this"); + if (p_itype.cs_in.size()) { + cs_in_statements << sformat(p_itype.cs_in, p_itype.c_type, "this", + String(), String(), String(), INDENT2); + } + + icall_params += ", " + sformat(p_itype.cs_in_expr, "this"); } StringBuilder default_args_doc; @@ -1760,7 +1960,8 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf // Retrieve information from the arguments const ArgumentInterface &first = p_imethod.arguments.front()->get(); for (const ArgumentInterface &iarg : p_imethod.arguments) { - const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type); + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found ERR_FAIL_COND_V_MSG(arg_type->is_singleton, ERR_BUG, "Argument type is a singleton: '" + iarg.name + "' of method '" + p_itype.name + "." + p_imethod.name + "'."); @@ -1813,27 +2014,23 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf if (iarg.default_argument.size() && iarg.def_param_mode != ArgumentInterface::CONSTANT) { // The default value of an argument must be constant. Otherwise we make it Nullable and do the following: // Type arg_in = arg.HasValue ? arg.Value : <non-const default value>; - String arg_in = iarg.name; - arg_in += "_in"; + String arg_or_defval_local = iarg.name; + arg_or_defval_local += "OrDefVal"; - cs_in_statements += arg_cs_type; - cs_in_statements += " "; - cs_in_statements += arg_in; - cs_in_statements += " = "; - cs_in_statements += iarg.name; + cs_in_statements << INDENT2 << arg_cs_type << " " << arg_or_defval_local << " = " << iarg.name; if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { - cs_in_statements += ".HasValue ? "; + cs_in_statements << ".HasValue ? "; } else { - cs_in_statements += " != null ? "; + cs_in_statements << " != null ? "; } - cs_in_statements += iarg.name; + cs_in_statements << iarg.name; if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { - cs_in_statements += ".Value : "; + cs_in_statements << ".Value : "; } else { - cs_in_statements += " : "; + cs_in_statements << " : "; } String cs_type = arg_cs_type; @@ -1843,10 +2040,18 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf String def_arg = sformat(iarg.default_argument, cs_type); - cs_in_statements += def_arg; - cs_in_statements += ";\n" INDENT3; + cs_in_statements << def_arg << ";\n"; + + if (arg_type->cs_in.size()) { + cs_in_statements << sformat(arg_type->cs_in, arg_type->c_type, arg_or_defval_local, + String(), String(), String(), INDENT2); + } - icall_params += arg_type->cs_in.is_empty() ? arg_in : sformat(arg_type->cs_in, arg_in); + if (arg_type->cs_in_expr.is_empty()) { + icall_params += arg_or_defval_local; + } else { + icall_params += sformat(arg_type->cs_in_expr, arg_or_defval_local, arg_type->c_type); + } // Apparently the name attribute must not include the @ String param_tag_name = iarg.name.begins_with("@") ? iarg.name.substr(1, iarg.name.length()) : iarg.name; @@ -1855,18 +2060,32 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf default_args_doc.append(MEMBER_BEGIN "/// <param name=\"" + param_tag_name + "\">If the parameter is null, then the default value is <c>" + param_def_arg + "</c>.</param>"); } else { - icall_params += arg_type->cs_in.is_empty() ? iarg.name : sformat(arg_type->cs_in, iarg.name); + if (arg_type->cs_in.size()) { + cs_in_statements << sformat(arg_type->cs_in, arg_type->c_type, iarg.name, + String(), String(), String(), INDENT2); + } + + icall_params += arg_type->cs_in_expr.is_empty() ? iarg.name : sformat(arg_type->cs_in_expr, iarg.name, arg_type->c_type); } + + cs_in_expr_is_unsafe |= arg_type->cs_in_expr_is_unsafe; } // Generate method { if (!p_imethod.is_virtual && !p_imethod.requires_object_call) { - p_output.append(MEMBER_BEGIN "[DebuggerBrowsable(DebuggerBrowsableState.Never)]" MEMBER_BEGIN "private static readonly IntPtr "); - p_output.append(method_bind_field); - p_output.append(" = Object." ICALL_GET_METHODBIND "(" BINDINGS_NATIVE_NAME_FIELD ", \""); - p_output.append(p_imethod.name); - p_output.append("\");\n"); + p_output << MEMBER_BEGIN "[DebuggerBrowsable(DebuggerBrowsableState.Never)]\n" + << INDENT1 "private static readonly IntPtr " << method_bind_field << " = "; + + if (p_itype.is_singleton) { + // Singletons are static classes. They don't derive Godot.Object, + // so we need to specify the type to call the static method. + p_output << "Object."; + } + + p_output << ICALL_CLASSDB_GET_METHOD "(" BINDINGS_NATIVE_NAME_FIELD ", MethodName." + << p_imethod.proxy_name + << ");\n"; } if (p_imethod.method_doc && p_imethod.method_doc->description.size()) { @@ -1877,12 +2096,12 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf p_output.append(MEMBER_BEGIN "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - p_output.append(INDENT2 "/// "); + p_output.append(INDENT1 "/// "); p_output.append(summary_lines[i]); p_output.append("\n"); } - p_output.append(INDENT2 "/// </summary>"); + p_output.append(INDENT1 "/// </summary>"); } } @@ -1890,16 +2109,6 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf p_output.append(default_args_doc.as_string()); } - if (!p_imethod.is_internal) { - // TODO: This alone adds ~0.2 MB of bloat to the core API assembly. It would be - // better to generate a table in the C++ glue instead. That way the strings wouldn't - // add that much extra bloat as they're already used in engine code. Also, it would - // probably be much faster than looking up the attributes when fetching methods. - p_output.append(MEMBER_BEGIN "[GodotMethod(\""); - p_output.append(p_imethod.name); - p_output.append("\")]"); - } - if (p_imethod.is_deprecated) { if (p_imethod.deprecation_message.is_empty()) { WARN_PRINT("An empty deprecation message is discouraged. Method: '" + p_imethod.proxy_name + "'."); @@ -1919,21 +2128,23 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf p_output.append("virtual "); } + if (cs_in_expr_is_unsafe) { + p_output.append("unsafe "); + } + String return_cs_type = return_type->cs_type + _get_generic_type_parameters(*return_type, p_imethod.return_type.generic_type_parameters); p_output.append(return_cs_type + " "); p_output.append(p_imethod.proxy_name + "("); - p_output.append(arguments_sig + ")\n" OPEN_BLOCK_L2); + p_output.append(arguments_sig + ")\n" OPEN_BLOCK_L1); if (p_imethod.is_virtual) { // Godot virtual method must be overridden, therefore we return a default value by default. if (return_type->cname == name_cache.type_void) { - p_output.append("return;\n" CLOSE_BLOCK_L2); + p_output.append(CLOSE_BLOCK_L1); } else { - p_output.append("return default("); - p_output.append(return_cs_type); - p_output.append(");\n" CLOSE_BLOCK_L2); + p_output.append(INDENT2 "return default;\n" CLOSE_BLOCK_L1); } return OK; // Won't increment method bind count @@ -1942,7 +2153,7 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf if (p_imethod.requires_object_call) { // Fallback to Godot's object.Call(string, params) - p_output.append(CS_METHOD_CALL "(\""); + p_output.append(INDENT2 CS_METHOD_CALL "(\""); p_output.append(p_imethod.name); p_output.append("\""); @@ -1951,7 +2162,7 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf p_output.append(iarg.name); } - p_output.append(");\n" CLOSE_BLOCK_L2); + p_output.append(");\n" CLOSE_BLOCK_L1); return OK; // Won't increment method bind count } @@ -1965,20 +2176,21 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf im_call += "."; im_call += im_icall->name; - if (p_imethod.arguments.size()) { - p_output.append(cs_in_statements); + if (p_imethod.arguments.size() && cs_in_statements.get_string_length() > 0) { + p_output.append(cs_in_statements.as_string()); } if (return_type->cname == name_cache.type_void) { - p_output.append(im_call + "(" + icall_params + ");\n"); + p_output << INDENT2 << im_call << "(" << icall_params << ");\n"; } else if (return_type->cs_out.is_empty()) { - p_output.append("return " + im_call + "(" + icall_params + ");\n"); + p_output << INDENT2 "return " << im_call << "(" << icall_params << ");\n"; } else { - p_output.append(sformat(return_type->cs_out, im_call, icall_params, return_cs_type, return_type->im_type_out)); + p_output.append(sformat(return_type->cs_out, im_call, icall_params, + return_cs_type, return_type->c_type_out, String(), INDENT2)); p_output.append("\n"); } - p_output.append(CLOSE_BLOCK_L2); + p_output.append(CLOSE_BLOCK_L1); } p_method_bind_count++; @@ -1992,7 +2204,8 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf // Retrieve information from the arguments const ArgumentInterface &first = p_isignal.arguments.front()->get(); for (const ArgumentInterface &iarg : p_isignal.arguments) { - const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type); + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found ERR_FAIL_COND_V_MSG(arg_type->is_singleton, ERR_BUG, "Argument type is a singleton: '" + iarg.name + "' of signal '" + p_itype.name + "." + p_isignal.name + "'."); @@ -2024,12 +2237,12 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(MEMBER_BEGIN "/// <summary>\n"); for (int i = 0; i < summary_lines.size(); i++) { - p_output.append(INDENT2 "/// "); + p_output.append(INDENT1 "/// "); p_output.append(summary_lines[i]); p_output.append("\n"); } - p_output.append(INDENT2 "/// </summary>"); + p_output.append(INDENT1 "/// </summary>"); } } @@ -2044,7 +2257,7 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf } String delegate_name = p_isignal.proxy_name; - delegate_name += "Handler"; // Delegate name is [SignalName]Handler + delegate_name += "EventHandler"; // Delegate name is [SignalName]EventHandler // Generate delegate p_output.append(MEMBER_BEGIN "public delegate void "); @@ -2057,15 +2270,8 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf // Could we assume the StringName instance of signal name will never be freed (it's stored in ClassDB) before the managed world is unloaded? // If so, we could store the pointer we get from `data_unique_pointer()` instead of allocating StringName here. - // Cached signal name (StringName) - p_output.append(MEMBER_BEGIN "[DebuggerBrowsable(DebuggerBrowsableState.Never)]" MEMBER_BEGIN "private static StringName __signal_name_"); - p_output.append(p_isignal.name); - p_output.append(" = \""); - p_output.append(p_isignal.name); - p_output.append("\";\n"); - // Generate event - p_output.append(MEMBER_BEGIN "[Signal]" MEMBER_BEGIN "public "); + p_output.append(MEMBER_BEGIN "public "); if (p_itype.is_singleton) { p_output.append("static "); @@ -2075,413 +2281,241 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(delegate_name); p_output.append(" "); p_output.append(p_isignal.proxy_name); - p_output.append("\n" OPEN_BLOCK_L2); + p_output.append("\n" OPEN_BLOCK_L1 INDENT2); if (p_itype.is_singleton) { - p_output.append("add => Singleton.Connect(__signal_name_"); + p_output.append("add => " CS_PROPERTY_SINGLETON ".Connect(SignalName."); } else { - p_output.append("add => Connect(__signal_name_"); + p_output.append("add => Connect(SignalName."); } - p_output.append(p_isignal.name); + p_output.append(p_isignal.proxy_name); p_output.append(", new Callable(value));\n"); if (p_itype.is_singleton) { - p_output.append(INDENT3 "remove => Singleton.Disconnect(__signal_name_"); + p_output.append(INDENT2 "remove => " CS_PROPERTY_SINGLETON ".Disconnect(SignalName."); } else { - p_output.append(INDENT3 "remove => Disconnect(__signal_name_"); + p_output.append(INDENT2 "remove => Disconnect(SignalName."); } - p_output.append(p_isignal.name); + p_output.append(p_isignal.proxy_name); p_output.append(", new Callable(value));\n"); - p_output.append(CLOSE_BLOCK_L2); - } - - return OK; -} - -Error BindingsGenerator::generate_glue(const String &p_output_dir) { - ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED); - - bool dir_exists = DirAccess::exists(p_output_dir); - ERR_FAIL_COND_V_MSG(!dir_exists, ERR_FILE_BAD_PATH, "The output directory does not exist."); - - StringBuilder output; - - output.append("/* THIS FILE IS GENERATED DO NOT EDIT */\n"); - output.append("#include \"" GLUE_HEADER_FILE "\"\n"); - output.append("\n#ifdef MONO_GLUE_ENABLED\n"); - - generated_icall_funcs.clear(); - - for (const KeyValue<StringName, TypeInterface> &type_elem : obj_types) { - const TypeInterface &itype = type_elem.value; - - bool is_derived_type = itype.base_name != StringName(); - - if (!is_derived_type) { - // Some Object assertions - CRASH_COND(itype.cname != name_cache.type_Object); - CRASH_COND(!itype.is_instantiable); - CRASH_COND(itype.api_type != ClassDB::API_CORE); - CRASH_COND(itype.is_singleton); - } - - List<InternalCall> &custom_icalls = itype.api_type == ClassDB::API_EDITOR ? editor_custom_icalls : core_custom_icalls; - - OS::get_singleton()->print("Generating %s...\n", itype.name.utf8().get_data()); - - String ctor_method(ICALL_PREFIX + itype.proxy_name + "_Ctor"); // Used only for derived types - - for (const MethodInterface &imethod : itype.methods) { - Error method_err = _generate_glue_method(itype, imethod, output); - ERR_FAIL_COND_V_MSG(method_err != OK, method_err, - "Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'."); - } - - if (itype.is_singleton) { - String singleton_icall_name = ICALL_PREFIX + itype.name + SINGLETON_ICALL_SUFFIX; - InternalCall singleton_icall = InternalCall(itype.api_type, singleton_icall_name, "IntPtr"); - - if (!find_icall_by_name(singleton_icall.name, custom_icalls)) { - custom_icalls.push_back(singleton_icall); - } - - output.append("Object* "); - output.append(singleton_icall_name); - output.append("() " OPEN_BLOCK "\treturn Engine::get_singleton()->get_singleton_object(\""); - output.append(itype.proxy_name); - output.append("\");\n" CLOSE_BLOCK "\n"); - } - - if (is_derived_type && itype.is_instantiable) { - InternalCall ctor_icall = InternalCall(itype.api_type, ctor_method, "IntPtr", itype.proxy_name + " obj"); - - if (!find_icall_by_name(ctor_icall.name, custom_icalls)) { - custom_icalls.push_back(ctor_icall); - } - - output.append("Object* "); - output.append(ctor_method); - output.append("(MonoObject* obj) " OPEN_BLOCK - "\t" C_MACRO_OBJECT_CONSTRUCT "(instance, \""); - output.append(itype.name); - output.append("\");\n" - "\t" C_METHOD_TIE_MANAGED_TO_UNMANAGED "(obj, instance);\n" - "\treturn instance;\n" CLOSE_BLOCK "\n"); - } - } - - output.append("namespace GodotSharpBindings\n" OPEN_BLOCK "\n"); - - output.append("uint64_t get_core_api_hash() { return "); - output.append(String::num_uint64(GDMono::get_singleton()->get_api_core_hash()) + "U; }\n"); - - output.append("#ifdef TOOLS_ENABLED\n" - "uint64_t get_editor_api_hash() { return "); - output.append(String::num_uint64(GDMono::get_singleton()->get_api_editor_hash()) + "U; }\n"); - output.append("#endif // TOOLS_ENABLED\n"); - - output.append("uint32_t get_bindings_version() { return "); - output.append(String::num_uint64(BINDINGS_GENERATOR_VERSION) + "; }\n"); - - output.append("uint32_t get_cs_glue_version() { return "); - output.append(String::num_uint64(CS_GLUE_VERSION) + "; }\n"); - - output.append("\nvoid register_generated_icalls() " OPEN_BLOCK); - output.append("\tgodot_register_glue_header_icalls();\n"); - -#define ADD_INTERNAL_CALL_REGISTRATION(m_icall) \ - { \ - output.append("\tGDMonoUtils::add_internal_call("); \ - output.append("\"" BINDINGS_NAMESPACE "."); \ - output.append(m_icall.editor_only ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS); \ - output.append("::"); \ - output.append(m_icall.name); \ - output.append("\", "); \ - output.append(m_icall.name); \ - output.append(");\n"); \ - } - - bool tools_sequence = false; - for (const InternalCall &internal_call : core_custom_icalls) { - if (tools_sequence) { - if (!internal_call.editor_only) { - tools_sequence = false; - output.append("#endif\n"); - } - } else { - if (internal_call.editor_only) { - output.append("#ifdef TOOLS_ENABLED\n"); - tools_sequence = true; - } - } - - ADD_INTERNAL_CALL_REGISTRATION(internal_call); - } - - if (tools_sequence) { - tools_sequence = false; - output.append("#endif\n"); - } - - output.append("#ifdef TOOLS_ENABLED\n"); - for (const InternalCall &internal_call : editor_custom_icalls) { - ADD_INTERNAL_CALL_REGISTRATION(internal_call); - } - output.append("#endif // TOOLS_ENABLED\n"); - - for (const InternalCall &internal_call : method_icalls) { - if (tools_sequence) { - if (!internal_call.editor_only) { - tools_sequence = false; - output.append("#endif\n"); - } - } else { - if (internal_call.editor_only) { - output.append("#ifdef TOOLS_ENABLED\n"); - tools_sequence = true; - } - } - - ADD_INTERNAL_CALL_REGISTRATION(internal_call); - } - - if (tools_sequence) { - tools_sequence = false; - output.append("#endif\n"); - } - -#undef ADD_INTERNAL_CALL_REGISTRATION - - output.append(CLOSE_BLOCK "\n} // namespace GodotSharpBindings\n"); - - output.append("\n#endif // MONO_GLUE_ENABLED\n"); - - Error save_err = _save_file(path::join(p_output_dir, "mono_glue.gen.cpp"), output); - if (save_err != OK) { - return save_err; + p_output.append(CLOSE_BLOCK_L1); } - OS::get_singleton()->print("Mono glue generated successfully\n"); - - return OK; -} - -uint32_t BindingsGenerator::get_version() { - return BINDINGS_GENERATOR_VERSION; -} - -Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p_content) { - Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(file.is_null(), ERR_FILE_CANT_WRITE, "Cannot open file: '" + p_path + "'."); - - file->store_string(p_content.as_string()); - return OK; } -Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, StringBuilder &p_output) { - if (p_imethod.is_virtual) { - return OK; // Ignore - } +Error BindingsGenerator::_generate_cs_native_calls(const InternalCall &p_icall, StringBuilder &r_output) { + bool ret_void = p_icall.return_type.cname == name_cache.type_void; - bool ret_void = p_imethod.return_type.cname == name_cache.type_void; + const TypeInterface *return_type = _get_type_or_null(p_icall.return_type); + ERR_FAIL_NULL_V(return_type, ERR_BUG); // Return type not found - const TypeInterface *return_type = _get_type_or_placeholder(p_imethod.return_type); + StringBuilder c_func_sig; + StringBuilder c_in_statements; + StringBuilder c_args_var_content; - String argc_str = itos(p_imethod.arguments.size()); + c_func_sig << "IntPtr " CS_PARAM_METHODBIND; - String c_func_sig = "MethodBind* " CS_PARAM_METHODBIND; - if (!p_imethod.is_static) { - c_func_sig += ", " + p_itype.c_type_in + " " CS_PARAM_INSTANCE; + if (!p_icall.is_static) { + c_func_sig += ", IntPtr " CS_PARAM_INSTANCE; } - String c_in_statements; - String c_args_var_content; // Get arguments information int i = 0; - for (const ArgumentInterface &iarg : p_imethod.arguments) { - const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type); + for (const TypeReference &arg_type_ref : p_icall.argument_types) { + const TypeInterface *arg_type = _get_type_or_null(arg_type_ref); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Return type not found String c_param_name = "arg" + itos(i + 1); - if (p_imethod.is_vararg) { - if (i < p_imethod.arguments.size() - 1) { - c_in_statements += sformat(arg_type->c_in.size() ? arg_type->c_in : TypeInterface::DEFAULT_VARARG_C_IN, "Variant", c_param_name); - c_in_statements += "\t" C_LOCAL_PTRCALL_ARGS ".set("; - c_in_statements += itos(i); - c_in_statements += sformat(", &%s_in);\n", c_param_name); + if (p_icall.is_vararg) { + if (i < p_icall.get_arguments_count() - 1) { + String c_in_vararg = arg_type->c_in_vararg; + + if (arg_type->is_object_type) { + c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromGodotObjectPtr(%1);\n"; + } + + ERR_FAIL_COND_V_MSG(c_in_vararg.is_empty(), ERR_BUG, + "VarArg support not implemented for parameter type: " + arg_type->name); + + c_in_statements + << sformat(c_in_vararg, return_type->c_type, c_param_name, + String(), String(), String(), INDENT3) + << INDENT3 C_LOCAL_PTRCALL_ARGS "[" << itos(i) + << "] = new IntPtr(&" << c_param_name << "_in);\n"; } } else { if (i > 0) { - c_args_var_content += ", "; + c_args_var_content << ", "; } if (arg_type->c_in.size()) { - c_in_statements += sformat(arg_type->c_in, arg_type->c_type, c_param_name); + c_in_statements << sformat(arg_type->c_in, arg_type->c_type, c_param_name, + String(), String(), String(), INDENT2); } - c_args_var_content += sformat(arg_type->c_arg_in, c_param_name); + c_args_var_content << sformat(arg_type->c_arg_in, c_param_name); } - c_func_sig += ", "; - c_func_sig += arg_type->c_type_in; - c_func_sig += " "; - c_func_sig += c_param_name; + c_func_sig << ", " << arg_type->c_type_in << " " << c_param_name; i++; } - if (return_type->ret_as_byref_arg) { - c_func_sig += ", "; - c_func_sig += return_type->c_type_in; - c_func_sig += " "; - c_func_sig += "arg_ret"; - - i++; - } + String icall_method = p_icall.name; - HashMap<const MethodInterface *, const InternalCall *>::ConstIterator match = method_icalls_map.find(&p_imethod); - ERR_FAIL_NULL_V(match, ERR_BUG); + // Generate icall function - const InternalCall *im_icall = match->value; - String icall_method = im_icall->name; + r_output << MEMBER_BEGIN "internal static unsafe " << (ret_void ? "void" : return_type->c_type_out) << " " + << icall_method << "(" << c_func_sig.as_string() << ") " OPEN_BLOCK; - if (!generated_icall_funcs.find(im_icall)) { - generated_icall_funcs.push_back(im_icall); + if (!ret_void && (!p_icall.is_vararg || return_type->cname != name_cache.type_Variant)) { + String ptrcall_return_type; + String initialization; - if (im_icall->editor_only) { - p_output.append("#ifdef TOOLS_ENABLED\n"); + if (return_type->is_object_type) { + ptrcall_return_type = return_type->is_ref_counted ? "godot_ref" : return_type->c_type; + initialization = " = default"; + } else { + ptrcall_return_type = return_type->c_type; } - // Generate icall function - - p_output.append((ret_void || return_type->ret_as_byref_arg) ? "void " : return_type->c_type_out + " "); - p_output.append(icall_method); - p_output.append("("); - p_output.append(c_func_sig); - p_output.append(") " OPEN_BLOCK); + r_output << INDENT2; - if (!ret_void) { - String ptrcall_return_type; - String initialization; + if (return_type->is_ref_counted || return_type->c_type_is_disposable_struct) { + r_output << "using "; - if (p_imethod.is_vararg && return_type->cname != name_cache.type_Variant) { - // VarArg methods always return Variant, but there are some cases in which MethodInfo provides - // a specific return type. We trust this information is valid. We need a temporary local to keep - // the Variant alive until the method returns. Otherwise, if the returned Variant holds a RefPtr, - // it could be deleted too early. This is the case with GDScript.new() which returns OBJECT. - // Alternatively, we could just return Variant, but that would result in a worse API. - p_output.append("\tVariant " C_LOCAL_VARARG_RET ";\n"); + if (initialization.is_empty()) { + initialization = " = default"; } + } else if (return_type->c_ret_needs_default_initialization) { + initialization = " = default"; + } - if (return_type->is_object_type) { - ptrcall_return_type = return_type->is_ref_counted ? "Ref<RefCounted>" : return_type->c_type; - initialization = return_type->is_ref_counted ? "" : " = nullptr"; - } else { - ptrcall_return_type = return_type->c_type; - } + r_output << ptrcall_return_type << " " C_LOCAL_RET << initialization << ";\n"; + } - p_output.append("\t" + ptrcall_return_type); - p_output.append(" " C_LOCAL_RET); - p_output.append(initialization + ";\n"); + if (!p_icall.is_static) { + r_output << INDENT2 "if (" CS_PARAM_INSTANCE " == IntPtr.Zero)\n" + << INDENT3 "throw new ArgumentNullException(nameof(" CS_PARAM_INSTANCE "));\n"; + } - String fail_ret = return_type->c_type_out.ends_with("*") && !return_type->ret_as_byref_arg ? "nullptr" : return_type->c_type_out + "()"; + String argc_str = itos(p_icall.get_arguments_count()); - if (!p_imethod.is_static) { - if (return_type->ret_as_byref_arg) { - p_output.append("\tif (" CS_PARAM_INSTANCE " == nullptr) { *arg_ret = "); - p_output.append(fail_ret); - p_output.append("; ERR_FAIL_MSG(\"Parameter ' " CS_PARAM_INSTANCE " ' is null.\"); }\n"); - } else { - p_output.append("\tERR_FAIL_NULL_V(" CS_PARAM_INSTANCE ", "); - p_output.append(fail_ret); - p_output.append(");\n"); - } - } - } else { - if (!p_imethod.is_static) { - p_output.append("\tERR_FAIL_NULL(" CS_PARAM_INSTANCE ");\n"); - } - } - - if (p_imethod.arguments.size()) { - if (p_imethod.is_vararg) { - String vararg_arg = "arg" + argc_str; - String real_argc_str = itos(p_imethod.arguments.size() - 1); // Arguments count without vararg - - p_output.append("\tint vararg_length = mono_array_length("); - p_output.append(vararg_arg); - p_output.append(");\n\tint total_length = "); - p_output.append(real_argc_str); - p_output.append(" + vararg_length;\n" - "\tArgumentsVector<Variant> varargs(vararg_length);\n" - "\tArgumentsVector<const Variant *> " C_LOCAL_PTRCALL_ARGS "(total_length);\n"); - p_output.append(c_in_statements); - p_output.append("\tfor (int i = 0; i < vararg_length; i++) " OPEN_BLOCK - "\t\tMonoObject* elem = mono_array_get("); - p_output.append(vararg_arg); - p_output.append(", MonoObject*, i);\n" - "\t\tvarargs.set(i, GDMonoMarshal::mono_object_to_variant(elem));\n" - "\t\t" C_LOCAL_PTRCALL_ARGS ".set("); - p_output.append(real_argc_str); - p_output.append(" + i, &varargs.get(i));\n\t" CLOSE_BLOCK); - } else { - p_output.append(c_in_statements); - p_output.append("\tconst void* " C_LOCAL_PTRCALL_ARGS "["); - p_output.append(argc_str + "] = { "); - p_output.append(c_args_var_content + " };\n"); - } - } + auto generate_call_and_return_stmts = [&](const char *base_indent) { + if (p_icall.is_vararg) { + // MethodBind Call + r_output << base_indent; - if (p_imethod.is_vararg) { - p_output.append("\tCallable::CallError vcall_error;\n\t"); + // VarArg methods always return Variant, but there are some cases in which MethodInfo provides + // a specific return type. We trust this information is valid. We need a temporary local to keep + // the Variant alive until the method returns. Otherwise, if the returned Variant holds a RefPtr, + // it could be deleted too early. This is the case with GDScript.new() which returns OBJECT. + // Alternatively, we could just return Variant, but that would result in a worse API. if (!ret_void) { - // See the comment on the C_LOCAL_VARARG_RET declaration if (return_type->cname != name_cache.type_Variant) { - p_output.append(C_LOCAL_VARARG_RET " = "); + r_output << "using godot_variant " << C_LOCAL_VARARG_RET " = "; } else { - p_output.append(C_LOCAL_RET " = "); + r_output << "using godot_variant " << C_LOCAL_RET " = "; } } - p_output.append(CS_PARAM_METHODBIND "->call("); - p_output.append(p_imethod.is_static ? "nullptr" : CS_PARAM_INSTANCE); - p_output.append(", "); - p_output.append(p_imethod.arguments.size() ? C_LOCAL_PTRCALL_ARGS ".ptr()" : "nullptr"); - p_output.append(", total_length, vcall_error);\n"); + r_output << C_CLASS_NATIVE_FUNCS ".godotsharp_method_bind_call(" + << CS_PARAM_METHODBIND ", " << (p_icall.is_static ? "IntPtr.Zero" : CS_PARAM_INSTANCE) + << ", " << (p_icall.get_arguments_count() ? "(godot_variant**)" C_LOCAL_PTRCALL_ARGS : "null") + << ", total_length, out _);\n"; if (!ret_void) { - // See the comment on the C_LOCAL_VARARG_RET declaration if (return_type->cname != name_cache.type_Variant) { - p_output.append("\t" C_LOCAL_RET " = " C_LOCAL_VARARG_RET ";\n"); + if (return_type->cname == name_cache.enum_Error) { + r_output << base_indent << C_LOCAL_RET " = VariantUtils.ConvertToInt64(" C_LOCAL_VARARG_RET ");\n"; + } else { + // TODO: Use something similar to c_in_vararg (see usage above, with error if not implemented) + CRASH_NOW_MSG("Custom VarArg return type not implemented: " + return_type->name); + r_output << base_indent << C_LOCAL_RET " = " C_LOCAL_VARARG_RET ";\n"; + } } } } else { - p_output.append("\t" CS_PARAM_METHODBIND "->ptrcall("); - p_output.append(p_imethod.is_static ? "nullptr" : CS_PARAM_INSTANCE); - p_output.append(", "); - p_output.append(p_imethod.arguments.size() ? C_LOCAL_PTRCALL_ARGS ", " : "nullptr, "); - p_output.append(!ret_void ? "&" C_LOCAL_RET ");\n" : "nullptr);\n"); + // MethodBind PtrCall + r_output << base_indent << C_CLASS_NATIVE_FUNCS ".godotsharp_method_bind_ptrcall(" + << CS_PARAM_METHODBIND ", " << (p_icall.is_static ? "IntPtr.Zero" : CS_PARAM_INSTANCE) + << ", " << (p_icall.get_arguments_count() ? C_LOCAL_PTRCALL_ARGS : "null") + << ", " << (!ret_void ? "&" C_LOCAL_RET ");\n" : "null);\n"); } + // Return statement + if (!ret_void) { if (return_type->c_out.is_empty()) { - p_output.append("\treturn " C_LOCAL_RET ";\n"); - } else if (return_type->ret_as_byref_arg) { - p_output.append(sformat(return_type->c_out, return_type->c_type_out, C_LOCAL_RET, return_type->name, "arg_ret")); + r_output << base_indent << "return " C_LOCAL_RET ";\n"; } else { - p_output.append(sformat(return_type->c_out, return_type->c_type_out, C_LOCAL_RET, return_type->name)); + r_output << sformat(return_type->c_out, return_type->c_type_out, C_LOCAL_RET, + return_type->name, String(), String(), base_indent); } } + }; + + if (p_icall.get_arguments_count()) { + if (p_icall.is_vararg) { + String vararg_arg = "arg" + argc_str; + String real_argc_str = itos(p_icall.get_arguments_count() - 1); // Arguments count without vararg - p_output.append(CLOSE_BLOCK "\n"); + p_icall.get_arguments_count(); - if (im_icall->editor_only) { - p_output.append("#endif // TOOLS_ENABLED\n"); + r_output << INDENT2 "int vararg_length = " << vararg_arg << ".Length;\n" + << INDENT2 "int total_length = " << real_argc_str << " + vararg_length;\n"; + + r_output << INDENT2 "Span<godot_variant.movable> varargs_span = vararg_length <= VarArgsSpanThreshold ?\n" + << INDENT3 "stackalloc godot_variant.movable[VarArgsSpanThreshold].Cleared() :\n" + << INDENT3 "new godot_variant.movable[vararg_length];\n"; + + r_output << INDENT2 "Span<IntPtr> " C_LOCAL_PTRCALL_ARGS "_span = total_length <= VarArgsSpanThreshold ?\n" + << INDENT3 "stackalloc IntPtr[VarArgsSpanThreshold] :\n" + << INDENT3 "new IntPtr[total_length];\n"; + + r_output << INDENT2 "using var variantSpanDisposer = new VariantSpanDisposer(varargs_span);\n"; + + r_output << INDENT2 "fixed (godot_variant.movable* varargs = &MemoryMarshal.GetReference(varargs_span))\n" + << INDENT2 "fixed (IntPtr* " C_LOCAL_PTRCALL_ARGS " = " + "&MemoryMarshal.GetReference(" C_LOCAL_PTRCALL_ARGS "_span))\n" + << OPEN_BLOCK_L2; + + r_output << c_in_statements.as_string(); + + r_output << INDENT3 "for (int i = 0; i < vararg_length; i++) " OPEN_BLOCK + << INDENT4 "varargs[i] = " << vararg_arg << "[i].NativeVar;\n" + << INDENT4 C_LOCAL_PTRCALL_ARGS "[" << real_argc_str << " + i] = new IntPtr(&varargs[i]);\n" + << CLOSE_BLOCK_L3; + + generate_call_and_return_stmts(INDENT3); + + r_output << CLOSE_BLOCK_L2; + } else { + r_output << c_in_statements.as_string(); + + r_output << INDENT2 "void** " C_LOCAL_PTRCALL_ARGS " = stackalloc void*[" + << argc_str << "] { " << c_args_var_content.as_string() << " };\n"; + + generate_call_and_return_stmts(INDENT2); } + } else { + generate_call_and_return_stmts(INDENT2); } + r_output << CLOSE_BLOCK_L1; + + return OK; +} + +Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p_content) { + Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(file.is_null(), ERR_FILE_CANT_WRITE, "Cannot open file: '" + p_path + "'."); + + file->store_string(p_content.as_string()); + return OK; } @@ -2514,27 +2548,6 @@ const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_null(con return nullptr; } -const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_placeholder(const TypeReference &p_typeref) { - const TypeInterface *found = _get_type_or_null(p_typeref); - - if (found) { - return found; - } - - ERR_PRINT(String() + "Type not found. Creating placeholder: '" + p_typeref.cname.operator String() + "'."); - - HashMap<StringName, TypeInterface>::ConstIterator match = placeholder_types.find(p_typeref.cname); - - if (match) { - return &match->value; - } - - TypeInterface placeholder; - TypeInterface::create_placeholder_type(placeholder, p_typeref.cname); - - return &placeholder_types.insert(placeholder.cname, placeholder)->value; -} - const String BindingsGenerator::_get_generic_type_parameters(const TypeInterface &p_itype, const List<TypeReference> &p_generic_type_parameters) { if (p_generic_type_parameters.is_empty()) { return ""; @@ -2548,7 +2561,8 @@ const String BindingsGenerator::_get_generic_type_parameters(const TypeInterface int i = 0; String params = "<"; for (const TypeReference ¶m_type : p_generic_type_parameters) { - const TypeInterface *param_itype = _get_type_or_placeholder(param_type); + const TypeInterface *param_itype = _get_type_or_null(param_type); + ERR_FAIL_NULL_V(param_itype, ""); ERR_FAIL_COND_V_MSG(param_itype->is_singleton, "", "Generic type parameter is a singleton: '" + param_itype->name + "'."); @@ -2666,8 +2680,6 @@ bool BindingsGenerator::_arg_default_value_is_assignable_to_type(const Variant & case Variant::RECT2: case Variant::VECTOR3: case Variant::RID: - case Variant::ARRAY: - case Variant::DICTIONARY: case Variant::PACKED_BYTE_ARRAY: case Variant::PACKED_INT32_ARRAY: case Variant::PACKED_INT64_ARRAY: @@ -2680,6 +2692,10 @@ bool BindingsGenerator::_arg_default_value_is_assignable_to_type(const Variant & case Variant::CALLABLE: case Variant::SIGNAL: return p_arg_type.name == Variant::get_type_name(p_val.get_type()); + case Variant::ARRAY: + return p_arg_type.name == Variant::get_type_name(p_val.get_type()) || p_arg_type.cname == name_cache.type_Array_generic; + case Variant::DICTIONARY: + return p_arg_type.name == Variant::get_type_name(p_val.get_type()) || p_arg_type.cname == name_cache.type_Dictionary_generic; case Variant::OBJECT: return p_arg_type.is_object_type; case Variant::VECTOR2I: @@ -2744,18 +2760,27 @@ bool BindingsGenerator::_populate_object_type_interfaces() { itype.is_ref_counted = ClassDB::is_parent_class(type_cname, name_cache.type_RefCounted); itype.memory_own = itype.is_ref_counted; - itype.c_out = "\treturn "; + itype.cs_variant_to_managed = "(%1)VariantUtils.ConvertToGodotObject(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromGodotObject(%0)"; + + itype.c_out = "%5return "; itype.c_out += C_METHOD_UNMANAGED_GET_MANAGED; - itype.c_out += itype.is_ref_counted ? "(%1.ptr());\n" : "(%1);\n"; + itype.c_out += itype.is_ref_counted ? "(%1.Reference);\n" : "(%1);\n"; + + itype.cs_type = itype.proxy_name; + + if (itype.is_singleton) { + itype.cs_in_expr = "Object." CS_STATIC_METHOD_GETINSTANCE "(" CS_PROPERTY_SINGLETON ")"; + } else { + itype.cs_in_expr = "Object." CS_STATIC_METHOD_GETINSTANCE "(%0)"; + } - itype.cs_in = itype.is_singleton ? BINDINGS_PTR_FIELD : "Object." CS_SMETHOD_GETINSTANCE "(%0)"; + itype.cs_out = "%5return (%2)%0(%1);"; - itype.c_type = "Object*"; + itype.c_arg_in = "(void*)%s"; + itype.c_type = "IntPtr"; itype.c_type_in = itype.c_type; - itype.c_type_out = "MonoObject*"; - itype.cs_type = itype.proxy_name; - itype.im_type_in = "IntPtr"; - itype.im_type_out = itype.proxy_name; + itype.c_type_out = "Object"; // Populate properties @@ -2846,6 +2871,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() { if (method_info.flags & METHOD_FLAG_VIRTUAL) { imethod.is_virtual = true; + itype.has_virtual_methods = true; } PropertyInfo return_info = method_info.return_val; @@ -2887,7 +2913,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() { String() + "Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'." + " Are you returning a reference type by pointer? Method: '" + itype.name + "." + imethod.name + "'."); } else if (return_info.type == Variant::ARRAY && return_info.hint == PROPERTY_HINT_ARRAY_TYPE) { - imethod.return_type.cname = Variant::get_type_name(return_info.type); + imethod.return_type.cname = Variant::get_type_name(return_info.type) + "_@generic"; imethod.return_type.generic_type_parameters.push_back(TypeReference(return_info.hint_string)); } else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) { imethod.return_type.cname = return_info.hint_string; @@ -2919,7 +2945,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (arginfo.class_name != StringName()) { iarg.type.cname = arginfo.class_name; } else if (arginfo.type == Variant::ARRAY && arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) { - iarg.type.cname = Variant::get_type_name(arginfo.type); + iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic"; iarg.type.generic_type_parameters.push_back(TypeReference(arginfo.hint_string)); } else if (arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) { iarg.type.cname = arginfo.hint_string; @@ -2985,7 +3011,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() { ERR_FAIL_COND_V_MSG(itype.find_property_by_name(imethod.cname), false, "Method name conflicts with property: '" + itype.name + "." + imethod.name + "'."); - // Classes starting with an underscore are ignored unless they're used as a property setter or getter + // Methods starting with an underscore are ignored unless they're used as a property setter or getter if (!imethod.is_virtual && imethod.name[0] == '_') { for (const PropertyInterface &iprop : itype.properties) { if (iprop.setter == imethod.name || iprop.getter == imethod.name) { @@ -3027,7 +3053,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (arginfo.class_name != StringName()) { iarg.type.cname = arginfo.class_name; } else if (arginfo.type == Variant::ARRAY && arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) { - iarg.type.cname = Variant::get_type_name(arginfo.type); + iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic"; iarg.type.generic_type_parameters.push_back(TypeReference(arginfo.hint_string)); } else if (arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) { iarg.type.cname = arginfo.hint_string; @@ -3132,6 +3158,8 @@ bool BindingsGenerator::_populate_object_type_interfaces() { enum_itype.cname = StringName(enum_itype.name); enum_itype.proxy_name = itype.proxy_name + "." + enum_proxy_name; TypeInterface::postsetup_enum_type(enum_itype); + enum_itype.cs_variant_to_managed = "(%1)VariantUtils.ConvertToInt32(%0)"; + enum_itype.cs_managed_to_variant = "VariantUtils.CreateFromInt((int)%0)"; enum_types.insert(enum_itype.cname, enum_itype); } @@ -3169,7 +3197,7 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar switch (p_val.get_type()) { case Variant::NIL: // Either Object type or Variant - r_iarg.default_argument = "null"; + r_iarg.default_argument = "default"; break; // Atomic types case Variant::BOOL: @@ -3189,8 +3217,13 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar case Variant::STRING_NAME: case Variant::NODE_PATH: if (r_iarg.type.cname == name_cache.type_StringName || r_iarg.type.cname == name_cache.type_NodePath) { - r_iarg.default_argument = "(%s)\"" + r_iarg.default_argument + "\""; - r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; + if (r_iarg.default_argument.length() > 0) { + r_iarg.default_argument = "(%s)\"" + r_iarg.default_argument + "\""; + r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; + } else { + // No need for a special `in` statement to change `null` to `""`. Marshaling takes care of this already. + r_iarg.default_argument = "null"; + } } else { CRASH_COND(r_iarg.type.cname != name_cache.type_String); r_iarg.default_argument = "\"" + r_iarg.default_argument + "\""; @@ -3236,8 +3269,11 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar r_iarg.default_argument = "null"; break; case Variant::DICTIONARY: - r_iarg.default_argument = "new %s()"; - r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; + ERR_FAIL_COND_V_MSG(!p_val.operator Dictionary().is_empty(), false, + "Default value of type 'Dictionary' must be an empty dictionary."); + // The [cs_in] expression already interprets null values as empty dictionaries. + r_iarg.default_argument = "null"; + r_iarg.def_param_mode = ArgumentInterface::CONSTANT; break; case Variant::RID: ERR_FAIL_COND_V_MSG(r_iarg.type.cname != name_cache.type_RID, false, @@ -3246,11 +3282,14 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar ERR_FAIL_COND_V_MSG(!p_val.is_zero(), false, "Parameter of type '" + String(r_iarg.type.cname) + "' can only have null/zero as the default value."); - r_iarg.default_argument = "null"; + r_iarg.default_argument = "default"; break; case Variant::ARRAY: - r_iarg.default_argument = "new %s { }"; - r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; + ERR_FAIL_COND_V_MSG(!p_val.operator Array().is_empty(), false, + "Default value of type 'Array' must be an empty array."); + // The [cs_in] expression already interprets null values as empty arrays. + r_iarg.default_argument = "null"; + r_iarg.def_param_mode = ArgumentInterface::CONSTANT; break; case Variant::PACKED_BYTE_ARRAY: case Variant::PACKED_INT32_ARRAY: @@ -3325,12 +3364,12 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar r_iarg.default_argument = "default"; break; default: - CRASH_NOW_MSG("Unexpected Variant type: " + itos(p_val.get_type())); + ERR_FAIL_V_MSG(false, "Unexpected Variant type: " + itos(p_val.get_type())); break; } - if (r_iarg.def_param_mode == ArgumentInterface::CONSTANT && r_iarg.type.cname == name_cache.type_Variant && r_iarg.default_argument != "null") { - r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; + if (r_iarg.def_param_mode == ArgumentInterface::CONSTANT && r_iarg.type.cname == name_cache.type_Variant && r_iarg.default_argument != "default") { + r_iarg.def_param_mode = ArgumentInterface::NULLABLE_VAL; } return true; @@ -3344,16 +3383,12 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { #define INSERT_STRUCT_TYPE(m_type) \ { \ itype = TypeInterface::create_value_type(String(#m_type)); \ - itype.c_in = "\t%0 %1_in = MARSHALLED_IN(" #m_type ", %1);\n"; \ - itype.c_out = "\t*%3 = MARSHALLED_OUT(" #m_type ", %1);\n"; \ - itype.c_arg_in = "&%s_in"; \ - itype.c_type_in = "GDMonoMarshal::M_" #m_type "*"; \ - itype.c_type_out = "GDMonoMarshal::M_" #m_type; \ - itype.cs_in = "ref %s"; \ - /* in cs_out, im_type_out (%3) includes the 'out ' part */ \ - itype.cs_out = "%0(%1, %3 argRet); return argRet;"; \ - itype.im_type_out = "out " + itype.cs_type; \ - itype.ret_as_byref_arg = true; \ + itype.c_type_in = #m_type "*"; \ + itype.c_type_out = itype.cs_type; \ + itype.cs_in_expr = "&%0"; \ + itype.cs_in_expr_is_unsafe = true; \ + itype.cs_variant_to_managed = "VariantUtils.ConvertTo%2(%0)"; \ + itype.cs_managed_to_variant = "VariantUtils.CreateFrom%2(%0)"; \ builtin_types.insert(itype.cname, itype); \ } @@ -3370,54 +3405,60 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { INSERT_STRUCT_TYPE(AABB) INSERT_STRUCT_TYPE(Color) INSERT_STRUCT_TYPE(Plane) + INSERT_STRUCT_TYPE(Vector4) + INSERT_STRUCT_TYPE(Vector4i) + INSERT_STRUCT_TYPE(Projection) #undef INSERT_STRUCT_TYPE // bool itype = TypeInterface::create_value_type(String("bool")); - { - // MonoBoolean <---> bool - itype.c_in = "\t%0 %1_in = (%0)%1;\n"; - itype.c_out = "\treturn (%0)%1;\n"; - itype.c_type = "bool"; - itype.c_type_in = "MonoBoolean"; - itype.c_type_out = itype.c_type_in; - itype.c_arg_in = "&%s_in"; - } - itype.im_type_in = itype.name; - itype.im_type_out = itype.name; + itype.cs_in_expr = "%0.ToGodotBool()"; + itype.cs_out = "%5return %0(%1).ToBool();"; + itype.c_type = "godot_bool"; + itype.c_type_in = itype.c_type; + itype.c_type_out = itype.c_type; + itype.c_arg_in = "&%s"; + itype.c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromBool(%1);\n"; + itype.cs_variant_to_managed = "VariantUtils.ConvertToBool(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromBool(%0)"; builtin_types.insert(itype.cname, itype); // Integer types { // C interface for 'uint32_t' is the same as that of enums. Remember to apply // any of the changes done here to 'TypeInterface::postsetup_enum_type' as well. -#define INSERT_INT_TYPE(m_name, m_c_type_in_out, m_c_type) \ - { \ - itype = TypeInterface::create_value_type(String(m_name)); \ - { \ - itype.c_in = "\t%0 %1_in = (%0)%1;\n"; \ - itype.c_out = "\treturn (%0)%1;\n"; \ - itype.c_type = #m_c_type; \ - itype.c_arg_in = "&%s_in"; \ - } \ - itype.c_type_in = #m_c_type_in_out; \ - itype.c_type_out = itype.c_type_in; \ - itype.im_type_in = itype.name; \ - itype.im_type_out = itype.name; \ - builtin_types.insert(itype.cname, itype); \ +#define INSERT_INT_TYPE(m_name, m_int_struct_name) \ + { \ + itype = TypeInterface::create_value_type(String(m_name)); \ + if (itype.name != "long" && itype.name != "ulong") { \ + itype.c_in = "%5%0 %1_in = %1;\n"; \ + itype.c_out = "%5return (%0)%1;\n"; \ + itype.c_type = "long"; \ + itype.c_arg_in = "&%s_in"; \ + } else { \ + itype.c_arg_in = "&%s"; \ + } \ + itype.c_type_in = itype.name; \ + itype.c_type_out = itype.name; \ + itype.c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromInt(%1);\n"; \ + itype.cs_variant_to_managed = "VariantUtils.ConvertTo" m_int_struct_name "(%0)"; \ + itype.cs_managed_to_variant = "VariantUtils.CreateFromInt(%0)"; \ + builtin_types.insert(itype.cname, itype); \ } // The expected type for all integers in ptrcall is 'int64_t', so that's what we use for 'c_type' - INSERT_INT_TYPE("sbyte", int8_t, int64_t); - INSERT_INT_TYPE("short", int16_t, int64_t); - INSERT_INT_TYPE("int", int32_t, int64_t); - INSERT_INT_TYPE("long", int64_t, int64_t); - INSERT_INT_TYPE("byte", uint8_t, int64_t); - INSERT_INT_TYPE("ushort", uint16_t, int64_t); - INSERT_INT_TYPE("uint", uint32_t, int64_t); - INSERT_INT_TYPE("ulong", uint64_t, int64_t); + INSERT_INT_TYPE("sbyte", "Int8"); + INSERT_INT_TYPE("short", "Int16"); + INSERT_INT_TYPE("int", "Int32"); + INSERT_INT_TYPE("long", "Int64"); + INSERT_INT_TYPE("byte", "UInt8"); + INSERT_INT_TYPE("ushort", "UInt16"); + INSERT_INT_TYPE("uint", "UInt32"); + INSERT_INT_TYPE("ulong", "UInt64"); + +#undef INSERT_INT_TYPE } // Floating point types @@ -3427,18 +3468,19 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "float"; itype.cname = itype.name; itype.proxy_name = "float"; + itype.cs_type = itype.proxy_name; { // The expected type for 'float' in ptrcall is 'double' - itype.c_in = "\t%0 %1_in = (%0)%1;\n"; - itype.c_out = "\treturn (%0)%1;\n"; + itype.c_in = "%5%0 %1_in = %1;\n"; + itype.c_out = "%5return (%0)%1;\n"; itype.c_type = "double"; - itype.c_type_in = "float"; - itype.c_type_out = "float"; itype.c_arg_in = "&%s_in"; } - itype.cs_type = itype.proxy_name; - itype.im_type_in = itype.proxy_name; - itype.im_type_out = itype.proxy_name; + itype.c_type_in = itype.proxy_name; + itype.c_type_out = itype.proxy_name; + itype.c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromFloat(%1);\n"; + itype.cs_variant_to_managed = "VariantUtils.ConvertToFloat32(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromFloat(%0)"; builtin_types.insert(itype.cname, itype); // double @@ -3446,15 +3488,14 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "double"; itype.cname = itype.name; itype.proxy_name = "double"; - { - itype.c_type = "double"; - itype.c_type_in = "double"; - itype.c_type_out = "double"; - itype.c_arg_in = "&%s"; - } itype.cs_type = itype.proxy_name; - itype.im_type_in = itype.proxy_name; - itype.im_type_out = itype.proxy_name; + itype.c_type = "double"; + itype.c_arg_in = "&%s"; + itype.c_type_in = itype.proxy_name; + itype.c_type_out = itype.proxy_name; + itype.c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromFloat(%1);\n"; + itype.cs_variant_to_managed = "VariantUtils.ConvertToFloat64(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromFloat(%0)"; builtin_types.insert(itype.cname, itype); } @@ -3463,15 +3504,17 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "String"; itype.cname = itype.name; itype.proxy_name = "string"; - itype.c_in = "\t%0 %1_in = " C_METHOD_MONOSTR_TO_GODOT "(%1);\n"; - itype.c_out = "\treturn " C_METHOD_MONOSTR_FROM_GODOT "(%1);\n"; - itype.c_arg_in = "&%s_in"; - itype.c_type = itype.name; - itype.c_type_in = "MonoString*"; - itype.c_type_out = "MonoString*"; itype.cs_type = itype.proxy_name; - itype.im_type_in = itype.proxy_name; - itype.im_type_out = itype.proxy_name; + itype.c_in = "%5using %0 %1_in = " C_METHOD_MONOSTR_TO_GODOT "(%1);\n"; + itype.c_out = "%5return " C_METHOD_MONOSTR_FROM_GODOT "(%1);\n"; + itype.c_arg_in = "&%s_in"; + itype.c_type = "godot_string"; + itype.c_type_in = itype.cs_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = true; + itype.c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromString(%1);\n"; + itype.cs_variant_to_managed = "VariantUtils.ConvertToStringObject(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromString(%0)"; builtin_types.insert(itype.cname, itype); // StringName @@ -3479,17 +3522,19 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "StringName"; itype.cname = itype.name; itype.proxy_name = "StringName"; - itype.c_in = "\t%0 %1_in = %1 ? *%1 : StringName();\n"; - itype.c_out = "\treturn memnew(StringName(%1));\n"; - itype.c_arg_in = "&%s_in"; - itype.c_type = itype.name; - itype.c_type_in = itype.c_type + "*"; - itype.c_type_out = itype.c_type + "*"; itype.cs_type = itype.proxy_name; - itype.cs_in = "StringName." CS_SMETHOD_GETINSTANCE "(%0)"; - itype.cs_out = "return new %2(%0(%1));"; - itype.im_type_in = "IntPtr"; - itype.im_type_out = "IntPtr"; + itype.cs_in_expr = "(%1)(%0?.NativeValue ?? default)"; + // Cannot pass null StringName to ptrcall + itype.c_out = "%5return %0.CreateTakingOwnershipOfDisposableValue(%1);\n"; + itype.c_arg_in = "&%s"; + itype.c_type = "godot_string_name"; + itype.c_type_in = itype.c_type; + itype.c_type_out = itype.cs_type; + itype.c_in_vararg = "%5using godot_variant %1_in = VariantUtils.CreateFromStringName(%1);\n"; + itype.c_type_is_disposable_struct = false; // [c_out] takes ownership + itype.c_ret_needs_default_initialization = true; + itype.cs_variant_to_managed = "VariantUtils.ConvertToStringNameObject(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromStringName(%0)"; builtin_types.insert(itype.cname, itype); // NodePath @@ -3497,15 +3542,18 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "NodePath"; itype.cname = itype.name; itype.proxy_name = "NodePath"; - itype.c_out = "\treturn memnew(NodePath(%1));\n"; - itype.c_type = itype.name; - itype.c_type_in = itype.c_type + "*"; - itype.c_type_out = itype.c_type + "*"; itype.cs_type = itype.proxy_name; - itype.cs_in = "NodePath." CS_SMETHOD_GETINSTANCE "(%0)"; - itype.cs_out = "return new %2(%0(%1));"; - itype.im_type_in = "IntPtr"; - itype.im_type_out = "IntPtr"; + itype.cs_in_expr = "(%1)(%0?.NativeValue ?? default)"; + // Cannot pass null NodePath to ptrcall + itype.c_out = "%5return %0.CreateTakingOwnershipOfDisposableValue(%1);\n"; + itype.c_arg_in = "&%s"; + itype.c_type = "godot_node_path"; + itype.c_type_in = itype.c_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = false; // [c_out] takes ownership + itype.c_ret_needs_default_initialization = true; + itype.cs_variant_to_managed = "VariantUtils.ConvertToNodePathObject(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromNodePath(%0)"; builtin_types.insert(itype.cname, itype); // RID @@ -3513,45 +3561,45 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "RID"; itype.cname = itype.name; itype.proxy_name = "RID"; - itype.c_out = "\treturn memnew(RID(%1));\n"; - itype.c_type = itype.name; - itype.c_type_in = itype.c_type + "*"; - itype.c_type_out = itype.c_type + "*"; itype.cs_type = itype.proxy_name; - itype.cs_in = "RID." CS_SMETHOD_GETINSTANCE "(%0)"; - itype.cs_out = "return new %2(%0(%1));"; - itype.im_type_in = "IntPtr"; - itype.im_type_out = "IntPtr"; + itype.c_arg_in = "&%s"; + itype.c_type = itype.cs_type; + itype.c_type_in = itype.c_type; + itype.c_type_out = itype.c_type; + itype.cs_variant_to_managed = "VariantUtils.ConvertToRID(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromRID(%0)"; builtin_types.insert(itype.cname, itype); // Variant itype = TypeInterface(); itype.name = "Variant"; itype.cname = itype.name; - itype.proxy_name = "object"; - itype.c_in = "\t%0 %1_in = " C_METHOD_MANAGED_TO_VARIANT "(%1);\n"; - itype.c_out = "\treturn " C_METHOD_MANAGED_FROM_VARIANT "(%1);\n"; - itype.c_arg_in = "&%s_in"; - itype.c_type = itype.name; - itype.c_type_in = "MonoObject*"; - itype.c_type_out = "MonoObject*"; + itype.proxy_name = "Variant"; itype.cs_type = itype.proxy_name; - itype.im_type_in = "object"; - itype.im_type_out = itype.proxy_name; + itype.c_in = "%5%0 %1_in = (%0)%1.NativeVar;\n"; + itype.c_out = "%5return Variant.CreateTakingOwnershipOfDisposableValue(%1);\n"; + itype.c_arg_in = "&%s_in"; + itype.c_type = "godot_variant"; + itype.c_type_in = itype.cs_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = false; // [c_out] takes ownership + itype.c_ret_needs_default_initialization = true; + itype.cs_variant_to_managed = "Variant.CreateCopyingBorrowed(%0)"; + itype.cs_managed_to_variant = "%0.CopyNativeVariant()"; builtin_types.insert(itype.cname, itype); // Callable itype = TypeInterface::create_value_type(String("Callable")); - itype.c_in = "\t%0 %1_in = " C_METHOD_MANAGED_TO_CALLABLE "(*%1);\n"; - itype.c_out = "\t*%3 = " C_METHOD_MANAGED_FROM_CALLABLE "(%1);\n"; + itype.cs_in_expr = "%0"; + itype.c_in = "%5using %0 %1_in = " C_METHOD_MANAGED_TO_CALLABLE "(in %1);\n"; + itype.c_out = "%5return " C_METHOD_MANAGED_FROM_CALLABLE "(in %1);\n"; itype.c_arg_in = "&%s_in"; - itype.c_type_in = "GDMonoMarshal::M_Callable*"; - itype.c_type_out = "GDMonoMarshal::M_Callable"; - itype.cs_in = "ref %s"; - /* in cs_out, im_type_out (%3) includes the 'out ' part */ - itype.cs_out = "%0(%1, %3 argRet); return argRet;"; - itype.im_type_out = "out " + itype.cs_type; - itype.ret_as_byref_arg = true; + itype.c_type = "godot_callable"; + itype.c_type_in = "in " + itype.cs_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = true; + itype.cs_variant_to_managed = "VariantUtils.ConvertToCallableManaged(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromCallable(%0)"; builtin_types.insert(itype.cname, itype); // Signal @@ -3559,66 +3607,65 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "Signal"; itype.cname = itype.name; itype.proxy_name = "SignalInfo"; - itype.c_in = "\t%0 %1_in = " C_METHOD_MANAGED_TO_SIGNAL "(*%1);\n"; - itype.c_out = "\t*%3 = " C_METHOD_MANAGED_FROM_SIGNAL "(%1);\n"; - itype.c_arg_in = "&%s_in"; - itype.c_type = itype.name; - itype.c_type_in = "GDMonoMarshal::M_SignalInfo*"; - itype.c_type_out = "GDMonoMarshal::M_SignalInfo"; - itype.cs_in = "ref %s"; - /* in cs_out, im_type_out (%3) includes the 'out ' part */ - itype.cs_out = "%0(%1, %3 argRet); return argRet;"; itype.cs_type = itype.proxy_name; - itype.im_type_in = "ref " + itype.cs_type; - itype.im_type_out = "out " + itype.cs_type; - itype.ret_as_byref_arg = true; + itype.cs_in_expr = "%0"; + itype.c_in = "%5using %0 %1_in = " C_METHOD_MANAGED_TO_SIGNAL "(in %1);\n"; + itype.c_out = "%5return " C_METHOD_MANAGED_FROM_SIGNAL "(&%1);\n"; + itype.c_arg_in = "&%s_in"; + itype.c_type = "godot_signal"; + itype.c_type_in = "in " + itype.cs_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = true; + itype.cs_variant_to_managed = "VariantUtils.ConvertToSignalInfo(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromSignalInfo(%0)"; builtin_types.insert(itype.cname, itype); // VarArg (fictitious type to represent variable arguments) itype = TypeInterface(); itype.name = "VarArg"; itype.cname = itype.name; - itype.proxy_name = "object[]"; - itype.c_in = "\t%0 %1_in = " C_METHOD_MONOARRAY_TO(Array) "(%1);\n"; + itype.proxy_name = "Variant[]"; + itype.cs_type = "params Variant[]"; + itype.cs_in_expr = "%0 ?? Array.Empty<Variant>()"; + // c_type, c_in and c_arg_in are hard-coded in the generator. + // c_out and c_type_out are not applicable to VarArg. itype.c_arg_in = "&%s_in"; - itype.c_type = "Array"; - itype.c_type_in = "MonoArray*"; - itype.cs_type = "params object[]"; - itype.im_type_in = "object[]"; + itype.c_type_in = "Variant[]"; builtin_types.insert(itype.cname, itype); -#define INSERT_ARRAY_FULL(m_name, m_type, m_proxy_t) \ - { \ - itype = TypeInterface(); \ - itype.name = #m_name; \ - itype.cname = itype.name; \ - itype.proxy_name = #m_proxy_t "[]"; \ - itype.c_in = "\t%0 %1_in = " C_METHOD_MONOARRAY_TO(m_type) "(%1);\n"; \ - itype.c_out = "\treturn " C_METHOD_MONOARRAY_FROM(m_type) "(%1);\n"; \ - itype.c_arg_in = "&%s_in"; \ - itype.c_type = #m_type; \ - itype.c_type_in = "MonoArray*"; \ - itype.c_type_out = "MonoArray*"; \ - itype.cs_type = itype.proxy_name; \ - itype.im_type_in = itype.proxy_name; \ - itype.im_type_out = itype.proxy_name; \ - builtin_types.insert(itype.name, itype); \ +#define INSERT_ARRAY_FULL(m_name, m_type, m_managed_type, m_proxy_t) \ + { \ + itype = TypeInterface(); \ + itype.name = #m_name; \ + itype.cname = itype.name; \ + itype.proxy_name = #m_proxy_t "[]"; \ + itype.cs_type = itype.proxy_name; \ + itype.c_in = "%5using %0 %1_in = " C_METHOD_MONOARRAY_TO(m_type) "(%1);\n"; \ + itype.c_out = "%5return " C_METHOD_MONOARRAY_FROM(m_type) "(%1);\n"; \ + itype.c_arg_in = "&%s_in"; \ + itype.c_type = #m_managed_type; \ + itype.c_type_in = itype.proxy_name; \ + itype.c_type_out = itype.proxy_name; \ + itype.c_type_is_disposable_struct = true; \ + itype.cs_variant_to_managed = "VariantUtils.ConvertAs%2ToSystemArray(%0)"; \ + itype.cs_managed_to_variant = "VariantUtils.CreateFrom%2(%0)"; \ + builtin_types.insert(itype.name, itype); \ } -#define INSERT_ARRAY(m_type, m_proxy_t) INSERT_ARRAY_FULL(m_type, m_type, m_proxy_t) +#define INSERT_ARRAY(m_type, m_managed_type, m_proxy_t) INSERT_ARRAY_FULL(m_type, m_type, m_managed_type, m_proxy_t) - INSERT_ARRAY(PackedInt32Array, int); - INSERT_ARRAY(PackedInt64Array, long); - INSERT_ARRAY_FULL(PackedByteArray, PackedByteArray, byte); + INSERT_ARRAY(PackedInt32Array, godot_packed_int32_array, int); + INSERT_ARRAY(PackedInt64Array, godot_packed_int64_array, long); + INSERT_ARRAY_FULL(PackedByteArray, PackedByteArray, godot_packed_byte_array, byte); - INSERT_ARRAY(PackedFloat32Array, float); - INSERT_ARRAY(PackedFloat64Array, double); + INSERT_ARRAY(PackedFloat32Array, godot_packed_float32_array, float); + INSERT_ARRAY(PackedFloat64Array, godot_packed_float64_array, double); - INSERT_ARRAY(PackedStringArray, string); + INSERT_ARRAY(PackedStringArray, godot_packed_string_array, string); - INSERT_ARRAY(PackedColorArray, Color); - INSERT_ARRAY(PackedVector2Array, Vector2); - INSERT_ARRAY(PackedVector3Array, Vector3); + INSERT_ARRAY(PackedColorArray, godot_packed_color_array, Color); + INSERT_ARRAY(PackedVector2Array, godot_packed_vector2_array, Vector2); + INSERT_ARRAY(PackedVector3Array, godot_packed_vector3_array, Vector3); #undef INSERT_ARRAY @@ -3628,15 +3675,24 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.cname = itype.name; itype.proxy_name = itype.name; itype.type_parameter_count = 1; - itype.c_out = "\treturn memnew(Array(%1));\n"; - itype.c_type = itype.name; - itype.c_type_in = itype.c_type + "*"; - itype.c_type_out = itype.c_type + "*"; itype.cs_type = BINDINGS_NAMESPACE_COLLECTIONS "." + itype.proxy_name; - itype.cs_in = "%0." CS_SMETHOD_GETINSTANCE "()"; - itype.cs_out = "return new %2(%0(%1));"; - itype.im_type_in = "IntPtr"; - itype.im_type_out = "IntPtr"; + itype.cs_in_expr = "(%1)(%0 ?? new()).NativeValue"; + itype.c_out = "%5return %0.CreateTakingOwnershipOfDisposableValue(%1);\n"; + itype.c_arg_in = "&%s"; + itype.c_type = "godot_array"; + itype.c_type_in = itype.c_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = false; // [c_out] takes ownership + itype.c_ret_needs_default_initialization = true; + itype.cs_variant_to_managed = "VariantUtils.ConvertToArrayObject(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromArray(%0)"; + builtin_types.insert(itype.cname, itype); + + // Array_@generic + // Re-use Array's itype + itype.name = "Array_@generic"; + itype.cname = itype.name; + itype.cs_out = "%5return new %2(%0(%1));"; builtin_types.insert(itype.cname, itype); // Dictionary @@ -3645,15 +3701,24 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.cname = itype.name; itype.proxy_name = itype.name; itype.type_parameter_count = 2; - itype.c_out = "\treturn memnew(Dictionary(%1));\n"; - itype.c_type = itype.name; - itype.c_type_in = itype.c_type + "*"; - itype.c_type_out = itype.c_type + "*"; itype.cs_type = BINDINGS_NAMESPACE_COLLECTIONS "." + itype.proxy_name; - itype.cs_in = "%0." CS_SMETHOD_GETINSTANCE "()"; - itype.cs_out = "return new %2(%0(%1));"; - itype.im_type_in = "IntPtr"; - itype.im_type_out = "IntPtr"; + itype.cs_in_expr = "(%1)(%0 ?? new()).NativeValue"; + itype.c_out = "%5return %0.CreateTakingOwnershipOfDisposableValue(%1);\n"; + itype.c_arg_in = "&%s"; + itype.c_type = "godot_dictionary"; + itype.c_type_in = itype.c_type; + itype.c_type_out = itype.cs_type; + itype.c_type_is_disposable_struct = false; // [c_out] takes ownership + itype.c_ret_needs_default_initialization = true; + itype.cs_variant_to_managed = "VariantUtils.ConvertToDictionaryObject(%0)"; + itype.cs_managed_to_variant = "VariantUtils.CreateFromDictionary(%0)"; + builtin_types.insert(itype.cname, itype); + + // Dictionary_@generic + // Re-use Dictionary's itype + itype.name = "Dictionary_@generic"; + itype.cname = itype.name; + itype.cs_out = "%5return new %2(%0(%1));"; builtin_types.insert(itype.cname, itype); // void (fictitious type to represent the return type of methods that do not return anything) @@ -3661,12 +3726,10 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { itype.name = "void"; itype.cname = itype.name; itype.proxy_name = itype.name; - itype.c_type = itype.name; + itype.cs_type = itype.proxy_name; + itype.c_type = itype.proxy_name; itype.c_type_in = itype.c_type; itype.c_type_out = itype.c_type; - itype.cs_type = itype.proxy_name; - itype.im_type_in = itype.proxy_name; - itype.im_type_out = itype.proxy_name; builtin_types.insert(itype.cname, itype); } @@ -3721,6 +3784,8 @@ void BindingsGenerator::_populate_global_constants() { enum_itype.cname = ienum.cname; enum_itype.proxy_name = enum_itype.name; TypeInterface::postsetup_enum_type(enum_itype); + enum_itype.cs_variant_to_managed = "(%1)VariantUtils.ConvertToInt32(%0)"; + enum_itype.cs_managed_to_variant = "VariantUtils.CreateFromInt((int)%0)"; enum_types.insert(enum_itype.cname, enum_itype); int prefix_length = _determine_enum_prefix(ienum); @@ -3753,6 +3818,8 @@ void BindingsGenerator::_populate_global_constants() { enum_itype.cname = enum_cname; enum_itype.proxy_name = enum_itype.name; TypeInterface::postsetup_enum_type(enum_itype); + enum_itype.cs_variant_to_managed = "(%1)VariantUtils.ConvertToInt32(%0)"; + enum_itype.cs_managed_to_variant = "VariantUtils.CreateFromInt((int)%0)"; enum_types.insert(enum_itype.cname, enum_itype); } } @@ -3791,21 +3858,18 @@ void BindingsGenerator::_initialize() { // Generate internal calls (after populating type interfaces and global constants) - core_custom_icalls.clear(); - editor_custom_icalls.clear(); - for (const KeyValue<StringName, TypeInterface> &E : obj_types) { - _generate_method_icalls(E.value); + const TypeInterface &itype = E.value; + Error err = _populate_method_icalls_table(itype); + ERR_FAIL_COND_MSG(err != OK, "Failed to generate icalls table for type: " + itype.name); } initialized = true; } static String generate_all_glue_option = "--generate-mono-glue"; -static String generate_cs_glue_option = "--generate-mono-cs-glue"; -static String generate_cpp_glue_option = "--generate-mono-cpp-glue"; -static void handle_cmdline_options(String glue_dir_path, String cs_dir_path, String cpp_dir_path) { +static void handle_cmdline_options(String glue_dir_path) { BindingsGenerator bindings_generator; bindings_generator.set_log_print_enabled(true); @@ -3814,43 +3878,25 @@ static void handle_cmdline_options(String glue_dir_path, String cs_dir_path, Str return; } - if (glue_dir_path.length()) { - if (bindings_generator.generate_glue(glue_dir_path) != OK) { - ERR_PRINT(generate_all_glue_option + ": Failed to generate the C++ glue."); - } - - if (bindings_generator.generate_cs_api(glue_dir_path.plus_file(API_SOLUTION_NAME)) != OK) { - ERR_PRINT(generate_all_glue_option + ": Failed to generate the C# API."); - } - } + CRASH_COND(glue_dir_path.is_empty()); - if (cs_dir_path.length()) { - if (bindings_generator.generate_cs_api(cs_dir_path) != OK) { - ERR_PRINT(generate_cs_glue_option + ": Failed to generate the C# API."); - } + if (bindings_generator.generate_cs_api(glue_dir_path.path_join(API_SOLUTION_NAME)) != OK) { + ERR_PRINT(generate_all_glue_option + ": Failed to generate the C# API."); } +} - if (cpp_dir_path.length()) { - if (bindings_generator.generate_glue(cpp_dir_path) != OK) { - ERR_PRINT(generate_cpp_glue_option + ": Failed to generate the C++ glue."); - } - } +static void cleanup_and_exit_godot() { + // Exit once done + Main::cleanup(true); + ::exit(0); } void BindingsGenerator::handle_cmdline_args(const List<String> &p_cmdline_args) { - const int NUM_OPTIONS = 2; - String glue_dir_path; - String cs_dir_path; - String cpp_dir_path; - - int options_left = NUM_OPTIONS; - - bool exit_godot = false; const List<String>::Element *elem = p_cmdline_args.front(); - while (elem && options_left) { + while (elem) { if (elem->get() == generate_all_glue_option) { const List<String>::Element *path_elem = elem->next(); @@ -3859,48 +3905,20 @@ void BindingsGenerator::handle_cmdline_args(const List<String> &p_cmdline_args) elem = elem->next(); } else { ERR_PRINT(generate_all_glue_option + ": No output directory specified (expected path to '{GODOT_ROOT}/modules/mono/glue')."); - exit_godot = true; - } - - --options_left; - } else if (elem->get() == generate_cs_glue_option) { - const List<String>::Element *path_elem = elem->next(); - - if (path_elem) { - cs_dir_path = path_elem->get(); - elem = elem->next(); - } else { - ERR_PRINT(generate_cs_glue_option + ": No output directory specified."); - exit_godot = true; + // Exit once done with invalid command line arguments + cleanup_and_exit_godot(); } - --options_left; - } else if (elem->get() == generate_cpp_glue_option) { - const List<String>::Element *path_elem = elem->next(); - - if (path_elem) { - cpp_dir_path = path_elem->get(); - elem = elem->next(); - } else { - ERR_PRINT(generate_cpp_glue_option + ": No output directory specified."); - exit_godot = true; - } - - --options_left; + break; } elem = elem->next(); } - if (glue_dir_path.length() || cs_dir_path.length() || cpp_dir_path.length()) { - handle_cmdline_options(glue_dir_path, cs_dir_path, cpp_dir_path); - exit_godot = true; - } - - if (exit_godot) { + if (glue_dir_path.length()) { + handle_cmdline_options(glue_dir_path); // Exit once done - Main::cleanup(true); - ::exit(0); + cleanup_and_exit_godot(); } } diff --git a/modules/mono/editor/bindings_generator.h b/modules/mono/editor/bindings_generator.h index ee170e4558..c1295385dc 100644 --- a/modules/mono/editor/bindings_generator.h +++ b/modules/mono/editor/bindings_generator.h @@ -229,6 +229,23 @@ class BindingsGenerator { bool is_ref_counted = false; /** + * Determines whether the native return value of this type must be disposed + * by the generated internal call (think of `godot_string`, whose destructor + * must be called). Some structs that are disposable may still disable this + * flag if the ownership is transferred. + */ + bool c_type_is_disposable_struct = false; + + /** + * Determines whether the native return value of this type must be zero initialized + * before its address is passed to ptrcall. This is required for types whose destructor + * is called before being assigned the return value by `PtrToArg::encode`, e.g.: + * Array, Dictionary, String, StringName, Variant. + * It's not necessary to set this to `true` if [c_type_is_disposable_struct] is already `true`. + */ + bool c_ret_needs_default_initialization = false; + + /** * Used only by Object-derived types. * Determines if this type is not abstract (incomplete). * e.g.: CanvasItem cannot be instantiated. @@ -242,31 +259,34 @@ class BindingsGenerator { */ bool memory_own = false; - /** - * This must be set to true for any struct bigger than 32-bits. Those cannot be passed/returned by value - * with internal calls, so we must use pointers instead. Returns must be replace with out parameters. - * In this case, [c_out] and [cs_out] must have a different format, explained below. - * The Mono IL interpreter icall trampolines don't support passing structs bigger than 32-bits by value (at least not on WASM). - */ - bool ret_as_byref_arg = false; - // !! The comments of the following fields make reference to other fields via square brackets, e.g.: [field_name] // !! When renaming those fields, make sure to rename their references in the comments // --- C INTERFACE --- - static const char *DEFAULT_VARARG_C_IN; + /** + * One or more statements that transform the parameter before being passed as argument of a ptrcall. + * If the statement adds a local that must be passed as the argument instead of the parameter, + * the expression with the name of that local must be specified with [c_arg_in]. + * Formatting elements: + * %0: [c_type] of the parameter + * %1: name of the parameter + * %2-4: reserved + * %5: indentation text + */ + String c_in; /** - * One or more statements that manipulate the parameter before being passed as argument of a ptrcall. + * One or more statements that transform the parameter before being passed as argument of a vararg call. * If the statement adds a local that must be passed as the argument instead of the parameter, * the name of that local must be specified with [c_arg_in]. - * For variadic methods, this field is required and, if empty, [DEFAULT_VARARG_C_IN] is used instead. * Formatting elements: * %0: [c_type] of the parameter * %1: name of the parameter + * %2-4: reserved + * %5: indentation text */ - String c_in; + String c_in_vararg; /** * Determines the expression that will be passed as argument to ptrcall. @@ -291,7 +311,8 @@ class BindingsGenerator { * %0: [c_type_out] of the return type * %1: name of the variable to be returned * %2: [name] of the return type - * %3: name of the parameter that must be assigned the return value + * %3-4: reserved + * %5: indentation text */ String c_out; @@ -327,7 +348,21 @@ class BindingsGenerator { * An expression that overrides the way the parameter is passed to the internal call. * If empty, the parameter is passed as is. * Formatting elements: - * %0 or %s: name of the parameter + * %0: name of the parameter + * %1: [c_type] of the parameter + */ + String cs_in_expr; + bool cs_in_expr_is_unsafe = false; + + /** + * One or more statements that transform the parameter before being passed to the internal call. + * If the statement adds a local that must be passed as the argument instead of the parameter, + * the expression with the name of that local must be specified with [cs_in_expr]. + * Formatting elements: + * %0: [c_type] of the parameter + * %1: name of the parameter + * %2-4: reserved + * %5: indentation text */ String cs_in; @@ -338,7 +373,9 @@ class BindingsGenerator { * %0: internal method name * %1: internal method call arguments without surrounding parenthesis * %2: [cs_type] of the return type - * %3: [im_type_out] of the return type + * %3: [c_type_out] of the return type + * %4: reserved + * %5: indentation text */ String cs_out; @@ -349,14 +386,20 @@ class BindingsGenerator { String cs_type; /** - * Type used for parameters of internal call methods. + * Formatting elements: + * %0: input expression of type `in godot_variant` + * %1: [cs_type] of this type + * %2: [name] of this type */ - String im_type_in; + String cs_variant_to_managed; /** - * Type used for the return type of internal call methods. + * Formatting elements: + * %0: input expression + * %1: [cs_type] of this type + * %2: [name] of this type */ - String im_type_out; + String cs_managed_to_variant; const DocData::ClassDoc *class_doc = nullptr; @@ -366,6 +409,8 @@ class BindingsGenerator { List<MethodInterface> methods; List<SignalInterface> signals_; + bool has_virtual_methods = false; + const MethodInterface *find_method_by_name(const StringName &p_cname) const { for (const MethodInterface &E : methods) { if (E.cname == p_cname) { @@ -432,8 +477,8 @@ class BindingsGenerator { itype.c_type = itype.name; itype.cs_type = itype.proxy_name; - itype.im_type_in = "ref " + itype.proxy_name; - itype.im_type_out = itype.proxy_name; + itype.c_type_in = itype.proxy_name + "*"; + itype.c_type_out = itype.proxy_name; itype.class_doc = &EditorHelp::get_doc_data()->class_list[itype.proxy_name]; } @@ -467,65 +512,27 @@ class BindingsGenerator { return itype; } - static void create_placeholder_type(TypeInterface &r_itype, const StringName &p_cname) { - r_itype.name = p_cname; - r_itype.cname = p_cname; - r_itype.proxy_name = r_itype.name; - - r_itype.c_type = r_itype.name; - r_itype.c_type_in = "MonoObject*"; - r_itype.c_type_out = "MonoObject*"; - r_itype.cs_type = r_itype.proxy_name; - r_itype.im_type_in = r_itype.proxy_name; - r_itype.im_type_out = r_itype.proxy_name; - } - - static void postsetup_enum_type(TypeInterface &r_enum_itype) { - // C interface for enums is the same as that of 'uint32_t'. Remember to apply - // any of the changes done here to the 'uint32_t' type interface as well. - - r_enum_itype.c_arg_in = "&%s_in"; - { - // The expected types for parameters and return value in ptrcall are 'int64_t' or 'uint64_t'. - r_enum_itype.c_in = "\t%0 %1_in = (%0)%1;\n"; - r_enum_itype.c_out = "\treturn (%0)%1;\n"; - r_enum_itype.c_type = "int64_t"; - } - r_enum_itype.c_type_in = "int32_t"; - r_enum_itype.c_type_out = r_enum_itype.c_type_in; - - r_enum_itype.cs_type = r_enum_itype.proxy_name; - r_enum_itype.cs_in = "(int)%s"; - r_enum_itype.cs_out = "return (%2)%0(%1);"; - r_enum_itype.im_type_in = "int"; - r_enum_itype.im_type_out = "int"; - r_enum_itype.class_doc = &EditorHelp::get_doc_data()->class_list[r_enum_itype.proxy_name]; - } + static void postsetup_enum_type(TypeInterface &r_enum_itype); TypeInterface() {} }; struct InternalCall { String name; - String im_type_out; // Return type for the C# method declaration. Also used as companion of [unique_siq] - String im_sig; // Signature for the C# method declaration String unique_sig; // Unique signature to avoid duplicates in containers bool editor_only = false; - InternalCall() {} + bool is_vararg = false; + bool is_static = false; + TypeReference return_type; + List<TypeReference> argument_types; - InternalCall(const String &p_name, const String &p_im_type_out, const String &p_im_sig = String(), const String &p_unique_sig = String()) { - name = p_name; - im_type_out = p_im_type_out; - im_sig = p_im_sig; - unique_sig = p_unique_sig; - editor_only = false; - } + _FORCE_INLINE_ int get_arguments_count() const { return argument_types.size(); } + + InternalCall() {} - InternalCall(ClassDB::APIType api_type, const String &p_name, const String &p_im_type_out, const String &p_im_sig = String(), const String &p_unique_sig = String()) { + InternalCall(ClassDB::APIType api_type, const String &p_name, const String &p_unique_sig = String()) { name = p_name; - im_type_out = p_im_type_out; - im_sig = p_im_sig; unique_sig = p_unique_sig; editor_only = api_type == ClassDB::API_EDITOR; } @@ -540,7 +547,6 @@ class BindingsGenerator { HashMap<StringName, TypeInterface> obj_types; - HashMap<StringName, TypeInterface> placeholder_types; HashMap<StringName, TypeInterface> builtin_types; HashMap<StringName, TypeInterface> enum_types; @@ -548,13 +554,9 @@ class BindingsGenerator { List<ConstantInterface> global_constants; List<InternalCall> method_icalls; + /// Stores the unique internal calls from [method_icalls] that are assigned to each method. HashMap<const MethodInterface *, const InternalCall *> method_icalls_map; - List<const InternalCall *> generated_icall_funcs; - - List<InternalCall> core_custom_icalls; - List<InternalCall> editor_custom_icalls; - HashMap<StringName, List<StringName>> blacklisted_methods; void _initialize_blacklisted_methods(); @@ -571,6 +573,8 @@ class BindingsGenerator { StringName type_String = StaticCString::create("String"); StringName type_StringName = StaticCString::create("StringName"); StringName type_NodePath = StaticCString::create("NodePath"); + StringName type_Array_generic = StaticCString::create("Array_@generic"); + StringName type_Dictionary_generic = StaticCString::create("Dictionary_@generic"); StringName type_at_GlobalScope = StaticCString::create("@GlobalScope"); StringName enum_Error = StaticCString::create("Error"); @@ -595,12 +599,14 @@ class BindingsGenerator { StringName type_Vector4i = StaticCString::create("Vector4i"); // Object not included as it must be checked for all derived classes - static constexpr int nullable_types_count = 17; + static constexpr int nullable_types_count = 18; StringName nullable_types[nullable_types_count] = { type_String, type_StringName, type_NodePath, + type_Array_generic, + type_Dictionary_generic, StaticCString::create(_STR(Array)), StaticCString::create(_STR(Dictionary)), StaticCString::create(_STR(Callable)), @@ -636,17 +642,6 @@ class BindingsGenerator { NameCache name_cache; - const List<InternalCall>::Element *find_icall_by_name(const String &p_name, const List<InternalCall> &p_list) { - const List<InternalCall>::Element *it = p_list.front(); - while (it) { - if (it->get().name == p_name) { - return it; - } - it = it->next(); - } - return nullptr; - } - const ConstantInterface *find_constant_by_name(const String &p_name, const List<ConstantInterface> &p_constants) const { for (const ConstantInterface &E : p_constants) { if (E.name == p_name) { @@ -657,18 +652,38 @@ class BindingsGenerator { return nullptr; } - inline String get_unique_sig(const TypeInterface &p_type) { - if (p_type.is_ref_counted) { - return "Ref"; - } else if (p_type.is_object_type) { + inline String get_arg_unique_sig(const TypeInterface &p_type) { + // For parameters, we treat reference and non-reference derived types the same. + if (p_type.is_object_type) { return "Obj"; } else if (p_type.is_enum) { return "int"; + } else if (p_type.cname == name_cache.type_Array_generic) { + return "Array"; + } else if (p_type.cname == name_cache.type_Dictionary_generic) { + return "Dictionary"; } return p_type.name; } + inline String get_ret_unique_sig(const TypeInterface *p_type) { + // Reference derived return types are treated differently. + if (p_type->is_ref_counted) { + return "Ref"; + } else if (p_type->is_object_type) { + return "Obj"; + } else if (p_type->is_enum) { + return "int"; + } else if (p_type->cname == name_cache.type_Array_generic) { + return "Array"; + } else if (p_type->cname == name_cache.type_Dictionary_generic) { + return "Dictionary"; + } + + return p_type->name; + } + String bbcode_to_xml(const String &p_bbcode, const TypeInterface *p_itype); void _append_xml_method(StringBuilder &p_xml_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); @@ -682,10 +697,9 @@ class BindingsGenerator { int _determine_enum_prefix(const EnumInterface &p_ienum); void _apply_prefix_to_enum_constants(EnumInterface &p_ienum, int p_prefix_length); - void _generate_method_icalls(const TypeInterface &p_itype); + Error _populate_method_icalls_table(const TypeInterface &p_itype); const TypeInterface *_get_type_or_null(const TypeReference &p_typeref); - const TypeInterface *_get_type_or_placeholder(const TypeReference &p_typeref); const String _get_generic_type_parameters(const TypeInterface &p_itype, const List<TypeReference> &p_generic_type_parameters); @@ -706,11 +720,11 @@ class BindingsGenerator { Error _generate_cs_method(const TypeInterface &p_itype, const MethodInterface &p_imethod, int &p_method_bind_count, StringBuilder &p_output); Error _generate_cs_signal(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::SignalInterface &p_isignal, StringBuilder &p_output); + Error _generate_cs_native_calls(const InternalCall &p_icall, StringBuilder &r_output); + void _generate_array_extensions(StringBuilder &p_output); void _generate_global_constants(StringBuilder &p_output); - Error _generate_glue_method(const TypeInterface &p_itype, const MethodInterface &p_imethod, StringBuilder &p_output); - Error _save_file(const String &p_path, const StringBuilder &p_content); void _log(const char *p_format, ...) _PRINTF_FORMAT_ATTRIBUTE_2_3; @@ -721,15 +735,12 @@ public: Error generate_cs_core_project(const String &p_proj_dir); Error generate_cs_editor_project(const String &p_proj_dir); Error generate_cs_api(const String &p_output_dir); - Error generate_glue(const String &p_output_dir); _FORCE_INLINE_ bool is_log_print_enabled() { return log_print_enabled; } _FORCE_INLINE_ void set_log_print_enabled(bool p_enabled) { log_print_enabled = p_enabled; } _FORCE_INLINE_ bool is_initialized() { return initialized; } - static uint32_t get_version(); - static void handle_cmdline_args(const List<String> &p_cmdline_args); BindingsGenerator() { diff --git a/modules/mono/editor/code_completion.cpp b/modules/mono/editor/code_completion.cpp index 7bce6f2c21..40296eef10 100644 --- a/modules/mono/editor/code_completion.cpp +++ b/modules/mono/editor/code_completion.cpp @@ -35,6 +35,7 @@ #include "editor/editor_settings.h" #include "scene/gui/control.h" #include "scene/main/node.h" +#include "scene/theme/theme_db.h" namespace gdmono { @@ -162,9 +163,9 @@ PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_scr } if (dir_access->dir_exists(filename)) { - directories.push_back(dir_access->get_current_dir().plus_file(filename)); + directories.push_back(dir_access->get_current_dir().path_join(filename)); } else if (filename.ends_with(".tscn") || filename.ends_with(".scn")) { - suggestions.push_back(quoted(dir_access->get_current_dir().plus_file(filename))); + suggestions.push_back(quoted(dir_access->get_current_dir().path_join(filename))); } filename = dir_access->get_next(); @@ -195,7 +196,7 @@ PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_scr Node *base = _try_find_owner_node_in_tree(script); if (base && Object::cast_to<Control>(base)) { List<StringName> sn; - Theme::get_default()->get_color_list(base->get_class(), &sn); + ThemeDB::get_singleton()->get_default_theme()->get_color_list(base->get_class(), &sn); for (const StringName &E : sn) { suggestions.push_back(quoted(E)); @@ -207,7 +208,7 @@ PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_scr Node *base = _try_find_owner_node_in_tree(script); if (base && Object::cast_to<Control>(base)) { List<StringName> sn; - Theme::get_default()->get_constant_list(base->get_class(), &sn); + ThemeDB::get_singleton()->get_default_theme()->get_constant_list(base->get_class(), &sn); for (const StringName &E : sn) { suggestions.push_back(quoted(E)); @@ -219,7 +220,7 @@ PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_scr Node *base = _try_find_owner_node_in_tree(script); if (base && Object::cast_to<Control>(base)) { List<StringName> sn; - Theme::get_default()->get_font_list(base->get_class(), &sn); + ThemeDB::get_singleton()->get_default_theme()->get_font_list(base->get_class(), &sn); for (const StringName &E : sn) { suggestions.push_back(quoted(E)); @@ -231,7 +232,7 @@ PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_scr Node *base = _try_find_owner_node_in_tree(script); if (base && Object::cast_to<Control>(base)) { List<StringName> sn; - Theme::get_default()->get_font_size_list(base->get_class(), &sn); + ThemeDB::get_singleton()->get_default_theme()->get_font_size_list(base->get_class(), &sn); for (const StringName &E : sn) { suggestions.push_back(quoted(E)); @@ -243,7 +244,7 @@ PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_scr Node *base = _try_find_owner_node_in_tree(script); if (base && Object::cast_to<Control>(base)) { List<StringName> sn; - Theme::get_default()->get_stylebox_list(base->get_class(), &sn); + ThemeDB::get_singleton()->get_default_theme()->get_stylebox_list(base->get_class(), &sn); for (const StringName &E : sn) { suggestions.push_back(quoted(E)); diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index f830c7ffe1..6f42ad6916 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -46,179 +46,81 @@ #include "main/main.h" #include "../csharp_script.h" -#include "../glue/cs_glue_version.gen.h" #include "../godotsharp_dirs.h" -#include "../mono_gd/gd_mono_marshal.h" #include "../utils/macos_utils.h" #include "code_completion.h" -#include "godotsharp_export.h" -MonoString *godot_icall_GodotSharpDirs_ResDataDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_data_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResMetadataDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_metadata_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResAssembliesBaseDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_assemblies_base_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResAssembliesDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_assemblies_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResConfigDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_config_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResTempDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_temp_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_temp_assemblies_base_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_ResTempAssembliesDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_temp_assemblies_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_MonoUserDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_mono_user_dir()); -} +#include "../interop_types.h" -MonoString *godot_icall_GodotSharpDirs_MonoLogsDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_mono_logs_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_MonoSolutionsDir() { -#ifdef TOOLS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_mono_solutions_dir()); -#else - return nullptr; +#ifdef __cplusplus +extern "C" { #endif -} -MonoString *godot_icall_GodotSharpDirs_BuildLogsDirs() { -#ifdef TOOLS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_build_logs_dir()); -#else - return nullptr; -#endif +void godot_icall_GodotSharpDirs_ResMetadataDir(godot_string *r_dest) { + memnew_placement(r_dest, String(GodotSharpDirs::get_res_metadata_dir())); } -MonoString *godot_icall_GodotSharpDirs_ProjectSlnPath() { -#ifdef TOOLS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_project_sln_path()); -#else - return nullptr; -#endif +void godot_icall_GodotSharpDirs_MonoUserDir(godot_string *r_dest) { + memnew_placement(r_dest, String(GodotSharpDirs::get_mono_user_dir())); } -MonoString *godot_icall_GodotSharpDirs_ProjectCsProjPath() { +void godot_icall_GodotSharpDirs_BuildLogsDirs(godot_string *r_dest) { #ifdef TOOLS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_project_csproj_path()); + memnew_placement(r_dest, String(GodotSharpDirs::get_build_logs_dir())); #else return nullptr; #endif } -MonoString *godot_icall_GodotSharpDirs_DataEditorToolsDir() { +void godot_icall_GodotSharpDirs_DataEditorToolsDir(godot_string *r_dest) { #ifdef TOOLS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_data_editor_tools_dir()); + memnew_placement(r_dest, String(GodotSharpDirs::get_data_editor_tools_dir())); #else return nullptr; #endif } -MonoString *godot_icall_GodotSharpDirs_DataEditorPrebuiltApiDir() { -#ifdef TOOLS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_data_editor_prebuilt_api_dir()); -#else - return nullptr; -#endif -} - -MonoString *godot_icall_GodotSharpDirs_DataMonoEtcDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_data_mono_etc_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_DataMonoLibDir() { - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_data_mono_lib_dir()); -} - -MonoString *godot_icall_GodotSharpDirs_DataMonoBinDir() { -#ifdef WINDOWS_ENABLED - return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_data_mono_bin_dir()); -#else - return nullptr; -#endif -} - -void godot_icall_EditorProgress_Create(MonoString *p_task, MonoString *p_label, int32_t p_amount, MonoBoolean p_can_cancel) { - String task = GDMonoMarshal::mono_string_to_godot(p_task); - String label = GDMonoMarshal::mono_string_to_godot(p_label); +void godot_icall_EditorProgress_Create(const godot_string *p_task, const godot_string *p_label, int32_t p_amount, bool p_can_cancel) { + String task = *reinterpret_cast<const String *>(p_task); + String label = *reinterpret_cast<const String *>(p_label); EditorNode::progress_add_task(task, label, p_amount, (bool)p_can_cancel); } -void godot_icall_EditorProgress_Dispose(MonoString *p_task) { - String task = GDMonoMarshal::mono_string_to_godot(p_task); +void godot_icall_EditorProgress_Dispose(const godot_string *p_task) { + String task = *reinterpret_cast<const String *>(p_task); EditorNode::progress_end_task(task); } -MonoBoolean godot_icall_EditorProgress_Step(MonoString *p_task, MonoString *p_state, int32_t p_step, MonoBoolean p_force_refresh) { - String task = GDMonoMarshal::mono_string_to_godot(p_task); - String state = GDMonoMarshal::mono_string_to_godot(p_state); +bool godot_icall_EditorProgress_Step(const godot_string *p_task, const godot_string *p_state, int32_t p_step, bool p_force_refresh) { + String task = *reinterpret_cast<const String *>(p_task); + String state = *reinterpret_cast<const String *>(p_state); return EditorNode::progress_task_step(task, state, p_step, (bool)p_force_refresh); } -uint32_t godot_icall_ExportPlugin_GetExportedAssemblyDependencies(MonoObject *p_initial_assemblies, - MonoString *p_build_config, MonoString *p_custom_bcl_dir, MonoObject *r_assembly_dependencies) { - Dictionary initial_dependencies = GDMonoMarshal::mono_object_to_variant(p_initial_assemblies); - String build_config = GDMonoMarshal::mono_string_to_godot(p_build_config); - String custom_bcl_dir = GDMonoMarshal::mono_string_to_godot(p_custom_bcl_dir); - Dictionary assembly_dependencies = GDMonoMarshal::mono_object_to_variant(r_assembly_dependencies); - - return GodotSharpExport::get_exported_assembly_dependencies(initial_dependencies, build_config, custom_bcl_dir, assembly_dependencies); -} - -MonoString *godot_icall_Internal_UpdateApiAssembliesFromPrebuilt(MonoString *p_config) { - String config = GDMonoMarshal::mono_string_to_godot(p_config); - String error_str = GDMono::get_singleton()->update_api_assemblies_from_prebuilt(config); - return GDMonoMarshal::mono_string_from_godot(error_str); +void godot_icall_Internal_FullExportTemplatesDir(godot_string *r_dest) { + String full_templates_dir = EditorPaths::get_singleton()->get_export_templates_dir().path_join(VERSION_FULL_CONFIG); + memnew_placement(r_dest, String(full_templates_dir)); } -MonoString *godot_icall_Internal_FullExportTemplatesDir() { - String full_templates_dir = EditorPaths::get_singleton()->get_export_templates_dir().plus_file(VERSION_FULL_CONFIG); - return GDMonoMarshal::mono_string_from_godot(full_templates_dir); -} - -MonoString *godot_icall_Internal_SimplifyGodotPath(MonoString *p_path) { - String path = GDMonoMarshal::mono_string_to_godot(p_path); - return GDMonoMarshal::mono_string_from_godot(path.simplify_path()); -} - -MonoBoolean godot_icall_Internal_IsMacOSAppBundleInstalled(MonoString *p_bundle_id) { +bool godot_icall_Internal_IsMacOSAppBundleInstalled(const godot_string *p_bundle_id) { #ifdef MACOS_ENABLED - String bundle_id = GDMonoMarshal::mono_string_to_godot(p_bundle_id); - return (MonoBoolean)macos_is_app_bundle_installed(bundle_id); + String bundle_id = *reinterpret_cast<const String *>(p_bundle_id); + return (bool)macos_is_app_bundle_installed(bundle_id); #else (void)p_bundle_id; // UNUSED - return (MonoBoolean) false; + return (bool)false; #endif } -MonoBoolean godot_icall_Internal_GodotIs32Bits() { +bool godot_icall_Internal_GodotIs32Bits() { return sizeof(void *) == 4; } -MonoBoolean godot_icall_Internal_GodotIsRealTDouble() { +bool godot_icall_Internal_GodotIsRealTDouble() { #ifdef REAL_T_IS_DOUBLE - return (MonoBoolean) true; + return (bool)true; #else - return (MonoBoolean) false; + return (bool)false; #endif } @@ -226,23 +128,15 @@ void godot_icall_Internal_GodotMainIteration() { Main::iteration(); } -uint64_t godot_icall_Internal_GetCoreApiHash() { - return ClassDB::get_api_hash(ClassDB::API_CORE); -} - -uint64_t godot_icall_Internal_GetEditorApiHash() { - return ClassDB::get_api_hash(ClassDB::API_EDITOR); -} - -MonoBoolean godot_icall_Internal_IsAssembliesReloadingNeeded() { +bool godot_icall_Internal_IsAssembliesReloadingNeeded() { #ifdef GD_MONO_HOT_RELOAD - return (MonoBoolean)CSharpLanguage::get_singleton()->is_assembly_reloading_needed(); + return (bool)CSharpLanguage::get_singleton()->is_assembly_reloading_needed(); #else - return (MonoBoolean) false; + return (bool)false; #endif } -void godot_icall_Internal_ReloadAssemblies(MonoBoolean p_soft_reload) { +void godot_icall_Internal_ReloadAssemblies(bool p_soft_reload) { #ifdef GD_MONO_HOT_RELOAD mono_bind::GodotSharp::get_singleton()->call_deferred(SNAME("_reload_assemblies"), (bool)p_soft_reload); #endif @@ -252,24 +146,15 @@ void godot_icall_Internal_EditorDebuggerNodeReloadScripts() { EditorDebuggerNode::get_singleton()->reload_scripts(); } -MonoBoolean godot_icall_Internal_ScriptEditorEdit(MonoObject *p_resource, int32_t p_line, int32_t p_col, MonoBoolean p_grab_focus) { - Ref<Resource> resource = GDMonoMarshal::mono_object_to_variant(p_resource); - return (MonoBoolean)ScriptEditor::get_singleton()->edit(resource, p_line, p_col, (bool)p_grab_focus); +bool godot_icall_Internal_ScriptEditorEdit(Resource *p_resource, int32_t p_line, int32_t p_col, bool p_grab_focus) { + Ref<Resource> resource = p_resource; + return (bool)ScriptEditor::get_singleton()->edit(resource, p_line, p_col, (bool)p_grab_focus); } void godot_icall_Internal_EditorNodeShowScriptScreen() { EditorNode::get_singleton()->call("_editor_select", EditorNode::EDITOR_SCRIPT); } -MonoString *godot_icall_Internal_MonoWindowsInstallRoot() { -#ifdef WINDOWS_ENABLED - String install_root_dir = GDMono::get_singleton()->get_mono_reg_info().install_root_dir; - return GDMonoMarshal::mono_string_from_godot(install_root_dir); -#else - return nullptr; -#endif -} - void godot_icall_Internal_EditorRunPlay() { EditorNode::get_singleton()->run_play(); } @@ -285,114 +170,93 @@ void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() { } } -MonoArray *godot_icall_Internal_CodeCompletionRequest(int32_t p_kind, MonoString *p_script_file) { - String script_file = GDMonoMarshal::mono_string_to_godot(p_script_file); +void godot_icall_Internal_CodeCompletionRequest(int32_t p_kind, const godot_string *p_script_file, godot_packed_array *r_ret) { + String script_file = *reinterpret_cast<const String *>(p_script_file); PackedStringArray suggestions = gdmono::get_code_completion((gdmono::CompletionKind)p_kind, script_file); - return GDMonoMarshal::PackedStringArray_to_mono_array(suggestions); + memnew_placement(r_ret, PackedStringArray(suggestions)); } float godot_icall_Globals_EditorScale() { return EDSCALE; } -MonoObject *godot_icall_Globals_GlobalDef(MonoString *p_setting, MonoObject *p_default_value, MonoBoolean p_restart_if_changed) { - String setting = GDMonoMarshal::mono_string_to_godot(p_setting); - Variant default_value = GDMonoMarshal::mono_object_to_variant(p_default_value); +void godot_icall_Globals_GlobalDef(const godot_string *p_setting, const godot_variant *p_default_value, bool p_restart_if_changed, godot_variant *r_result) { + String setting = *reinterpret_cast<const String *>(p_setting); + Variant default_value = *reinterpret_cast<const Variant *>(p_default_value); Variant result = _GLOBAL_DEF(setting, default_value, (bool)p_restart_if_changed); - return GDMonoMarshal::variant_to_mono_object(result); + memnew_placement(r_result, Variant(result)); } -MonoObject *godot_icall_Globals_EditorDef(MonoString *p_setting, MonoObject *p_default_value, MonoBoolean p_restart_if_changed) { - String setting = GDMonoMarshal::mono_string_to_godot(p_setting); - Variant default_value = GDMonoMarshal::mono_object_to_variant(p_default_value); +void godot_icall_Globals_EditorDef(const godot_string *p_setting, const godot_variant *p_default_value, bool p_restart_if_changed, godot_variant *r_result) { + String setting = *reinterpret_cast<const String *>(p_setting); + Variant default_value = *reinterpret_cast<const Variant *>(p_default_value); Variant result = _EDITOR_DEF(setting, default_value, (bool)p_restart_if_changed); - return GDMonoMarshal::variant_to_mono_object(result); + memnew_placement(r_result, Variant(result)); } -MonoObject *godot_icall_Globals_EditorShortcut(MonoString *p_setting) { - String setting = GDMonoMarshal::mono_string_to_godot(p_setting); +void godot_icall_Globals_EditorShortcut(const godot_string *p_setting, godot_variant *r_result) { + String setting = *reinterpret_cast<const String *>(p_setting); Ref<Shortcut> result = ED_GET_SHORTCUT(setting); - return GDMonoMarshal::variant_to_mono_object(result); + memnew_placement(r_result, Variant(result)); } -MonoString *godot_icall_Globals_TTR(MonoString *p_text) { - String text = GDMonoMarshal::mono_string_to_godot(p_text); - return GDMonoMarshal::mono_string_from_godot(TTR(text)); +void godot_icall_Globals_TTR(const godot_string *p_text, godot_string *r_dest) { + String text = *reinterpret_cast<const String *>(p_text); + memnew_placement(r_dest, String(TTR(text))); } -MonoString *godot_icall_Utils_OS_GetPlatformName() { +void godot_icall_Utils_OS_GetPlatformName(godot_string *r_dest) { String os_name = OS::get_singleton()->get_name(); - return GDMonoMarshal::mono_string_from_godot(os_name); + memnew_placement(r_dest, String(os_name)); } -MonoBoolean godot_icall_Utils_OS_UnixFileHasExecutableAccess(MonoString *p_file_path) { +bool godot_icall_Utils_OS_UnixFileHasExecutableAccess(const godot_string *p_file_path) { #ifdef UNIX_ENABLED - String file_path = GDMonoMarshal::mono_string_to_godot(p_file_path); + String file_path = *reinterpret_cast<const String *>(p_file_path); return access(file_path.utf8().get_data(), X_OK) == 0; #else ERR_FAIL_V(false); #endif } -void register_editor_internal_calls() { - // GodotSharpDirs - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResDataDir", godot_icall_GodotSharpDirs_ResDataDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResMetadataDir", godot_icall_GodotSharpDirs_ResMetadataDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResAssembliesBaseDir", godot_icall_GodotSharpDirs_ResAssembliesBaseDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResAssembliesDir", godot_icall_GodotSharpDirs_ResAssembliesDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResConfigDir", godot_icall_GodotSharpDirs_ResConfigDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResTempDir", godot_icall_GodotSharpDirs_ResTempDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResTempAssembliesBaseDir", godot_icall_GodotSharpDirs_ResTempAssembliesBaseDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResTempAssembliesDir", godot_icall_GodotSharpDirs_ResTempAssembliesDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_MonoUserDir", godot_icall_GodotSharpDirs_MonoUserDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_MonoLogsDir", godot_icall_GodotSharpDirs_MonoLogsDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_MonoSolutionsDir", godot_icall_GodotSharpDirs_MonoSolutionsDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_BuildLogsDirs", godot_icall_GodotSharpDirs_BuildLogsDirs); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ProjectSlnPath", godot_icall_GodotSharpDirs_ProjectSlnPath); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ProjectCsProjPath", godot_icall_GodotSharpDirs_ProjectCsProjPath); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_DataEditorToolsDir", godot_icall_GodotSharpDirs_DataEditorToolsDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_DataEditorPrebuiltApiDir", godot_icall_GodotSharpDirs_DataEditorPrebuiltApiDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_DataMonoEtcDir", godot_icall_GodotSharpDirs_DataMonoEtcDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_DataMonoLibDir", godot_icall_GodotSharpDirs_DataMonoLibDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_DataMonoBinDir", godot_icall_GodotSharpDirs_DataMonoBinDir); - - // EditorProgress - GDMonoUtils::add_internal_call("GodotTools.Internals.EditorProgress::internal_Create", godot_icall_EditorProgress_Create); - GDMonoUtils::add_internal_call("GodotTools.Internals.EditorProgress::internal_Dispose", godot_icall_EditorProgress_Dispose); - GDMonoUtils::add_internal_call("GodotTools.Internals.EditorProgress::internal_Step", godot_icall_EditorProgress_Step); - - // ExportPlugin - GDMonoUtils::add_internal_call("GodotTools.Export.ExportPlugin::internal_GetExportedAssemblyDependencies", godot_icall_ExportPlugin_GetExportedAssemblyDependencies); - - // Internals - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_UpdateApiAssembliesFromPrebuilt", godot_icall_Internal_UpdateApiAssembliesFromPrebuilt); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_FullExportTemplatesDir", godot_icall_Internal_FullExportTemplatesDir); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_SimplifyGodotPath", godot_icall_Internal_SimplifyGodotPath); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_IsMacOSAppBundleInstalled", godot_icall_Internal_IsMacOSAppBundleInstalled); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_GodotIs32Bits", godot_icall_Internal_GodotIs32Bits); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_GodotIsRealTDouble", godot_icall_Internal_GodotIsRealTDouble); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_GodotMainIteration", godot_icall_Internal_GodotMainIteration); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_GetCoreApiHash", godot_icall_Internal_GetCoreApiHash); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_GetEditorApiHash", godot_icall_Internal_GetEditorApiHash); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_IsAssembliesReloadingNeeded", godot_icall_Internal_IsAssembliesReloadingNeeded); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_ReloadAssemblies", godot_icall_Internal_ReloadAssemblies); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorDebuggerNodeReloadScripts", godot_icall_Internal_EditorDebuggerNodeReloadScripts); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorEdit", godot_icall_Internal_ScriptEditorEdit); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorNodeShowScriptScreen", godot_icall_Internal_EditorNodeShowScriptScreen); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_MonoWindowsInstallRoot", godot_icall_Internal_MonoWindowsInstallRoot); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", godot_icall_Internal_EditorRunPlay); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", godot_icall_Internal_EditorRunStop); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", godot_icall_Internal_ScriptEditorDebugger_ReloadScripts); - GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_CodeCompletionRequest", godot_icall_Internal_CodeCompletionRequest); - - // Globals - GDMonoUtils::add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", godot_icall_Globals_EditorScale); - GDMonoUtils::add_internal_call("GodotTools.Internals.Globals::internal_GlobalDef", godot_icall_Globals_GlobalDef); - GDMonoUtils::add_internal_call("GodotTools.Internals.Globals::internal_EditorDef", godot_icall_Globals_EditorDef); - GDMonoUtils::add_internal_call("GodotTools.Internals.Globals::internal_EditorShortcut", godot_icall_Globals_EditorShortcut); - GDMonoUtils::add_internal_call("GodotTools.Internals.Globals::internal_TTR", godot_icall_Globals_TTR); - - // Utils.OS - GDMonoUtils::add_internal_call("GodotTools.Utils.OS::GetPlatformName", godot_icall_Utils_OS_GetPlatformName); - GDMonoUtils::add_internal_call("GodotTools.Utils.OS::UnixFileHasExecutableAccess", godot_icall_Utils_OS_UnixFileHasExecutableAccess); +#ifdef __cplusplus +} +#endif + +// The order in this array must match the declaration order of +// the methods in 'GodotTools/Internals/Internal.cs'. +static const void *unmanaged_callbacks[]{ + (void *)godot_icall_GodotSharpDirs_ResMetadataDir, + (void *)godot_icall_GodotSharpDirs_MonoUserDir, + (void *)godot_icall_GodotSharpDirs_BuildLogsDirs, + (void *)godot_icall_GodotSharpDirs_DataEditorToolsDir, + (void *)godot_icall_EditorProgress_Create, + (void *)godot_icall_EditorProgress_Dispose, + (void *)godot_icall_EditorProgress_Step, + (void *)godot_icall_Internal_FullExportTemplatesDir, + (void *)godot_icall_Internal_IsMacOSAppBundleInstalled, + (void *)godot_icall_Internal_GodotIs32Bits, + (void *)godot_icall_Internal_GodotIsRealTDouble, + (void *)godot_icall_Internal_GodotMainIteration, + (void *)godot_icall_Internal_IsAssembliesReloadingNeeded, + (void *)godot_icall_Internal_ReloadAssemblies, + (void *)godot_icall_Internal_EditorDebuggerNodeReloadScripts, + (void *)godot_icall_Internal_ScriptEditorEdit, + (void *)godot_icall_Internal_EditorNodeShowScriptScreen, + (void *)godot_icall_Internal_EditorRunPlay, + (void *)godot_icall_Internal_EditorRunStop, + (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts, + (void *)godot_icall_Internal_CodeCompletionRequest, + (void *)godot_icall_Globals_EditorScale, + (void *)godot_icall_Globals_GlobalDef, + (void *)godot_icall_Globals_EditorDef, + (void *)godot_icall_Globals_EditorShortcut, + (void *)godot_icall_Globals_TTR, + (void *)godot_icall_Utils_OS_GetPlatformName, + (void *)godot_icall_Utils_OS_UnixFileHasExecutableAccess, +}; + +const void **godotsharp::get_editor_interop_funcs(int32_t &r_size) { + r_size = sizeof(unmanaged_callbacks); + return unmanaged_callbacks; } diff --git a/modules/mono/editor/editor_internal_calls.h b/modules/mono/editor/editor_internal_calls.h index 8262ac211a..35391f1f04 100644 --- a/modules/mono/editor/editor_internal_calls.h +++ b/modules/mono/editor/editor_internal_calls.h @@ -31,6 +31,10 @@ #ifndef EDITOR_INTERNAL_CALLS_H #define EDITOR_INTERNAL_CALLS_H -void register_editor_internal_calls(); +#include "core/typedefs.h" + +namespace godotsharp { +const void **get_editor_interop_funcs(int32_t &r_size); +} #endif // EDITOR_INTERNAL_CALLS_H diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp deleted file mode 100644 index f9ea403334..0000000000 --- a/modules/mono/editor/godotsharp_export.cpp +++ /dev/null @@ -1,144 +0,0 @@ -/*************************************************************************/ -/* godotsharp_export.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#include "godotsharp_export.h" - -#include <mono/metadata/image.h> - -#include "core/config/project_settings.h" -#include "core/io/file_access_pack.h" -#include "core/os/os.h" - -#include "../mono_gd/gd_mono.h" -#include "../mono_gd/gd_mono_assembly.h" -#include "../mono_gd/gd_mono_cache.h" -#include "../utils/macros.h" - -namespace GodotSharpExport { - -MonoAssemblyName *new_mono_assembly_name() { - // Mono has no public API to create an empty MonoAssemblyName and the struct is private. - // As such the only way to create it is with a stub name and then clear it. - - MonoAssemblyName *aname = mono_assembly_name_new("stub"); - CRASH_COND(aname == nullptr); - mono_assembly_name_free(aname); // Frees the string fields, not the struct - return aname; -} - -struct AssemblyRefInfo { - String name; - uint16_t major = 0; - uint16_t minor = 0; - uint16_t build = 0; - uint16_t revision = 0; -}; - -AssemblyRefInfo get_assemblyref_name(MonoImage *p_image, int index) { - const MonoTableInfo *table_info = mono_image_get_table_info(p_image, MONO_TABLE_ASSEMBLYREF); - - uint32_t cols[MONO_ASSEMBLYREF_SIZE]; - - mono_metadata_decode_row(table_info, index, cols, MONO_ASSEMBLYREF_SIZE); - - return { - String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME])), - (uint16_t)cols[MONO_ASSEMBLYREF_MAJOR_VERSION], - (uint16_t)cols[MONO_ASSEMBLYREF_MINOR_VERSION], - (uint16_t)cols[MONO_ASSEMBLYREF_BUILD_NUMBER], - (uint16_t)cols[MONO_ASSEMBLYREF_REV_NUMBER] - }; -} - -Error get_assembly_dependencies(GDMonoAssembly *p_assembly, MonoAssemblyName *reusable_aname, const Vector<String> &p_search_dirs, Dictionary &r_assembly_dependencies) { - MonoImage *image = p_assembly->get_image(); - - for (int i = 0; i < mono_image_get_table_rows(image, MONO_TABLE_ASSEMBLYREF); i++) { - AssemblyRefInfo ref_info = get_assemblyref_name(image, i); - - const String &ref_name = ref_info.name; - - if (r_assembly_dependencies.has(ref_name)) { - continue; - } - - mono_assembly_get_assemblyref(image, i, reusable_aname); - - GDMonoAssembly *ref_assembly = nullptr; - if (!GDMono::get_singleton()->load_assembly(ref_name, reusable_aname, &ref_assembly, /* refonly: */ true, p_search_dirs)) { - ERR_FAIL_V_MSG(ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'."); - } - - r_assembly_dependencies[ref_name] = ref_assembly->get_path(); - - Error err = get_assembly_dependencies(ref_assembly, reusable_aname, p_search_dirs, r_assembly_dependencies); - ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'."); - } - - return OK; -} - -Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies, - const String &p_build_config, const String &p_custom_bcl_dir, Dictionary &r_assembly_dependencies) { - MonoDomain *export_domain = GDMonoUtils::create_domain("GodotEngine.Domain.ProjectExport"); - ERR_FAIL_NULL_V(export_domain, FAILED); - _GDMONO_SCOPE_EXIT_DOMAIN_UNLOAD_(export_domain); - - _GDMONO_SCOPE_DOMAIN_(export_domain); - - Vector<String> search_dirs; - GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_bcl_dir); - - if (p_custom_bcl_dir.length()) { - // Only one mscorlib can be loaded. We need this workaround to make sure we get it from the right BCL directory. - r_assembly_dependencies["mscorlib"] = p_custom_bcl_dir.plus_file("mscorlib.dll").simplify_path(); - } - - for (const Variant *key = p_initial_assemblies.next(); key; key = p_initial_assemblies.next(key)) { - String assembly_name = *key; - String assembly_path = p_initial_assemblies[*key]; - - GDMonoAssembly *assembly = nullptr; - bool load_success = GDMono::get_singleton()->load_assembly_from(assembly_name, assembly_path, &assembly, /* refonly: */ true); - - ERR_FAIL_COND_V_MSG(!load_success, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + assembly_name + "'."); - - MonoAssemblyName *reusable_aname = new_mono_assembly_name(); - SCOPE_EXIT { mono_free(reusable_aname); }; - - Error err = get_assembly_dependencies(assembly, reusable_aname, search_dirs, r_assembly_dependencies); - if (err != OK) { - return err; - } - } - - return OK; -} -} // namespace GodotSharpExport diff --git a/modules/mono/editor/godotsharp_export.h b/modules/mono/editor/godotsharp_export.h deleted file mode 100644 index 60620b5f4d..0000000000 --- a/modules/mono/editor/godotsharp_export.h +++ /dev/null @@ -1,48 +0,0 @@ -/*************************************************************************/ -/* godotsharp_export.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef GODOTSHARP_EXPORT_H -#define GODOTSHARP_EXPORT_H - -#include "core/error/error_list.h" -#include "core/string/ustring.h" -#include "core/variant/dictionary.h" - -#include "../mono_gd/gd_mono_header.h" - -namespace GodotSharpExport { - -Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_dependencies); - -Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies, - const String &p_build_config, const String &p_custom_lib_dir, Dictionary &r_assembly_dependencies); -} // namespace GodotSharpExport - -#endif // GODOTSHARP_EXPORT_H diff --git a/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs b/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs index 2ca81ab7cd..1f5ea7532d 100644 --- a/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs +++ b/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs @@ -8,8 +8,8 @@ public partial class _CLASS_ : _BASE_ public const float Speed = 300.0f; public const float JumpVelocity = -400.0f; - // Get the gravity from the project settings to be synced with RigidDynamicBody nodes. - public float gravity = (float)ProjectSettings.GetSetting("physics/2d/default_gravity"); + // Get the gravity from the project settings to be synced with RigidBody nodes. + public float gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle(); public override void _PhysicsProcess(float delta) { diff --git a/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs b/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs index a6935fe497..4e978b7549 100644 --- a/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs +++ b/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs @@ -8,8 +8,8 @@ public partial class _CLASS_ : _BASE_ public const float Speed = 5.0f; public const float JumpVelocity = 4.5f; - // Get the gravity from the project settings to be synced with RigidDynamicBody nodes. - public float gravity = (float)ProjectSettings.GetSetting("physics/3d/default_gravity"); + // Get the gravity from the project settings to be synced with RigidBody nodes. + public float gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle(); public override void _PhysicsProcess(float delta) { @@ -26,7 +26,7 @@ public partial class _CLASS_ : _BASE_ // Get the input direction and handle the movement/deceleration. // As good practice, you should replace UI actions with custom gameplay actions. Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down"); - Vector3 direction = Transform.basis.Xform(new Vector3(inputDir.x, 0, inputDir.y)).Normalized(); + Vector3 direction = (Transform.basis * new Vector3(inputDir.x, 0, inputDir.y)).Normalized(); if (direction != Vector3.Zero) { velocity.x = direction.x * Speed; diff --git a/modules/mono/editor/script_templates/VisualShaderNodeCustom/basic.cs b/modules/mono/editor/script_templates/VisualShaderNodeCustom/basic.cs index a1b93e7daa..bb482e0d6a 100644 --- a/modules/mono/editor/script_templates/VisualShaderNodeCustom/basic.cs +++ b/modules/mono/editor/script_templates/VisualShaderNodeCustom/basic.cs @@ -55,7 +55,7 @@ public partial class VisualShaderNode_CLASS_ : _BASE_ return 0; } - public override string _GetCode(Godot.Collections.Array inputVars, Godot.Collections.Array outputVars, Shader.Mode mode, VisualShader.Type type) + public override string _GetCode(Godot.Collections.Array<string> inputVars, Godot.Collections.Array<string> outputVars, Shader.Mode mode, VisualShader.Type type) { return ""; } |