Ver Fonte

Merge pull request #41408 from neikeq/senkyu

3.2 New csproj style with backport of Godot.NET.Sdk
Rémi Verschelde há 5 anos atrás
pai
commit
20e411545c
41 ficheiros alterados com 873 adições e 779 exclusões
  1. 4 0
      .editorconfig
  2. 16 0
      modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
  3. 35 0
      modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj
  4. 22 0
      modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec
  5. 119 0
      modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props
  6. 34 0
      modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets
  7. 14 2
      modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
  8. 3 5
      modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj
  9. 4 1
      modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs
  10. 2 6
      modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
  11. 2 12
      modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs
  12. 12 3
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj
  13. 32 24
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs
  14. 0 0
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe
  15. 19 7
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
  16. 22 147
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs
  17. 196 175
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
  18. 33 37
      modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs
  19. 7 10
      modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
  20. 7 23
      modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
  21. 6 6
      modules/mono/editor/GodotTools/GodotTools/BuildTab.cs
  22. 52 64
      modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs
  23. 11 13
      modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
  24. 36 40
      modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
  25. 1 5
      modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
  26. 1 1
      modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
  27. 3 3
      modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
  28. 4 0
      modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs
  29. 12 15
      modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
  30. 0 1
      modules/mono/editor/bindings_generator.cpp
  31. 42 35
      modules/mono/editor/godotsharp_export.cpp
  32. 6 29
      modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
  33. 0 24
      modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs
  34. 14 34
      modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
  35. 0 25
      modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs
  36. 17 14
      modules/mono/mono_gd/gd_mono.cpp
  37. 1 0
      modules/mono/mono_gd/gd_mono.h
  38. 58 15
      modules/mono/mono_gd/gd_mono_assembly.cpp
  39. 4 2
      modules/mono/mono_gd/gd_mono_assembly.h
  40. 1 1
      modules/mono/mono_gd/gd_mono_log.cpp
  41. 21 0
      modules/mono/utils/macros.h

+ 4 - 0
.editorconfig

@@ -20,3 +20,7 @@ indent_size = 4
 [.travis.yml]
 [.travis.yml]
 indent_style = space
 indent_style = space
 indent_size = 2
 indent_size = 2
+
+[*.{csproj,props,targets,nuspec}]
+indent_style = space
+indent_size = 2

+ 16 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln

@@ -0,0 +1,16 @@
+
+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
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+EndGlobal

+ 35 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj

@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.Build.NoTargets/2.0.1">
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+
+    <Description>MSBuild .NET Sdk for Godot projects.</Description>
+    <Authors>Godot Engine contributors</Authors>
+
+    <PackageId>Godot.NET.Sdk</PackageId>
+    <Version>3.2.3</Version>
+    <PackageVersion>3.2.3</PackageVersion>
+    <PackageProjectUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</PackageProjectUrl>
+    <PackageType>MSBuildSdk</PackageType>
+    <PackageTags>MSBuildSdk</PackageTags>
+    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <NuspecFile>Godot.NET.Sdk.nuspec</NuspecFile>
+    <GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuSpecProperties</GenerateNuspecDependsOn>
+  </PropertyGroup>
+
+  <Target Name="SetNuSpecProperties" Condition=" Exists('$(NuspecFile)') ">
+    <PropertyGroup>
+      <NuspecProperties>
+        id=$(PackageId);
+        description=$(Description);
+        authors=$(Authors);
+        version=$(PackageVersion);
+        packagetype=$(PackageType);
+        tags=$(PackageTags);
+        projecturl=$(PackageProjectUrl)
+      </NuspecProperties>
+    </PropertyGroup>
+  </Target>
+</Project>

+ 22 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
+  <metadata>
+    <id>$id$</id>
+    <version>$version$</version>
+    <description>$description$</description>
+    <authors>$authors$</authors>
+    <owners>$authors$</owners>
+    <projectUrl>$projecturl$</projectUrl>
+    <requireLicenseAcceptance>false</requireLicenseAcceptance>
+    <license type="expression">MIT</license>
+    <licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
+    <tags>$tags$</tags>
+    <packageTypes>
+      <packageType name="$packagetype$" />
+    </packageTypes>
+    <repository url="$projecturl$" />
+  </metadata>
+  <files>
+    <file src="Sdk\**" target="Sdk" />\
+  </files>
+</package>

+ 119 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props

@@ -0,0 +1,119 @@
+<Project>
+  <PropertyGroup>
+    <!-- Determines if we should import Microsoft.NET.Sdk, if it wasn't already imported. -->
+    <GodotSdkImportsMicrosoftNetSdk Condition=" '$(UsingMicrosoftNETSdk)' != 'true' ">true</GodotSdkImportsMicrosoftNetSdk>
+
+    <GodotProjectTypeGuid>{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}</GodotProjectTypeGuid>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <Configurations>Debug;ExportDebug;ExportRelease</Configurations>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+
+    <GodotProjectDir Condition=" '$(SolutionDir)' != '' ">$(SolutionDir)</GodotProjectDir>
+    <GodotProjectDir Condition=" '$(SolutionDir)' == '' ">$(MSBuildProjectDirectory)</GodotProjectDir>
+    <GodotProjectDir>$([MSBuild]::EnsureTrailingSlash('$(GodotProjectDir)'))</GodotProjectDir>
+
+    <!-- Custom output paths for Godot projects. In brief, 'bin\' and 'obj\' are moved to '$(GodotProjectDir)\.mono\temp\'. -->
+    <BaseOutputPath>$(GodotProjectDir).mono\temp\bin\</BaseOutputPath>
+    <OutputPath>$(GodotProjectDir).mono\temp\bin\$(Configuration)\</OutputPath>
+    <!--
+    Use custom IntermediateOutputPath and BaseIntermediateOutputPath only if it wasn't already set.
+    Otherwise the old values may have already been changed by MSBuild which can cause problems with NuGet.
+    -->
+    <IntermediateOutputPath Condition=" '$(IntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\$(Configuration)\</IntermediateOutputPath>
+    <BaseIntermediateOutputPath Condition=" '$(BaseIntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\</BaseIntermediateOutputPath>
+
+    <!-- Do not append the target framework name to the output path. -->
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+  </PropertyGroup>
+
+  <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " />
+
+  <PropertyGroup>
+    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
+  </PropertyGroup>
+
+  <!--
+  The Microsoft.NET.Sdk only understands of the Debug and Release configurations.
+  We need to set the following properties manually for ExportDebug and ExportRelease.
+  -->
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' or '$(Configuration)' == 'ExportDebug' ">
+    <DebugSymbols Condition=" '$(DebugSymbols)' == '' ">true</DebugSymbols>
+    <Optimize Condition=" '$(Optimize)' == '' ">false</Optimize>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)' == 'ExportRelease' ">
+    <Optimize Condition=" '$(Optimize)' == '' ">true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <GodotApiConfiguration Condition=" '$(Configuration)' != 'ExportRelease' ">Debug</GodotApiConfiguration>
+    <GodotApiConfiguration Condition=" '$(Configuration)' == 'ExportRelease' ">Release</GodotApiConfiguration>
+  </PropertyGroup>
+
+  <!-- Auto-detect the target Godot platform if it was not specified. -->
+  <PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' ">
+    <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">x11</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">x11</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(OSX))' ">osx</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Windows))' ">windows</GodotTargetPlatform>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <GodotRealTIsDouble Condition=" '$(GodotRealTIsDouble)' == '' ">false</GodotRealTIsDouble>
+  </PropertyGroup>
+
+  <!-- Godot DefineConstants. -->
+  <PropertyGroup>
+    <!-- Define constant to identify Godot builds. -->
+    <GodotDefineConstants>GODOT</GodotDefineConstants>
+
+    <!--
+    Define constant to determine the target Godot platform. This includes the
+    recognized platform names and the platform category (PC, MOBILE or WEB).
+    -->
+    <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'windows' ">GODOT_WINDOWS;GODOT_PC</GodotPlatformConstants>
+    <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'x11' ">GODOT_X11;GODOT_PC</GodotPlatformConstants>
+    <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'osx' ">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)' == 'iphone' ">GODOT_IPHONE;GODOT_IOS;GODOT_MOBILE</GodotPlatformConstants>
+    <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'javascript' ">GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB</GodotPlatformConstants>
+
+    <GodotDefineConstants>$(GodotDefineConstants);$(GodotPlatformConstants)</GodotDefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <!-- ExportDebug also defines DEBUG like Debug does. -->
+    <DefineConstants Condition=" '$(Configuration)' == 'ExportDebug' ">$(DefineConstants);DEBUG</DefineConstants>
+    <!-- Debug defines TOOLS to differenciate between Debug and ExportDebug configurations. -->
+    <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">$(DefineConstants);TOOLS</DefineConstants>
+
+    <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
+  </PropertyGroup>
+
+  <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).mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath>
+    </Reference>
+    <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' ">
+      <Private>false</Private>
+      <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(AutomaticallyUseReferenceAssemblyPackages)' == '' and '$(MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion)' == '' ">
+    <!-- Old 'Microsoft.NET.Sdk' so we reference the 'Microsoft.NETFramework.ReferenceAssemblies' package ourselves. -->
+    <AutomaticallyUseReferenceAssemblyPackages>true</AutomaticallyUseReferenceAssemblyPackages>
+    <MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion>1.0.0</MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion>
+    <GodotUseNETFrameworkRefAssemblies>true</GodotUseNETFrameworkRefAssemblies>
+  </PropertyGroup>
+</Project>

+ 34 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets

@@ -0,0 +1,34 @@
+<Project>
+  <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " />
+
+  <PropertyGroup>
+    <EnableGodotProjectTypeGuid Condition=" '$(EnableGodotProjectTypeGuid)' == '' ">true</EnableGodotProjectTypeGuid>
+    <ProjectTypeGuids Condition=" '$(EnableGodotProjectTypeGuid)' == 'true' ">$(GodotProjectTypeGuid);$(DefaultProjectTypeGuid)</ProjectTypeGuids>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <!--
+    Define constant to determine whether the real_t type in Godot is double precision or not.
+    By default this is false, like the official Godot builds. If someone is using a custom
+    Godot build where real_t is double, they can override the GodotRealTIsDouble property.
+    -->
+    <DefineConstants Condition=" '$(GodotRealTIsDouble)' == 'true' ">GODOT_REAL_T_IS_DOUBLE;$(DefineConstants)</DefineConstants>
+  </PropertyGroup>
+
+  <!-- Backported from newer Microsoft.NET.Sdk version -->
+  <Target Name="IncludeTargetingPackReference" BeforeTargets="_GetRestoreSettingsPerFramework;_CheckForInvalidConfigurationAndPlatform"
+          Condition=" '$(GodotUseNETFrameworkRefAssemblies)' == 'true' and '$(TargetFrameworkMoniker)' != '' and '$(TargetFrameworkIdentifier)' == '.NETFramework' and '$(AutomaticallyUseReferenceAssemblyPackages)' == 'true' ">
+    <GetReferenceAssemblyPaths
+        TargetFrameworkMoniker="$(TargetFrameworkMoniker)"
+        RootPath="$(TargetFrameworkRootPath)"
+        TargetFrameworkFallbackSearchPaths="$(TargetFrameworkFallbackSearchPaths)"
+        BypassFrameworkInstallChecks="$(BypassFrameworkInstallChecks)"
+        SuppressNotFoundError="true">
+      <Output TaskParameter="FullFrameworkReferenceAssemblyPaths" PropertyName="_FullFrameworkReferenceAssemblyPaths"/>
+    </GetReferenceAssemblyPaths>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="$(MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion)" IsImplicitlyDefined="true" Condition="'$(_FullFrameworkReferenceAssemblyPaths)' == ''"/>
+    </ItemGroup>
+  </Target>
+</Project>

+ 14 - 2
modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs

@@ -2,7 +2,6 @@ using System;
 using System.IO;
 using System.IO;
 using System.Security;
 using System.Security;
 using Microsoft.Build.Framework;
 using Microsoft.Build.Framework;
-using GodotTools.Core;
 
 
 namespace GodotTools.BuildLogger
 namespace GodotTools.BuildLogger
 {
 {
@@ -18,7 +17,7 @@ namespace GodotTools.BuildLogger
             if (null == Parameters)
             if (null == Parameters)
                 throw new LoggerException("Log directory was not set.");
                 throw new LoggerException("Log directory was not set.");
 
 
-            var parameters = Parameters.Split(new[] { ';' });
+            var parameters = Parameters.Split(new[] {';'});
 
 
             string logDir = parameters[0];
             string logDir = parameters[0];
 
 
@@ -183,4 +182,17 @@ namespace GodotTools.BuildLogger
         private StreamWriter issuesStreamWriter;
         private StreamWriter issuesStreamWriter;
         private int indent;
         private int indent;
     }
     }
+
+    internal static class StringExtensions
+    {
+        public static string CsvEscape(this string value, char delimiter = ',')
+        {
+            bool hasSpecialChar = value.IndexOfAny(new[] {'\"', '\n', '\r', delimiter}) != -1;
+
+            if (hasSpecialChar)
+                return "\"" + value.Replace("\"", "\"\"") + "\"";
+
+            return value;
+        }
+    }
 }
 }

+ 3 - 5
modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj

@@ -1,13 +1,11 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
     <ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid>
     <ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid>
-    <OutputType>Library</OutputType>
-    <TargetFramework>net472</TargetFramework>
-    <LangVersion>7</LangVersion>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>7.2</LangVersion>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
-    <Reference Include="Microsoft.Build.Framework" />
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
     <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />

+ 4 - 1
modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs

@@ -19,7 +19,10 @@ namespace GodotTools.Core
             }
             }
 
 
             if (attempt > maxAttempts + 1)
             if (attempt > maxAttempts + 1)
-                return;
+            {
+                // Overwrite the oldest one
+                backupPath = backupPathBase;
+            }
 
 
             File.Copy(filePath, backupPath, overwrite: true);
             File.Copy(filePath, backupPath, overwrite: true);
         }
         }

+ 2 - 6
modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj

@@ -1,11 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
     <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
     <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
-    <OutputType>Library</OutputType>
-    <TargetFramework>net472</TargetFramework>
-    <LangVersion>7</LangVersion>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>7.2</LangVersion>
   </PropertyGroup>
   </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
-  </ItemGroup>
 </Project>
 </Project>

+ 2 - 12
modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs

@@ -34,23 +34,13 @@ namespace GodotTools.Core
             return rooted ? Path.DirectorySeparatorChar + path : path;
             return rooted ? Path.DirectorySeparatorChar + path : path;
         }
         }
 
 
-        private static readonly string driveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
+        private static readonly string DriveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
 
 
         public static bool IsAbsolutePath(this string path)
         public static bool IsAbsolutePath(this string path)
         {
         {
             return path.StartsWith("/", StringComparison.Ordinal) ||
             return path.StartsWith("/", StringComparison.Ordinal) ||
                    path.StartsWith("\\", StringComparison.Ordinal) ||
                    path.StartsWith("\\", StringComparison.Ordinal) ||
-                   path.StartsWith(driveRoot, StringComparison.Ordinal);
-        }
-
-        public static string CsvEscape(this string value, char delimiter = ',')
-        {
-            bool hasSpecialChar = value.IndexOfAny(new char[] { '\"', '\n', '\r', delimiter }) != -1;
-
-            if (hasSpecialChar)
-                return "\"" + value.Replace("\"", "\"\"") + "\"";
-
-            return value;
+                   path.StartsWith(DriveRoot, StringComparison.Ordinal);
         }
         }
 
 
         public static string ToSafeDirName(this string dirName, bool allowDirSeparator)
         public static string ToSafeDirName(this string dirName, bool allowDirSeparator)

+ 12 - 3
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj

@@ -1,16 +1,25 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
     <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
     <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
-    <OutputType>Library</OutputType>
     <TargetFramework>net472</TargetFramework>
     <TargetFramework>net472</TargetFramework>
-    <LangVersion>7</LangVersion>
+    <LangVersion>7.2</LangVersion>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
     <Reference Include="Microsoft.Build" />
     <Reference Include="Microsoft.Build" />
-    <PackageReference Include="DotNet.Glob" Version="2.1.1" />
+    <PackageReference Include="Microsoft.Build" Version="16.5.0" />
+    <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="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
     <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
   </ItemGroup>
   </ItemGroup>
+  <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.
+    -->
+    <None Include="MSBuild.exe" CopyToOutputDirectory="Always" />
+  </ItemGroup>
 </Project>
 </Project>

+ 32 - 24
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs

@@ -22,6 +22,37 @@ namespace GodotTools.ProjectEditor
             return string.Join(".", identifiers);
             return string.Join(".", identifiers);
         }
         }
 
 
+        /// <summary>
+        /// Skips invalid identifier characters including decimal digit numbers at the start of the identifier.
+        /// </summary>
+        private static void SkipInvalidCharacters(string source, int startIndex, StringBuilder outputBuilder)
+        {
+            for (int i = startIndex; i < source.Length; i++)
+            {
+                char @char = source[i];
+
+                switch (char.GetUnicodeCategory(@char))
+                {
+                    case UnicodeCategory.UppercaseLetter:
+                    case UnicodeCategory.LowercaseLetter:
+                    case UnicodeCategory.TitlecaseLetter:
+                    case UnicodeCategory.ModifierLetter:
+                    case UnicodeCategory.LetterNumber:
+                    case UnicodeCategory.OtherLetter:
+                        outputBuilder.Append(@char);
+                        break;
+                    case UnicodeCategory.NonSpacingMark:
+                    case UnicodeCategory.SpacingCombiningMark:
+                    case UnicodeCategory.ConnectorPunctuation:
+                    case UnicodeCategory.DecimalDigitNumber:
+                        // Identifiers may start with underscore
+                        if (outputBuilder.Length > startIndex || @char == '_')
+                            outputBuilder.Append(@char);
+                        break;
+                }
+            }
+        }
+
         public static string SanitizeIdentifier(string identifier, bool allowEmpty)
         public static string SanitizeIdentifier(string identifier, bool allowEmpty)
         {
         {
             if (string.IsNullOrEmpty(identifier))
             if (string.IsNullOrEmpty(identifier))
@@ -44,30 +75,7 @@ namespace GodotTools.ProjectEditor
                 startIndex += 1;
                 startIndex += 1;
             }
             }
 
 
-            for (int i = startIndex; i < identifier.Length; i++)
-            {
-                char @char = identifier[i];
-
-                switch (Char.GetUnicodeCategory(@char))
-                {
-                    case UnicodeCategory.UppercaseLetter:
-                    case UnicodeCategory.LowercaseLetter:
-                    case UnicodeCategory.TitlecaseLetter:
-                    case UnicodeCategory.ModifierLetter:
-                    case UnicodeCategory.LetterNumber:
-                    case UnicodeCategory.OtherLetter:
-                        identifierBuilder.Append(@char);
-                        break;
-                    case UnicodeCategory.NonSpacingMark:
-                    case UnicodeCategory.SpacingCombiningMark:
-                    case UnicodeCategory.ConnectorPunctuation:
-                    case UnicodeCategory.DecimalDigitNumber:
-                        // Identifiers may start with underscore
-                        if (identifierBuilder.Length > startIndex || @char == '_')
-                            identifierBuilder.Append(@char);
-                        break;
-                }
-            }
+            SkipInvalidCharacters(identifier, startIndex, identifierBuilder);
 
 
             if (identifierBuilder.Length == startIndex)
             if (identifierBuilder.Length == startIndex)
             {
             {

+ 0 - 0
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe


+ 19 - 7
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs

@@ -2,8 +2,9 @@ using GodotTools.Core;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
-using DotNet.Globbing;
+using System.Linq;
 using Microsoft.Build.Construction;
 using Microsoft.Build.Construction;
+using Microsoft.Build.Globbing;
 
 
 namespace GodotTools.ProjectEditor
 namespace GodotTools.ProjectEditor
 {
 {
@@ -11,8 +12,6 @@ namespace GodotTools.ProjectEditor
     {
     {
         public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
         public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
         {
         {
-            GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}};
-
             string normalizedInclude = include.NormalizePath();
             string normalizedInclude = include.NormalizePath();
 
 
             foreach (var itemGroup in root.ItemGroups)
             foreach (var itemGroup in root.ItemGroups)
@@ -25,7 +24,7 @@ namespace GodotTools.ProjectEditor
                     if (item.ItemType != itemType)
                     if (item.ItemType != itemType)
                         continue;
                         continue;
 
 
-                    var glob = Glob.Parse(item.Include.NormalizePath(), globOptions);
+                    var glob = MSBuildGlob.Parse(item.Include.NormalizePath());
 
 
                     if (glob.IsMatch(normalizedInclude))
                     if (glob.IsMatch(normalizedInclude))
                         return item;
                         return item;
@@ -34,10 +33,9 @@ namespace GodotTools.ProjectEditor
 
 
             return null;
             return null;
         }
         }
+
         public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
         public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
         {
         {
-            GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}};
-
             string normalizedInclude = Path.GetFullPath(include).NormalizePath();
             string normalizedInclude = Path.GetFullPath(include).NormalizePath();
 
 
             foreach (var itemGroup in root.ItemGroups)
             foreach (var itemGroup in root.ItemGroups)
@@ -50,7 +48,7 @@ namespace GodotTools.ProjectEditor
                     if (item.ItemType != itemType)
                     if (item.ItemType != itemType)
                         continue;
                         continue;
 
 
-                    var glob = Glob.Parse(Path.GetFullPath(item.Include).NormalizePath(), globOptions);
+                    var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath());
 
 
                     if (glob.IsMatch(normalizedInclude))
                     if (glob.IsMatch(normalizedInclude))
                         return item;
                         return item;
@@ -117,5 +115,19 @@ namespace GodotTools.ProjectEditor
 
 
             return Guid.Empty;
             return Guid.Empty;
         }
         }
+
+        public static bool AreDefaultCompileItemsEnabled(this ProjectRootElement root)
+        {
+            var enableDefaultCompileItemsProps = root.PropertyGroups
+                .Where(g => string.IsNullOrEmpty(g.Condition))
+                .SelectMany(g => g.Properties
+                    .Where(p => p.Name == "EnableDefaultCompileItems" && string.IsNullOrEmpty(p.Condition)));
+
+            bool enableDefaultCompileItems = true;
+            foreach (var prop in enableDefaultCompileItemsProps)
+                enableDefaultCompileItems = prop.Value.Equals("true", StringComparison.OrdinalIgnoreCase);
+
+            return enableDefaultCompileItems;
+        }
     }
     }
 }
 }

+ 22 - 147
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs

@@ -1,174 +1,49 @@
-using GodotTools.Core;
 using System;
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.IO;
-using System.Reflection;
 using Microsoft.Build.Construction;
 using Microsoft.Build.Construction;
+using Microsoft.Build.Evaluation;
 
 
 namespace GodotTools.ProjectEditor
 namespace GodotTools.ProjectEditor
 {
 {
     public static class ProjectGenerator
     public static class ProjectGenerator
     {
     {
-        private const string CoreApiProjectName = "GodotSharp";
-        private const string EditorApiProjectName = "GodotSharpEditor";
+        public const string GodotSdkVersionToUse = "3.2.3";
 
 
-        public const string CSharpProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
-        public const string GodotProjectTypeGuid = "{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}";
+        public static string GodotSdkAttrValue => $"Godot.NET.Sdk/{GodotSdkVersionToUse}";
 
 
-        public static readonly string GodotDefaultProjectTypeGuids = $"{GodotProjectTypeGuid};{CSharpProjectTypeGuid}";
-
-        public static string GenGameProject(string dir, string name, IEnumerable<string> compileItems)
-        {
-            string path = Path.Combine(dir, name + ".csproj");
-
-            ProjectPropertyGroupElement mainGroup;
-            var root = CreateLibraryProject(name, "Debug", out mainGroup);
-
-            mainGroup.SetProperty("ProjectTypeGuids", GodotDefaultProjectTypeGuids);
-            mainGroup.SetProperty("OutputPath", Path.Combine(".mono", "temp", "bin", "$(Configuration)"));
-            mainGroup.SetProperty("BaseIntermediateOutputPath", Path.Combine(".mono", "temp", "obj"));
-            mainGroup.SetProperty("IntermediateOutputPath", Path.Combine("$(BaseIntermediateOutputPath)", "$(Configuration)"));
-            mainGroup.SetProperty("ApiConfiguration", "Debug").Condition = " '$(Configuration)' != 'ExportRelease' ";
-            mainGroup.SetProperty("ApiConfiguration", "Release").Condition = " '$(Configuration)' == 'ExportRelease' ";
-
-            var debugGroup = root.AddPropertyGroup();
-            debugGroup.Condition = " '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ";
-            debugGroup.AddProperty("DebugSymbols", "true");
-            debugGroup.AddProperty("DebugType", "portable");
-            debugGroup.AddProperty("Optimize", "false");
-            debugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;TOOLS;");
-            debugGroup.AddProperty("ErrorReport", "prompt");
-            debugGroup.AddProperty("WarningLevel", "4");
-            debugGroup.AddProperty("ConsolePause", "false");
-
-            var coreApiRef = root.AddItem("Reference", CoreApiProjectName);
-            coreApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", CoreApiProjectName + ".dll"));
-            coreApiRef.AddMetadata("Private", "False");
-
-            var editorApiRef = root.AddItem("Reference", EditorApiProjectName);
-            editorApiRef.Condition = " '$(Configuration)' == 'Debug' ";
-            editorApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", EditorApiProjectName + ".dll"));
-            editorApiRef.AddMetadata("Private", "False");
-
-            GenAssemblyInfoFile(root, dir, name);
-
-            foreach (var item in compileItems)
-            {
-                root.AddItem("Compile", item.RelativeToPath(dir).Replace("/", "\\"));
-            }
-
-            root.Save(path);
-
-            return root.GetGuid().ToString().ToUpper();
-        }
-
-        private static void GenAssemblyInfoFile(ProjectRootElement root, string dir, string name, string[] assemblyLines = null, string[] usingDirectives = null)
+        public static ProjectRootElement GenGameProject(string name)
         {
         {
-            string propertiesDir = Path.Combine(dir, "Properties");
-            if (!Directory.Exists(propertiesDir))
-                Directory.CreateDirectory(propertiesDir);
-
-            string usingDirectivesText = string.Empty;
+            if (name.Length == 0)
+                throw new ArgumentException("Project name is empty", nameof(name));
 
 
-            if (usingDirectives != null)
-            {
-                foreach (var usingDirective in usingDirectives)
-                    usingDirectivesText += "\nusing " + usingDirective + ";";
-            }
+            var root = ProjectRootElement.Create(NewProjectFileOptions.None);
 
 
-            string assemblyLinesText = string.Empty;
+            root.Sdk = GodotSdkAttrValue;
 
 
-            if (assemblyLines != null)
-                assemblyLinesText += string.Join("\n", assemblyLines) + "\n";
+            var mainGroup = root.AddPropertyGroup();
+            mainGroup.AddProperty("TargetFramework", "net472");
 
 
-            string content = string.Format(AssemblyInfoTemplate, usingDirectivesText, name, assemblyLinesText);
+            string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true);
 
 
-            string assemblyInfoFile = Path.Combine(propertiesDir, "AssemblyInfo.cs");
+            // If the name is not a valid namespace, manually set RootNamespace to a sanitized one.
+            if (sanitizedName != name)
+                mainGroup.AddProperty("RootNamespace", sanitizedName);
 
 
-            File.WriteAllText(assemblyInfoFile, content);
-
-            root.AddItem("Compile", assemblyInfoFile.RelativeToPath(dir).Replace("/", "\\"));
+            return root;
         }
         }
 
 
-        public static ProjectRootElement CreateLibraryProject(string name, string defaultConfig, out ProjectPropertyGroupElement mainGroup)
+        public static string GenAndSaveGameProject(string dir, string name)
         {
         {
-            if (string.IsNullOrEmpty(name))
-                throw new ArgumentException($"{nameof(name)} cannot be empty", nameof(name));
-
-            var root = ProjectRootElement.Create();
-            root.DefaultTargets = "Build";
-
-            mainGroup = root.AddPropertyGroup();
-            mainGroup.AddProperty("Configuration", defaultConfig).Condition = " '$(Configuration)' == '' ";
-            mainGroup.AddProperty("Platform", "AnyCPU").Condition = " '$(Platform)' == '' ";
-            mainGroup.AddProperty("ProjectGuid", "{" + Guid.NewGuid().ToString().ToUpper() + "}");
-            mainGroup.AddProperty("OutputType", "Library");
-            mainGroup.AddProperty("OutputPath", Path.Combine("bin", "$(Configuration)"));
-            mainGroup.AddProperty("RootNamespace", IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true));
-            mainGroup.AddProperty("AssemblyName", name);
-            mainGroup.AddProperty("TargetFrameworkVersion", "v4.7");
-            mainGroup.AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString());
+            if (name.Length == 0)
+                throw new ArgumentException("Project name is empty", nameof(name));
 
 
-            var exportDebugGroup = root.AddPropertyGroup();
-            exportDebugGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportDebug|AnyCPU' ";
-            exportDebugGroup.AddProperty("DebugSymbols", "true");
-            exportDebugGroup.AddProperty("DebugType", "portable");
-            exportDebugGroup.AddProperty("Optimize", "false");
-            exportDebugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;");
-            exportDebugGroup.AddProperty("ErrorReport", "prompt");
-            exportDebugGroup.AddProperty("WarningLevel", "4");
-            exportDebugGroup.AddProperty("ConsolePause", "false");
-
-            var exportReleaseGroup = root.AddPropertyGroup();
-            exportReleaseGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportRelease|AnyCPU' ";
-            exportReleaseGroup.AddProperty("DebugType", "portable");
-            exportReleaseGroup.AddProperty("Optimize", "true");
-            exportReleaseGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;");
-            exportReleaseGroup.AddProperty("ErrorReport", "prompt");
-            exportReleaseGroup.AddProperty("WarningLevel", "4");
-            exportReleaseGroup.AddProperty("ConsolePause", "false");
-
-            // References
-            var referenceGroup = root.AddItemGroup();
-            referenceGroup.AddItem("Reference", "System");
-            var frameworkRefAssembliesItem = referenceGroup.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies");
+            string path = Path.Combine(dir, name + ".csproj");
 
 
-            // Use metadata (child nodes) instead of attributes for the PackageReference.
-            // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build.
-            frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0");
-            frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All");
+            var root = GenGameProject(name);
 
 
-            root.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\"));
+            root.Save(path);
 
 
-            return root;
+            return Guid.NewGuid().ToString().ToUpper();
         }
         }
-
-        private const string AssemblyInfoTemplate =
-            @"using System.Reflection;{0}
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle(""{1}"")]
-[assembly: AssemblyDescription("""")]
-[assembly: AssemblyConfiguration("""")]
-[assembly: AssemblyCompany("""")]
-[assembly: AssemblyProduct("""")]
-[assembly: AssemblyCopyright("""")]
-[assembly: AssemblyTrademark("""")]
-[assembly: AssemblyCulture("""")]
-
-// The assembly version has the format ""{{Major}}.{{Minor}}.{{Build}}.{{Revision}}"".
-// The form ""{{Major}}.{{Minor}}.*"" will automatically update the build and revision,
-// and ""{{Major}}.{{Minor}}.{{Build}}.*"" will update just the revision.
-
-[assembly: AssemblyVersion(""1.0.*"")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("""")]
-{2}";
     }
     }
 }
 }

+ 196 - 175
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs

@@ -1,17 +1,20 @@
+using System;
 using GodotTools.Core;
 using GodotTools.Core;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Reflection;
-using DotNet.Globbing;
+using System.Xml;
+using System.Xml.Linq;
+using JetBrains.Annotations;
 using Microsoft.Build.Construction;
 using Microsoft.Build.Construction;
+using Microsoft.Build.Globbing;
 
 
 namespace GodotTools.ProjectEditor
 namespace GodotTools.ProjectEditor
 {
 {
     public sealed class MSBuildProject
     public sealed class MSBuildProject
     {
     {
-        public ProjectRootElement Root { get; }
+        internal ProjectRootElement Root { get; set; }
 
 
         public bool HasUnsavedChanges { get; set; }
         public bool HasUnsavedChanges { get; set; }
 
 
@@ -31,12 +34,20 @@ namespace GodotTools.ProjectEditor
             return root != null ? new MSBuildProject(root) : null;
             return root != null ? new MSBuildProject(root) : null;
         }
         }
 
 
+        [PublicAPI]
         public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
         public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
         {
         {
             var dir = Directory.GetParent(projectPath).FullName;
             var dir = Directory.GetParent(projectPath).FullName;
             var root = ProjectRootElement.Open(projectPath);
             var root = ProjectRootElement.Open(projectPath);
             Debug.Assert(root != null);
             Debug.Assert(root != null);
 
 
+            if (root.AreDefaultCompileItemsEnabled())
+            {
+                // No need to add. It's already included automatically by the MSBuild Sdk.
+                // This assumes the source file is inside the project directory and not manually excluded in the csproj
+                return;
+            }
+
             var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\");
             var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\");
 
 
             if (root.AddItemChecked(itemType, normalizedInclude))
             if (root.AddItemChecked(itemType, normalizedInclude))
@@ -49,6 +60,13 @@ namespace GodotTools.ProjectEditor
             var root = ProjectRootElement.Open(projectPath);
             var root = ProjectRootElement.Open(projectPath);
             Debug.Assert(root != null);
             Debug.Assert(root != null);
 
 
+            if (root.AreDefaultCompileItemsEnabled())
+            {
+                // No need to add. It's already included automatically by the MSBuild Sdk.
+                // This assumes the source file is inside the project directory and not manually excluded in the csproj
+                return;
+            }
+
             var normalizedOldInclude = oldInclude.NormalizePath();
             var normalizedOldInclude = oldInclude.NormalizePath();
             var normalizedNewInclude = newInclude.NormalizePath();
             var normalizedNewInclude = newInclude.NormalizePath();
 
 
@@ -66,6 +84,13 @@ namespace GodotTools.ProjectEditor
             var root = ProjectRootElement.Open(projectPath);
             var root = ProjectRootElement.Open(projectPath);
             Debug.Assert(root != null);
             Debug.Assert(root != null);
 
 
+            if (root.AreDefaultCompileItemsEnabled())
+            {
+                // No need to add. It's already included automatically by the MSBuild Sdk.
+                // This assumes the source file is inside the project directory and not manually excluded in the csproj
+                return;
+            }
+
             var normalizedInclude = include.NormalizePath();
             var normalizedInclude = include.NormalizePath();
 
 
             if (root.RemoveItemChecked(itemType, normalizedInclude))
             if (root.RemoveItemChecked(itemType, normalizedInclude))
@@ -78,6 +103,13 @@ namespace GodotTools.ProjectEditor
             var root = ProjectRootElement.Open(projectPath);
             var root = ProjectRootElement.Open(projectPath);
             Debug.Assert(root != null);
             Debug.Assert(root != null);
 
 
+            if (root.AreDefaultCompileItemsEnabled())
+            {
+                // No need to add. It's already included automatically by the MSBuild Sdk.
+                // This assumes the source file is inside the project directory and not manually excluded in the csproj
+                return;
+            }
+
             bool dirty = false;
             bool dirty = false;
 
 
             var oldFolderNormalized = oldFolder.NormalizePath();
             var oldFolderNormalized = oldFolder.NormalizePath();
@@ -102,6 +134,13 @@ namespace GodotTools.ProjectEditor
             var root = ProjectRootElement.Open(projectPath);
             var root = ProjectRootElement.Open(projectPath);
             Debug.Assert(root != null);
             Debug.Assert(root != null);
 
 
+            if (root.AreDefaultCompileItemsEnabled())
+            {
+                // No need to add. It's already included automatically by the MSBuild Sdk.
+                // This assumes the source file is inside the project directory and not manually excluded in the csproj
+                return;
+            }
+
             var folderNormalized = folder.NormalizePath();
             var folderNormalized = folder.NormalizePath();
 
 
             var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList();
             var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList();
@@ -133,12 +172,32 @@ namespace GodotTools.ProjectEditor
             var result = new List<string>();
             var result = new List<string>();
             var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
             var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
 
 
-            var globOptions = new GlobOptions();
-            globOptions.Evaluation.CaseInsensitive = false;
-
             var root = ProjectRootElement.Open(projectPath);
             var root = ProjectRootElement.Open(projectPath);
             Debug.Assert(root != null);
             Debug.Assert(root != null);
 
 
+            if (root.AreDefaultCompileItemsEnabled())
+            {
+                var excluded = new List<string>();
+                result = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs").ToList();
+
+                foreach (var item in root.Items)
+                {
+                    if (string.IsNullOrEmpty(item.Condition))
+                        continue;
+
+                    if (item.ItemType != itemType)
+                        continue;
+
+                    string normalizedExclude = item.Exclude.NormalizePath();
+
+                    var glob = MSBuildGlob.Parse(normalizedExclude);
+
+                    excluded.AddRange(result.Where(includedFile => glob.IsMatch(includedFile)));
+                }
+
+                result.RemoveAll(f => excluded.Contains(f));
+            }
+
             foreach (var itemGroup in root.ItemGroups)
             foreach (var itemGroup in root.ItemGroups)
             {
             {
                 if (itemGroup.Condition.Length != 0)
                 if (itemGroup.Condition.Length != 0)
@@ -151,9 +210,7 @@ namespace GodotTools.ProjectEditor
 
 
                     string normalizedInclude = item.Include.NormalizePath();
                     string normalizedInclude = item.Include.NormalizePath();
 
 
-                    var glob = Glob.Parse(normalizedInclude, globOptions);
-
-                    // TODO Check somehow if path has no blob to avoid the following loop...
+                    var glob = MSBuildGlob.Parse(normalizedInclude);
 
 
                     foreach (var existingFile in existingFiles)
                     foreach (var existingFile in existingFiles)
                     {
                     {
@@ -168,222 +225,186 @@ namespace GodotTools.ProjectEditor
             return result.ToArray();
             return result.ToArray();
         }
         }
 
 
-        public static void EnsureHasProjectTypeGuids(MSBuildProject project)
+        public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName)
         {
         {
             var root = project.Root;
             var root = project.Root;
 
 
-            bool found = root.PropertyGroups.Any(pg =>
-                string.IsNullOrEmpty(pg.Condition) && pg.Properties.Any(p => p.Name == "ProjectTypeGuids"));
-
-            if (found)
+            if (!string.IsNullOrEmpty(root.Sdk))
                 return;
                 return;
 
 
-            root.AddProperty("ProjectTypeGuids", ProjectGenerator.GodotDefaultProjectTypeGuids);
+            root.Sdk = ProjectGenerator.GodotSdkAttrValue;
 
 
-            project.HasUnsavedChanges = true;
-        }
+            root.ToolsVersion = null;
+            root.DefaultTargets = null;
 
 
-        ///  Simple function to make sure the Api assembly references are configured correctly
-        public static void FixApiHintPath(MSBuildProject project)
-        {
-            var root = project.Root;
+            root.AddProperty("TargetFramework", "net472");
 
 
-            void AddPropertyIfNotPresent(string name, string condition, string value)
-            {
-                if (root.PropertyGroups
-                    .Any(g => (string.IsNullOrEmpty(g.Condition) || g.Condition.Trim() == condition) &&
-                              g.Properties
-                                  .Any(p => p.Name == name &&
-                                            p.Value == value &&
-                                            (p.Condition.Trim() == condition || g.Condition.Trim() == condition))))
-                {
-                    return;
-                }
+            // Remove obsolete properties, items and elements. We're going to be conservative
+            // here to minimize the chances of introducing breaking changes. As such we will
+            // only remove elements that could potentially cause issues with the Godot.NET.Sdk.
 
 
-                root.AddProperty(name, value).Condition = " " + condition + " ";
-                project.HasUnsavedChanges = true;
+            void RemoveElements(IEnumerable<ProjectElement> elements)
+            {
+                foreach (var element in elements)
+                    element.Parent.RemoveChild(element);
             }
             }
 
 
-            AddPropertyIfNotPresent(name: "ApiConfiguration",
-                condition: "'$(Configuration)' != 'ExportRelease'",
-                value: "Debug");
-            AddPropertyIfNotPresent(name: "ApiConfiguration",
-                condition: "'$(Configuration)' == 'ExportRelease'",
-                value: "Release");
+            // Default Configuration
+
+            RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
+                .Where(p => p.Name == "Configuration" && p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Debug"));
+
+            // Default Platform
 
 
-            void SetReferenceHintPath(string referenceName, string condition, string hintPath)
+            RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
+                .Where(p => p.Name == "Platform" && p.Condition.Trim() == "'$(Platform)' == ''" && p.Value == "AnyCPU"));
+
+            // Simple properties
+
+            var yabaiProperties = new[]
             {
             {
-                foreach (var itemGroup in root.ItemGroups.Where(g =>
-                    g.Condition.Trim() == string.Empty || g.Condition.Trim() == condition))
-                {
-                    var references = itemGroup.Items.Where(item =>
-                        item.ItemType == "Reference" &&
-                        item.Include == referenceName &&
-                        (item.Condition.Trim() == condition || itemGroup.Condition.Trim() == condition));
+                "OutputPath",
+                "BaseIntermediateOutputPath",
+                "IntermediateOutputPath",
+                "TargetFrameworkVersion",
+                "ProjectTypeGuids",
+                "ApiConfiguration"
+            };
 
 
-                    var referencesWithHintPath = references.Where(reference =>
-                        reference.Metadata.Any(m => m.Name == "HintPath"));
+            RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
+                .Where(p => yabaiProperties.Contains(p.Name)));
 
 
-                    if (referencesWithHintPath.Any(reference => reference.Metadata
-                        .Any(m => m.Name == "HintPath" && m.Value == hintPath)))
-                    {
-                        // Found a Reference item with the right HintPath
-                        return;
-                    }
+            // Configuration dependent properties
 
 
-                    var referenceWithHintPath = referencesWithHintPath.FirstOrDefault();
-                    if (referenceWithHintPath != null)
-                    {
-                        // Found a Reference item with a wrong HintPath
-                        foreach (var metadata in referenceWithHintPath.Metadata.ToList()
-                            .Where(m => m.Name == "HintPath"))
-                        {
-                            // Safe to remove as we duplicate with ToList() to loop
-                            referenceWithHintPath.RemoveChild(metadata);
-                        }
+            var yabaiPropertiesForConfigs = new[]
+            {
+                "DebugSymbols",
+                "DebugType",
+                "Optimize",
+                "DefineConstants",
+                "ErrorReport",
+                "WarningLevel",
+                "ConsolePause"
+            };
+
+            foreach (var config in new[] {"ExportDebug", "ExportRelease", "Debug"})
+            {
+                var group = root.PropertyGroups
+                    .First(g => g.Condition.Trim() == $"'$(Configuration)|$(Platform)' == '{config}|AnyCPU'");
 
 
-                        referenceWithHintPath.AddMetadata("HintPath", hintPath);
-                        project.HasUnsavedChanges = true;
-                        return;
-                    }
+                RemoveElements(group.Properties.Where(p => yabaiPropertiesForConfigs.Contains(p.Name)));
 
 
-                    var referenceWithoutHintPath = references.FirstOrDefault();
-                    if (referenceWithoutHintPath != null)
-                    {
-                        // Found a Reference item without a HintPath
-                        referenceWithoutHintPath.AddMetadata("HintPath", hintPath);
-                        project.HasUnsavedChanges = true;
-                        return;
-                    }
+                if (group.Count == 0)
+                {
+                    // No more children, safe to delete the group
+                    group.Parent.RemoveChild(group);
                 }
                 }
-
-                // Found no Reference item at all. Add it.
-                root.AddItem("Reference", referenceName).Condition = " " + condition + " ";
-                project.HasUnsavedChanges = true;
             }
             }
 
 
-            const string coreProjectName = "GodotSharp";
-            const string editorProjectName = "GodotSharpEditor";
+            // Godot API References
 
 
-            const string coreCondition = "";
-            const string editorCondition = "'$(Configuration)' == 'Debug'";
+            var apiAssemblies = new[] {ApiAssemblyNames.Core, ApiAssemblyNames.Editor};
 
 
-            var coreHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{coreProjectName}.dll";
-            var editorHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{editorProjectName}.dll";
+            RemoveElements(root.ItemGroups.SelectMany(g => g.Items)
+                .Where(i => i.ItemType == "Reference" && apiAssemblies.Contains(i.Include)));
 
 
-            SetReferenceHintPath(coreProjectName, coreCondition, coreHintPath);
-            SetReferenceHintPath(editorProjectName, editorCondition, editorHintPath);
-        }
+            // Microsoft.NETFramework.ReferenceAssemblies PackageReference
 
 
-        public static void MigrateFromOldConfigNames(MSBuildProject project)
-        {
-            var root = project.Root;
+            RemoveElements(root.ItemGroups.SelectMany(g => g.Items).Where(i =>
+                i.ItemType == "PackageReference" &&
+                i.Include.Equals("Microsoft.NETFramework.ReferenceAssemblies", StringComparison.OrdinalIgnoreCase)));
 
 
-            bool hasGodotProjectGeneratorVersion = false;
-            bool foundOldConfiguration = false;
+            // Imports
 
 
-            foreach (var propertyGroup in root.PropertyGroups.Where(g => string.IsNullOrEmpty(g.Condition)))
+            var yabaiImports = new[]
             {
             {
-                if (!hasGodotProjectGeneratorVersion && propertyGroup.Properties.Any(p => p.Name == "GodotProjectGeneratorVersion"))
-                    hasGodotProjectGeneratorVersion = true;
+                "$(MSBuildBinPath)/Microsoft.CSharp.targets",
+                "$(MSBuildBinPath)Microsoft.CSharp.targets"
+            };
+
+            RemoveElements(root.Imports.Where(import => yabaiImports.Contains(
+                import.Project.Replace("\\", "/").Replace("//", "/"))));
+
+            // 'EnableDefaultCompileItems' and 'GenerateAssemblyInfo' are kept enabled by default
+            // on new projects, but when migrating old projects we disable them to avoid errors.
+            root.AddProperty("EnableDefaultCompileItems", "false");
+            root.AddProperty("GenerateAssemblyInfo", "false");
+
+            // Older AssemblyInfo.cs cause the following error:
+            // 'Properties/AssemblyInfo.cs(19,28): error CS8357:
+            // The specified version string contains wildcards, which are not compatible with determinism.
+            // Either remove wildcards from the version string, or disable determinism for this compilation.'
+            // We disable deterministic builds to prevent this. The user can then fix this manually when desired
+            // by fixing 'AssemblyVersion("1.0.*")' to not use wildcards.
+            root.AddProperty("Deterministic", "false");
 
 
-                foreach (var configItem in propertyGroup.Properties
-                    .Where(p => p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Tools"))
-                {
-                    configItem.Value = "Debug";
-                    foundOldConfiguration = true;
-                    project.HasUnsavedChanges = true;
-                }
-            }
+            project.HasUnsavedChanges = true;
 
 
-            if (!hasGodotProjectGeneratorVersion)
-            {
-                root.PropertyGroups.First(g => string.IsNullOrEmpty(g.Condition))?
-                    .AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString());
-                project.HasUnsavedChanges = true;
-            }
+            var xDoc = XDocument.Parse(root.RawXml);
 
 
-            if (!foundOldConfiguration)
-            {
-                var toolsConditions = new[]
-                {
-                    "'$(Configuration)|$(Platform)' == 'Tools|AnyCPU'",
-                    "'$(Configuration)|$(Platform)' != 'Tools|AnyCPU'",
-                    "'$(Configuration)' == 'Tools'",
-                    "'$(Configuration)' != 'Tools'"
-                };
-
-                foundOldConfiguration = root.PropertyGroups
-                    .Any(g => toolsConditions.Any(c => c == g.Condition.Trim()));
-            }
+            if (xDoc.Root == null)
+                return; // Too bad, we will have to keep the xmlns/namespace and xml declaration
 
 
-            if (foundOldConfiguration)
+            XElement GetElement(XDocument doc, string name, string value, string parentName)
             {
             {
-                void MigrateConfigurationConditions(string oldConfiguration, string newConfiguration)
+                foreach (var node in doc.DescendantNodes())
                 {
                 {
-                    void MigrateConditions(string oldCondition, string newCondition)
+                    if (!(node is XElement element))
+                        continue;
+                    if (element.Name.LocalName.Equals(name) && element.Value == value &&
+                        element.Parent != null && element.Parent.Name.LocalName.Equals(parentName))
                     {
                     {
-                        foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition.Trim() == oldCondition))
-                        {
-                            propertyGroup.Condition = " " + newCondition + " ";
-                            project.HasUnsavedChanges = true;
-                        }
+                        return element;
+                    }
+                }
 
 
-                        foreach (var propertyGroup in root.PropertyGroups)
-                        {
-                            foreach (var prop in propertyGroup.Properties.Where(p => p.Condition.Trim() == oldCondition))
-                            {
-                                prop.Condition = " " + newCondition + " ";
-                                project.HasUnsavedChanges = true;
-                            }
-                        }
+                return null;
+            }
 
 
-                        foreach (var itemGroup in root.ItemGroups.Where(g => g.Condition.Trim() == oldCondition))
-                        {
-                            itemGroup.Condition = " " + newCondition + " ";
-                            project.HasUnsavedChanges = true;
-                        }
+            // Add comment about Microsoft.NET.Sdk properties disabled during migration
 
 
-                        foreach (var itemGroup in root.ItemGroups)
-                        {
-                            foreach (var item in itemGroup.Items.Where(item => item.Condition.Trim() == oldCondition))
-                            {
-                                item.Condition = " " + newCondition + " ";
-                                project.HasUnsavedChanges = true;
-                            }
-                        }
-                    }
+            GetElement(xDoc, name: "EnableDefaultCompileItems", value: "false", parentName: "PropertyGroup")
+                .AddBeforeSelf(new XComment("The following properties were overriden during migration to prevent errors.\n" +
+                                            "    Enabling them may require other manual changes to the project and its files."));
 
 
-                    foreach (var op in new[] {"==", "!="})
+            void RemoveNamespace(XElement element)
+            {
+                element.Attributes().Where(x => x.IsNamespaceDeclaration).Remove();
+                element.Name = element.Name.LocalName;
+
+                foreach (var node in element.DescendantNodes())
+                {
+                    if (node is XElement xElement)
                     {
                     {
-                        MigrateConditions($"'$(Configuration)|$(Platform)' {op} '{oldConfiguration}|AnyCPU'", $"'$(Configuration)|$(Platform)' {op} '{newConfiguration}|AnyCPU'");
-                        MigrateConditions($"'$(Configuration)' {op} '{oldConfiguration}'", $"'$(Configuration)' {op} '{newConfiguration}'");
+                        // Need to do the same for all children recursively as it adds it to them for some reason...
+                        RemoveNamespace(xElement);
                     }
                     }
                 }
                 }
-
-                MigrateConfigurationConditions("Debug", "ExportDebug");
-                MigrateConfigurationConditions("Release", "ExportRelease");
-                MigrateConfigurationConditions("Tools", "Debug"); // Must be last
             }
             }
+
+            // Remove xmlns/namespace
+            RemoveNamespace(xDoc.Root);
+
+            // Remove xml declaration
+            xDoc.Nodes().FirstOrDefault(node => node.NodeType == XmlNodeType.XmlDeclaration)?.Remove();
+
+            string projectFullPath = root.FullPath;
+
+            root = ProjectRootElement.Create(xDoc.CreateReader());
+            root.FullPath = projectFullPath;
+
+            project.Root = root;
         }
         }
 
 
-        public static void EnsureHasNugetNetFrameworkRefAssemblies(MSBuildProject project)
+        public static void EnsureGodotSdkIsUpToDate(MSBuildProject project)
         {
         {
             var root = project.Root;
             var root = project.Root;
+            string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue;
 
 
-            bool found = root.ItemGroups.Any(g => string.IsNullOrEmpty(g.Condition) && g.Items.Any(
-                item => item.ItemType == "PackageReference" && item.Include == "Microsoft.NETFramework.ReferenceAssemblies"));
-
-            if (found)
+            if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase))
                 return;
                 return;
 
 
-            var frameworkRefAssembliesItem = root.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies");
-
-            // Use metadata (child nodes) instead of attributes for the PackageReference.
-            // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build.
-            frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0");
-            frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All");
-
+            root.Sdk = godotSdkAttrValue;
             project.HasUnsavedChanges = true;
             project.HasUnsavedChanges = true;
         }
         }
     }
     }

+ 33 - 37
modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs

@@ -24,48 +24,50 @@ namespace GodotTools
         private ToolButton errorsBtn;
         private ToolButton errorsBtn;
         private Button viewLogBtn;
         private Button viewLogBtn;
 
 
-        private void _UpdateBuildTabsList()
+        private void _UpdateBuildTab(int index, int? currentTab)
         {
         {
-            buildTabsList.Clear();
+            var tab = (BuildTab)buildTabs.GetChild(index);
 
 
-            int currentTab = buildTabs.CurrentTab;
+            string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution);
+            itemName += " [" + tab.BuildInfo.Configuration + "]";
 
 
-            bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount();
+            buildTabsList.AddItem(itemName, tab.IconTexture);
 
 
-            for (int i = 0; i < buildTabs.GetChildCount(); i++)
-            {
-                var tab = (BuildTab)buildTabs.GetChild(i);
+            string itemTooltip = "Solution: " + tab.BuildInfo.Solution;
+            itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration;
+            itemTooltip += "\nStatus: ";
 
 
-                if (tab == null)
-                    continue;
+            if (tab.BuildExited)
+                itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored";
+            else
+                itemTooltip += "Running";
 
 
-                string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution);
-                itemName += " [" + tab.BuildInfo.Configuration + "]";
+            if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error)
+                itemTooltip += $"\nErrors: {tab.ErrorCount}";
 
 
-                buildTabsList.AddItem(itemName, tab.IconTexture);
+            itemTooltip += $"\nWarnings: {tab.WarningCount}";
 
 
-                string itemTooltip = "Solution: " + tab.BuildInfo.Solution;
-                itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration;
-                itemTooltip += "\nStatus: ";
+            buildTabsList.SetItemTooltip(index, itemTooltip);
 
 
-                if (tab.BuildExited)
-                    itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored";
-                else
-                    itemTooltip += "Running";
+            // If this tab was already selected before the changes or if no tab was selected
+            if (currentTab == null || currentTab == index)
+            {
+                buildTabsList.Select(index);
+                _BuildTabsItemSelected(index);
+            }
+        }
 
 
-                if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error)
-                    itemTooltip += $"\nErrors: {tab.ErrorCount}";
+        private void _UpdateBuildTabsList()
+        {
+            buildTabsList.Clear();
 
 
-                itemTooltip += $"\nWarnings: {tab.WarningCount}";
+            int? currentTab = buildTabs.CurrentTab;
 
 
-                buildTabsList.SetItemTooltip(i, itemTooltip);
+            if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
+                currentTab = null;
 
 
-                if (noCurrentTab || currentTab == i)
-                {
-                    buildTabsList.Select(i);
-                    _BuildTabsItemSelected(i);
-                }
-            }
+            for (int i = 0; i < buildTabs.GetChildCount(); i++)
+                _UpdateBuildTab(i, currentTab);
         }
         }
 
 
         public BuildTab GetBuildTabFor(BuildInfo buildInfo)
         public BuildTab GetBuildTabFor(BuildInfo buildInfo)
@@ -160,13 +162,7 @@ namespace GodotTools
                 }
                 }
             }
             }
 
 
-            var godotDefines = new[]
-            {
-                OS.GetName(),
-                Internal.GodotIs32Bits() ? "32" : "64"
-            };
-
-            bool buildSuccess = BuildManager.BuildProjectBlocking("Debug", godotDefines);
+            bool buildSuccess = BuildManager.BuildProjectBlocking("Debug");
 
 
             if (!buildSuccess)
             if (!buildSuccess)
                 return;
                 return;
@@ -272,7 +268,7 @@ namespace GodotTools
                 };
                 };
                 panelTabs.AddChild(panelBuildsTab);
                 panelTabs.AddChild(panelBuildsTab);
 
 
-                var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill };
+                var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
                 panelBuildsTab.AddChild(toolBarHBox);
                 panelBuildsTab.AddChild(toolBarHBox);
 
 
                 var buildProjectBtn = new Button
                 var buildProjectBtn = new Button

+ 7 - 10
modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs

@@ -36,15 +36,13 @@ namespace GodotTools.Build
                     }
                     }
                     case BuildTool.MsBuildVs:
                     case BuildTool.MsBuildVs:
                     {
                     {
-                        if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath))
+                        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
                             // Try to search it again if it wasn't found last time or if it was removed from its location
                             _msbuildToolsPath = FindMsBuildToolsPathOnWindows();
                             _msbuildToolsPath = FindMsBuildToolsPathOnWindows();
 
 
-                            if (_msbuildToolsPath.Empty())
-                            {
+                            if (string.IsNullOrEmpty(_msbuildToolsPath))
                                 throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'.");
                                 throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'.");
-                            }
                         }
                         }
 
 
                         if (!_msbuildToolsPath.EndsWith("\\"))
                         if (!_msbuildToolsPath.EndsWith("\\"))
@@ -57,15 +55,14 @@ namespace GodotTools.Build
                         string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
                         string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
 
 
                         if (!File.Exists(msbuildPath))
                         if (!File.Exists(msbuildPath))
-                        {
                             throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}");
                             throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}");
-                        }
 
 
                         return (msbuildPath, BuildTool.MsBuildMono);
                         return (msbuildPath, BuildTool.MsBuildMono);
                     }
                     }
                     case BuildTool.JetBrainsMsBuild:
                     case BuildTool.JetBrainsMsBuild:
                     {
                     {
                         var editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName);
                         var editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName);
+
                         if (!File.Exists(editorPath))
                         if (!File.Exists(editorPath))
                             throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}");
                             throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}");
 
 
@@ -83,7 +80,7 @@ namespace GodotTools.Build
                 }
                 }
             }
             }
 
 
-            if (OS.IsUnixLike())
+            if (OS.IsUnixLike)
             {
             {
                 switch (buildTool)
                 switch (buildTool)
                 {
                 {
@@ -138,12 +135,12 @@ namespace GodotTools.Build
         {
         {
             string ret = OS.PathWhich(name);
             string ret = OS.PathWhich(name);
 
 
-            if (!ret.Empty())
+            if (!string.IsNullOrEmpty(ret))
                 return ret;
                 return ret;
 
 
             string retFallback = OS.PathWhich($"{name}.exe");
             string retFallback = OS.PathWhich($"{name}.exe");
 
 
-            if (!retFallback.Empty())
+            if (!string.IsNullOrEmpty(retFallback))
                 return retFallback;
                 return retFallback;
 
 
             foreach (string hintDir in MsBuildHintDirs)
             foreach (string hintDir in MsBuildHintDirs)
@@ -195,7 +192,7 @@ namespace GodotTools.Build
 
 
                 string value = line.Substring(sepIdx + 1).StripEdges();
                 string value = line.Substring(sepIdx + 1).StripEdges();
 
 
-                if (value.Empty())
+                if (string.IsNullOrEmpty(value))
                     throw new FormatException("installationPath value is empty");
                     throw new FormatException("installationPath value is empty");
 
 
                 if (!value.EndsWith("\\"))
                 if (!value.EndsWith("\\"))

+ 7 - 23
modules/mono/editor/GodotTools/GodotTools/BuildManager.cs

@@ -6,6 +6,7 @@ using GodotTools.Build;
 using GodotTools.Ides.Rider;
 using GodotTools.Ides.Rider;
 using GodotTools.Internals;
 using GodotTools.Internals;
 using GodotTools.Utils;
 using GodotTools.Utils;
+using JetBrains.Annotations;
 using static GodotTools.Internals.Globals;
 using static GodotTools.Internals.Globals;
 using File = GodotTools.Utils.File;
 using File = GodotTools.Utils.File;
 
 
@@ -152,7 +153,7 @@ namespace GodotTools
             }
             }
         }
         }
 
 
-        public static bool BuildProjectBlocking(string config, IEnumerable<string> godotDefines)
+        public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null)
         {
         {
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
                 return true; // No solution to build
                 return true; // No solution to build
@@ -168,29 +169,18 @@ namespace GodotTools
                 return false;
                 return false;
             }
             }
 
 
-            var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
-            var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool");
-
             using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1))
             using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1))
             {
             {
                 pr.Step("Building project solution", 0);
                 pr.Step("Building project solution", 0);
 
 
                 var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, new[] {"Build"}, config, restore: true);
                 var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, new[] {"Build"}, config, restore: true);
 
 
-                bool escapeNeedsDoubleBackslash = buildTool == BuildTool.MsBuildMono || buildTool == BuildTool.DotnetCli;
-
-                // Add Godot defines
-                string constants = !escapeNeedsDoubleBackslash ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\"";
-
-                foreach (var godotDefine in godotDefines)
-                    constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};";
+                // 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}");
 
 
                 if (Internal.GodotIsRealTDouble())
                 if (Internal.GodotIsRealTDouble())
-                    constants += "GODOT_REAL_T_IS_DOUBLE;";
-
-                constants += !escapeNeedsDoubleBackslash ? "\"" : "\\\"";
-
-                buildInfo.CustomProperties.Add(constants);
+                    buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
 
 
                 if (!Build(buildInfo))
                 if (!Build(buildInfo))
                 {
                 {
@@ -233,13 +223,7 @@ namespace GodotTools
                     return true; // Requested play from an external editor/IDE which already built the project
                     return true; // Requested play from an external editor/IDE which already built the project
             }
             }
 
 
-            var godotDefines = new[]
-            {
-                Godot.OS.GetName(),
-                Internal.GodotIs32Bits() ? "32" : "64"
-            };
-
-            return BuildProjectBlocking("Debug", godotDefines);
+            return BuildProjectBlocking("Debug");
         }
         }
 
 
         public static void Initialize()
         public static void Initialize()

+ 6 - 6
modules/mono/editor/GodotTools/GodotTools/BuildTab.cs

@@ -72,7 +72,7 @@ namespace GodotTools
                     {
                     {
                         string[] csvColumns = file.GetCsvLine();
                         string[] csvColumns = file.GetCsvLine();
 
 
-                        if (csvColumns.Length == 1 && csvColumns[0].Empty())
+                        if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
                             return;
                             return;
 
 
                         if (csvColumns.Length != 7)
                         if (csvColumns.Length != 7)
@@ -115,12 +115,12 @@ namespace GodotTools
             // Get correct issue idx from issue list
             // Get correct issue idx from issue list
             int issueIndex = (int)issuesList.GetItemMetadata(idx);
             int issueIndex = (int)issuesList.GetItemMetadata(idx);
 
 
-            if (idx < 0 || idx >= issues.Count)
+            if (issueIndex < 0 || issueIndex >= issues.Count)
                 throw new IndexOutOfRangeException("Issue index out of range");
                 throw new IndexOutOfRangeException("Issue index out of range");
 
 
             BuildIssue issue = issues[issueIndex];
             BuildIssue issue = issues[issueIndex];
 
 
-            if (issue.ProjectFile.Empty() && issue.File.Empty())
+            if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File))
                 return;
                 return;
 
 
             string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir();
             string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir();
@@ -158,14 +158,14 @@ namespace GodotTools
                     string tooltip = string.Empty;
                     string tooltip = string.Empty;
                     tooltip += $"Message: {issue.Message}";
                     tooltip += $"Message: {issue.Message}";
 
 
-                    if (!issue.Code.Empty())
+                    if (!string.IsNullOrEmpty(issue.Code))
                         tooltip += $"\nCode: {issue.Code}";
                         tooltip += $"\nCode: {issue.Code}";
 
 
                     tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
                     tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
 
 
                     string text = string.Empty;
                     string text = string.Empty;
 
 
-                    if (!issue.File.Empty())
+                    if (!string.IsNullOrEmpty(issue.File))
                     {
                     {
                         text += $"{issue.File}({issue.Line},{issue.Column}): ";
                         text += $"{issue.File}({issue.Line},{issue.Column}): ";
 
 
@@ -174,7 +174,7 @@ namespace GodotTools
                         tooltip += $"\nColumn: {issue.Column}";
                         tooltip += $"\nColumn: {issue.Column}";
                     }
                     }
 
 
-                    if (!issue.ProjectFile.Empty())
+                    if (!string.IsNullOrEmpty(issue.ProjectFile))
                         tooltip += $"\nProject: {issue.ProjectFile}";
                         tooltip += $"\nProject: {issue.ProjectFile}";
 
 
                     text += issue.Message;
                     text += issue.Message;

+ 52 - 64
modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs

@@ -1,9 +1,9 @@
 using Godot;
 using Godot;
 using System;
 using System;
+using System.Linq;
 using Godot.Collections;
 using Godot.Collections;
 using GodotTools.Internals;
 using GodotTools.Internals;
 using GodotTools.ProjectEditor;
 using GodotTools.ProjectEditor;
-using static GodotTools.Internals.Globals;
 using File = GodotTools.Utils.File;
 using File = GodotTools.Utils.File;
 using Directory = GodotTools.Utils.Directory;
 using Directory = GodotTools.Utils.Directory;
 
 
@@ -15,7 +15,7 @@ namespace GodotTools
         {
         {
             try
             try
             {
             {
-                return ProjectGenerator.GenGameProject(dir, name, compileItems: new string[] { });
+                return ProjectGenerator.GenAndSaveGameProject(dir, name);
             }
             }
             catch (Exception e)
             catch (Exception e)
             {
             {
@@ -24,14 +24,6 @@ namespace GodotTools
             }
             }
         }
         }
 
 
-        public static void AddItem(string projectPath, string itemType, string include)
-        {
-            if (!(bool)GlobalDef("mono/project/auto_update_project", true))
-                return;
-
-            ProjectUtils.AddItemToProjectChecked(projectPath, itemType, include);
-        }
-
         private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
         private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
 
         private static ulong ConvertToTimestamp(this DateTime value)
         private static ulong ConvertToTimestamp(this DateTime value)
@@ -40,81 +32,77 @@ namespace GodotTools
             return (ulong)elapsedTime.TotalSeconds;
             return (ulong)elapsedTime.TotalSeconds;
         }
         }
 
 
-        public static void GenerateScriptsMetadata(string projectPath, string outputPath)
+        private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata)
         {
         {
-            if (File.Exists(outputPath))
-                File.Delete(outputPath);
+            fileMetadata = null;
 
 
-            var oldDict = Internal.GetScriptsMetadataOrNothing();
-            var newDict = new Godot.Collections.Dictionary<string, object>();
+            var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr);
 
 
-            foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile"))
+            if (parseError != Error.Ok)
             {
             {
-                string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath();
+                GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}");
+                return false;
+            }
 
 
-                ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp();
+            string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile);
 
 
-                if (oldDict.TryGetValue(projectIncludeFile, out var oldFileVar))
-                {
-                    var oldFileDict = (Dictionary)oldFileVar;
-
-                    if (ulong.TryParse(oldFileDict["modified_time"] as string, out ulong storedModifiedTime))
-                    {
-                        if (storedModifiedTime == modifiedTime)
-                        {
-                            // No changes so no need to parse again
-                            newDict[projectIncludeFile] = oldFileDict;
-                            continue;
-                        }
-                    }
-                }
+            var firstMatch = classes.FirstOrDefault(classDecl =>
+                    classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object.
+                    classDecl.SearchName != searchName // Filter by the name we're looking for
+            );
+
+            if (firstMatch == null)
+                return false; // Not found
 
 
-                Error parseError = ScriptClassParser.ParseFile(projectIncludeFile, out var classes, out string errorStr);
-                if (parseError != Error.Ok)
+            fileMetadata = new Dictionary
+            {
+                ["modified_time"] = $"{modifiedTime}",
+                ["class"] = new Dictionary
                 {
                 {
-                    GD.PushError($"Failed to determine namespace and class for script: {projectIncludeFile}. Parse error: {errorStr ?? parseError.ToString()}");
-                    continue;
+                    ["namespace"] = firstMatch.Namespace,
+                    ["class_name"] = firstMatch.Name,
+                    ["nested"] = firstMatch.Nested
                 }
                 }
+            };
 
 
-                string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile);
-
-                var classDict = new Dictionary();
+            return true;
+        }
 
 
-                foreach (var classDecl in classes)
-                {
-                    if (classDecl.BaseCount == 0)
-                        continue; // Does not inherit nor implement anything, so it can't be a script class
+        public static void GenerateScriptsMetadata(string projectPath, string outputPath)
+        {
+            var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate();
 
 
-                    string classCmp = classDecl.Nested ?
-                        classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) :
-                        classDecl.Name;
+            bool IsUpToDate(string includeFile, ulong modifiedTime)
+            {
+                return metadataDict.TryGetValue(includeFile, out var oldFileVar) &&
+                       ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string,
+                           out ulong storedModifiedTime) && storedModifiedTime == modifiedTime;
+            }
 
 
-                    if (classCmp != searchName)
-                        continue;
+            var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile")
+                .Select(path => ("res://" + path).SimplifyGodotPath())
+                .ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp())
+                .Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value))
+                .ToArray();
 
 
-                    classDict["namespace"] = classDecl.Namespace;
-                    classDict["class_name"] = classDecl.Name;
-                    classDict["nested"] = classDecl.Nested;
-                    break;
-                }
+            foreach (var pair in outdatedFiles)
+            {
+                metadataDict.Remove(pair.Key);
 
 
-                if (classDict.Count == 0)
-                    continue; // Not found
+                string includeFile = pair.Key;
 
 
-                newDict[projectIncludeFile] = new Dictionary { ["modified_time"] = $"{modifiedTime}", ["class"] = classDict };
+                if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata))
+                    metadataDict[includeFile] = fileMetadata;
             }
             }
 
 
-            if (newDict.Count > 0)
-            {
-                string json = JSON.Print(newDict);
+            string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict);
 
 
-                string baseDir = outputPath.GetBaseDir();
+            string baseDir = outputPath.GetBaseDir();
 
 
-                if (!Directory.Exists(baseDir))
-                    Directory.CreateDirectory(baseDir);
+            if (!Directory.Exists(baseDir))
+                Directory.CreateDirectory(baseDir);
 
 
-                File.WriteAllText(outputPath, json);
-            }
+            File.WriteAllText(outputPath, json);
         }
         }
     }
     }
 }
 }

+ 11 - 13
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 using GodotTools.Core;
 using GodotTools.Core;
 using GodotTools.Internals;
 using GodotTools.Internals;
+using JetBrains.Annotations;
 using static GodotTools.Internals.Globals;
 using static GodotTools.Internals.Globals;
 using Directory = GodotTools.Utils.Directory;
 using Directory = GodotTools.Utils.Directory;
 using File = GodotTools.Utils.File;
 using File = GodotTools.Utils.File;
@@ -145,9 +146,7 @@ namespace GodotTools.Export
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
                 return;
                 return;
 
 
-            string platform = DeterminePlatformFromFeatures(features);
-
-            if (platform == null)
+            if (!DeterminePlatformFromFeatures(features, out string platform))
                 throw new NotSupportedException("Target platform not supported");
                 throw new NotSupportedException("Target platform not supported");
 
 
             string outputDir = new FileInfo(path).Directory?.FullName ??
             string outputDir = new FileInfo(path).Directory?.FullName ??
@@ -160,10 +159,7 @@ namespace GodotTools.Export
 
 
             AddFile(scriptsMetadataPath, scriptsMetadataPath);
             AddFile(scriptsMetadataPath, scriptsMetadataPath);
 
 
-            // Turn export features into defines
-            var godotDefines = features;
-
-            if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines))
+            if (!BuildManager.BuildProjectBlocking(buildConfig, platform))
                 throw new Exception("Failed to build project");
                 throw new Exception("Failed to build project");
 
 
             // Add dependency assemblies
             // Add dependency assemblies
@@ -289,6 +285,7 @@ namespace GodotTools.Export
             }
             }
         }
         }
 
 
+        [NotNull]
         private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir)
         private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir)
         {
         {
             string target = isDebug ? "release_debug" : "release";
             string target = isDebug ? "release_debug" : "release";
@@ -343,18 +340,19 @@ namespace GodotTools.Export
         private static bool PlatformHasTemplateDir(string platform)
         private static bool PlatformHasTemplateDir(string platform)
         {
         {
             // OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest.
             // OSX 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.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform);
+            return !new[] {OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform);
         }
         }
 
 
-        private static string DeterminePlatformFromFeatures(IEnumerable<string> features)
+        private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform)
         {
         {
             foreach (var feature in features)
             foreach (var feature in features)
             {
             {
-                if (OS.PlatformNameMap.TryGetValue(feature, out string platform))
-                    return platform;
+                if (OS.PlatformNameMap.TryGetValue(feature, out platform))
+                    return true;
             }
             }
 
 
-            return null;
+            platform = null;
+            return false;
         }
         }
 
 
         private static string GetBclProfileDir(string profile)
         private static string GetBclProfileDir(string profile)
@@ -391,7 +389,7 @@ namespace GodotTools.Export
         /// </summary>
         /// </summary>
         private static bool PlatformRequiresCustomBcl(string platform)
         private static bool PlatformRequiresCustomBcl(string platform)
         {
         {
-            if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform))
+            if (new[] {OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform))
                 return true;
                 return true;
 
 
             // The 'net_4_x' BCL is not compatible between Windows and the other platforms.
             // The 'net_4_x' BCL is not compatible between Windows and the other platforms.

+ 36 - 40
modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

@@ -302,7 +302,7 @@ namespace GodotTools
 
 
                 case ExternalEditorId.VsCode:
                 case ExternalEditorId.VsCode:
                 {
                 {
-                    if (_vsCodePath.Empty() || !File.Exists(_vsCodePath))
+                    if (string.IsNullOrEmpty(_vsCodePath) || !File.Exists(_vsCodePath))
                     {
                     {
                         // Try to search it again if it wasn't found last time or if it was removed from its location
                         // Try to search it again if it wasn't found last time or if it was removed from its location
                         _vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
                         _vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
@@ -354,7 +354,7 @@ namespace GodotTools
 
 
                     if (OS.IsOSX)
                     if (OS.IsOSX)
                     {
                     {
-                        if (!osxAppBundleInstalled && _vsCodePath.Empty())
+                        if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath))
                         {
                         {
                             GD.PushError("Cannot find code editor: VSCode");
                             GD.PushError("Cannot find code editor: VSCode");
                             return Error.FileNotFound;
                             return Error.FileNotFound;
@@ -364,7 +364,7 @@ namespace GodotTools
                     }
                     }
                     else
                     else
                     {
                     {
-                        if (_vsCodePath.Empty())
+                        if (string.IsNullOrEmpty(_vsCodePath))
                         {
                         {
                             GD.PushError("Cannot find code editor: VSCode");
                             GD.PushError("Cannot find code editor: VSCode");
                             return Error.FileNotFound;
                             return Error.FileNotFound;
@@ -403,6 +403,37 @@ namespace GodotTools
             return BuildManager.EditorBuildCallback();
             return BuildManager.EditorBuildCallback();
         }
         }
 
 
+        private void ApplyNecessaryChangesToSolution()
+        {
+            try
+            {
+                // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease
+                DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath);
+
+                var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath)
+                                     ?? throw new Exception("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.EnsureGodotSdkIsUpToDate(msbuildProject);
+
+                if (msbuildProject.HasUnsavedChanges)
+                {
+                    // Save a copy of the project before replacing it
+                    FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath);
+
+                    msbuildProject.Save();
+                }
+            }
+            catch (Exception e)
+            {
+                GD.PushError(e.ToString());
+            }
+        }
+
         public override void EnablePlugin()
         public override void EnablePlugin()
         {
         {
             base.EnablePlugin();
             base.EnablePlugin();
@@ -478,42 +509,7 @@ namespace GodotTools
 
 
             if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
             if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
             {
             {
-                try
-                {
-                    // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease
-                    DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath);
-
-                    var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath)
-                                         ?? throw new Exception("Cannot open C# project");
-
-                    // NOTE: The order in which changes are made to the project is important
-
-                    // Migrate csproj from old configuration names to: Debug, ExportDebug and ExportRelease
-                    ProjectUtils.MigrateFromOldConfigNames(msbuildProject);
-
-                    // Apply the other fixes only after configurations have been migrated
-
-                    // Make sure the existing project has the ProjectTypeGuids property (for VisualStudio)
-                    ProjectUtils.EnsureHasProjectTypeGuids(msbuildProject);
-
-                    // Make sure the existing project has Api assembly references configured correctly
-                    ProjectUtils.FixApiHintPath(msbuildProject);
-
-                    // Make sure the existing project references the Microsoft.NETFramework.ReferenceAssemblies nuget package
-                    ProjectUtils.EnsureHasNugetNetFrameworkRefAssemblies(msbuildProject);
-
-                    if (msbuildProject.HasUnsavedChanges)
-                    {
-                        // Save a copy of the project before replacing it
-                        FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath);
-
-                        msbuildProject.Save();
-                    }
-                }
-                catch (Exception e)
-                {
-                    GD.PushError(e.ToString());
-                }
+                ApplyNecessaryChangesToSolution();
             }
             }
             else
             else
             {
             {
@@ -551,7 +547,7 @@ namespace GodotTools
                                    $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
                                    $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
                                    $",JetBrains Rider:{(int)ExternalEditorId.Rider}";
                                    $",JetBrains Rider:{(int)ExternalEditorId.Rider}";
             }
             }
-            else if (OS.IsUnixLike())
+            else if (OS.IsUnixLike)
             {
             {
                 settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
                 settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
                                    $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
                                    $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +

+ 1 - 5
modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj

@@ -1,12 +1,8 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
     <ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid>
     <ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid>
-    <OutputType>Library</OutputType>
     <TargetFramework>net472</TargetFramework>
     <TargetFramework>net472</TargetFramework>
-    <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
-    <DataDirToolsOutputPath>$(GodotSourceRootPath)/bin/GodotSharp/Tools</DataDirToolsOutputPath>
-    <GodotApiConfiguration>Debug</GodotApiConfiguration>
-    <LangVersion>7</LangVersion>
+    <LangVersion>7.2</LangVersion>
     <GodotApiConfiguration>Debug</GodotApiConfiguration> <!-- The Godot editor uses the Debug Godot API assemblies -->
     <GodotApiConfiguration>Debug</GodotApiConfiguration> <!-- The Godot editor uses the Debug Godot API assemblies -->
     <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
     <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
     <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
     <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>

+ 1 - 1
modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs

@@ -128,7 +128,7 @@ namespace GodotTools.Ides.MonoDevelop
                     {EditorId.MonoDevelop, "MonoDevelop.exe"}
                     {EditorId.MonoDevelop, "MonoDevelop.exe"}
                 };
                 };
             }
             }
-            else if (OS.IsUnixLike())
+            else if (OS.IsUnixLike)
             {
             {
                 ExecutableNames = new Dictionary<EditorId, string>
                 ExecutableNames = new Dictionary<EditorId, string>
                 {
                 {

+ 3 - 3
modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs

@@ -36,7 +36,7 @@ namespace GodotTools.Ides.Rider
                 {
                 {
                     return CollectRiderInfosMac();
                     return CollectRiderInfosMac();
                 }
                 }
-                if (OS.IsUnixLike())
+                if (OS.IsUnixLike)
                 {
                 {
                     return CollectAllRiderPathsLinux();
                     return CollectAllRiderPathsLinux();
                 }
                 }
@@ -147,7 +147,7 @@ namespace GodotTools.Ides.Rider
                 return GetToolboxRiderRootPath(localAppData);
                 return GetToolboxRiderRootPath(localAppData);
             }
             }
 
 
-            if (OS.IsUnixLike())
+            if (OS.IsUnixLike)
             {
             {
                 var home = Environment.GetEnvironmentVariable("HOME");
                 var home = Environment.GetEnvironmentVariable("HOME");
                 if (string.IsNullOrEmpty(home))
                 if (string.IsNullOrEmpty(home))
@@ -209,7 +209,7 @@ namespace GodotTools.Ides.Rider
 
 
         private static string GetRelativePathToBuildTxt()
         private static string GetRelativePathToBuildTxt()
         {
         {
-            if (OS.IsWindows || OS.IsUnixLike())
+            if (OS.IsWindows || OS.IsUnixLike)
                 return "../../build.txt";
                 return "../../build.txt";
             if (OS.IsOSX)
             if (OS.IsOSX)
                 return "Contents/Resources/build.txt";
                 return "Contents/Resources/build.txt";

+ 4 - 0
modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs

@@ -15,6 +15,10 @@ namespace GodotTools.Internals
             public bool Nested { get; }
             public bool Nested { get; }
             public int BaseCount { get; }
             public int BaseCount { get; }
 
 
+            public string SearchName => Nested ?
+                Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) :
+                Name;
+
             public ClassDecl(string name, string @namespace, bool nested, int baseCount)
             public ClassDecl(string name, string @namespace, bool nested, int baseCount)
             {
             {
                 Name = name;
                 Name = name;

+ 12 - 15
modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs

@@ -62,6 +62,11 @@ namespace GodotTools.Utils
             return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
             return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
+        private static bool IsAnyOS(IEnumerable<string> names)
+        {
+            return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase));
+        }
+
         private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows));
         private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows));
         private static readonly Lazy<bool> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX));
         private static readonly Lazy<bool> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX));
         private static readonly Lazy<bool> _isX11 = new Lazy<bool>(() => IsOS(Names.X11));
         private static readonly Lazy<bool> _isX11 = new Lazy<bool>(() => IsOS(Names.X11));
@@ -71,6 +76,7 @@ namespace GodotTools.Utils
         private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android));
         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> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS));
         private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5));
         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 IsWindows => _isWindows.Value || IsUWP;
         public static bool IsOSX => _isOSX.Value;
         public static bool IsOSX => _isOSX.Value;
@@ -82,18 +88,9 @@ namespace GodotTools.Utils
         public static bool IsiOS => _isiOS.Value;
         public static bool IsiOS => _isiOS.Value;
         public static bool IsHTML5 => _isHTML5.Value;
         public static bool IsHTML5 => _isHTML5.Value;
 
 
-        private static bool? _isUnixCache;
-        private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS };
+        private static readonly string[] UnixLikePlatforms = {Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS};
 
 
-        public static bool IsUnixLike()
-        {
-            if (_isUnixCache.HasValue)
-                return _isUnixCache.Value;
-
-            string osName = GetPlatformName();
-            _isUnixCache = UnixLikePlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase));
-            return _isUnixCache.Value;
-        }
+        public static bool IsUnixLike => _isUnixLike.Value;
 
 
         public static char PathSep => IsWindows ? ';' : ':';
         public static char PathSep => IsWindows ? ';' : ':';
 
 
@@ -121,10 +118,10 @@ namespace GodotTools.Utils
                 return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists);
                 return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists);
 
 
             return (from dir in searchDirs
             return (from dir in searchDirs
-                    select Path.Combine(dir, name)
+                select Path.Combine(dir, name)
                 into path
                 into path
-                    from ext in windowsExts
-                    select path + ext).FirstOrDefault(File.Exists);
+                from ext in windowsExts
+                select path + ext).FirstOrDefault(File.Exists);
         }
         }
 
 
         private static string PathWhichUnix([NotNull] string name)
         private static string PathWhichUnix([NotNull] string name)
@@ -189,7 +186,7 @@ namespace GodotTools.Utils
 
 
             startInfo.UseShellExecute = false;
             startInfo.UseShellExecute = false;
 
 
-            using (var process = new Process { StartInfo = startInfo })
+            using (var process = new Process {StartInfo = startInfo})
             {
             {
                 process.Start();
                 process.Start();
                 process.WaitForExit();
                 process.WaitForExit();

+ 0 - 1
modules/mono/editor/bindings_generator.cpp

@@ -45,7 +45,6 @@
 #include "../mono_gd/gd_mono_marshal.h"
 #include "../mono_gd/gd_mono_marshal.h"
 #include "../utils/path_utils.h"
 #include "../utils/path_utils.h"
 #include "../utils/string_utils.h"
 #include "../utils/string_utils.h"
-#include "csharp_project.h"
 
 
 #define CS_INDENT "    " // 4 whitespaces
 #define CS_INDENT "    " // 4 whitespaces
 
 

+ 42 - 35
modules/mono/editor/godotsharp_export.cpp

@@ -32,68 +32,70 @@
 
 
 #include <mono/metadata/image.h>
 #include <mono/metadata/image.h>
 
 
+#include "core/io/file_access_pack.h"
 #include "core/os/os.h"
 #include "core/os/os.h"
+#include "core/project_settings.h"
 
 
 #include "../mono_gd/gd_mono.h"
 #include "../mono_gd/gd_mono.h"
 #include "../mono_gd/gd_mono_assembly.h"
 #include "../mono_gd/gd_mono_assembly.h"
 #include "../mono_gd/gd_mono_cache.h"
 #include "../mono_gd/gd_mono_cache.h"
+#include "../utils/macros.h"
 
 
 namespace GodotSharpExport {
 namespace GodotSharpExport {
 
 
-String get_assemblyref_name(MonoImage *p_image, int index) {
+struct AssemblyRefInfo {
+	String name;
+	uint16_t major;
+	uint16_t minor;
+	uint16_t build;
+	uint16_t revision;
+};
+
+AssemblyRefInfo get_assemblyref_name(MonoImage *p_image, int index) {
 	const MonoTableInfo *table_info = mono_image_get_table_info(p_image, MONO_TABLE_ASSEMBLYREF);
 	const MonoTableInfo *table_info = mono_image_get_table_info(p_image, MONO_TABLE_ASSEMBLYREF);
 
 
 	uint32_t cols[MONO_ASSEMBLYREF_SIZE];
 	uint32_t cols[MONO_ASSEMBLYREF_SIZE];
 
 
 	mono_metadata_decode_row(table_info, index, 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]));
+	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, const Vector<String> &p_search_dirs, Dictionary &r_assembly_dependencies) {
 Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_assembly_dependencies) {
 	MonoImage *image = p_assembly->get_image();
 	MonoImage *image = p_assembly->get_image();
 
 
 	for (int i = 0; i < mono_image_get_table_rows(image, MONO_TABLE_ASSEMBLYREF); i++) {
 	for (int i = 0; i < mono_image_get_table_rows(image, MONO_TABLE_ASSEMBLYREF); i++) {
-		String ref_name = get_assemblyref_name(image, i);
+		AssemblyRefInfo ref_info = get_assemblyref_name(image, i);
+
+		const String &ref_name = ref_info.name;
 
 
 		if (r_assembly_dependencies.has(ref_name))
 		if (r_assembly_dependencies.has(ref_name))
 			continue;
 			continue;
 
 
 		GDMonoAssembly *ref_assembly = NULL;
 		GDMonoAssembly *ref_assembly = NULL;
-		String path;
-		bool has_extension = ref_name.ends_with(".dll") || ref_name.ends_with(".exe");
-
-		for (int j = 0; j < p_search_dirs.size(); j++) {
-			const String &search_dir = p_search_dirs[j];
-
-			if (has_extension) {
-				path = search_dir.plus_file(ref_name);
-				if (FileAccess::exists(path)) {
-					GDMono::get_singleton()->load_assembly_from(ref_name.get_basename(), path, &ref_assembly, true);
-					if (ref_assembly != NULL)
-						break;
-				}
-			} else {
-				path = search_dir.plus_file(ref_name + ".dll");
-				if (FileAccess::exists(path)) {
-					GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true);
-					if (ref_assembly != NULL)
-						break;
-				}
-
-				path = search_dir.plus_file(ref_name + ".exe");
-				if (FileAccess::exists(path)) {
-					GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true);
-					if (ref_assembly != NULL)
-						break;
-				}
-			}
-		}
 
 
-		ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
+		{
+			MonoAssemblyName *ref_aname = mono_assembly_name_new("A"); // We can't allocate an empty MonoAssemblyName, hence "A"
+			CRASH_COND(ref_aname == nullptr);
+			SCOPE_EXIT {
+				mono_assembly_name_free(ref_aname);
+				mono_free(ref_aname);
+			};
+
+			mono_assembly_get_assemblyref(image, i, ref_aname);
 
 
-		// Use the path we got from the search. Don't try to get the path from the loaded assembly as we can't trust it will be from the selected BCL dir.
-		r_assembly_dependencies[ref_name] = path;
+			if (!GDMono::get_singleton()->load_assembly(ref_name, ref_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, p_search_dirs, r_assembly_dependencies);
 		Error err = get_assembly_dependencies(ref_assembly, 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 + "'.");
 		ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'.");
@@ -113,6 +115,11 @@ Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies,
 	Vector<String> search_dirs;
 	Vector<String> search_dirs;
 	GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_bcl_dir);
 	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)) {
 	for (const Variant *key = p_initial_assemblies.next(); key; key = p_initial_assemblies.next(key)) {
 		String assembly_name = *key;
 		String assembly_name = *key;
 		String assembly_path = p_initial_assemblies[*key];
 		String assembly_path = p_initial_assemblies[*key];

+ 6 - 29
modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj

@@ -1,38 +1,16 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
-    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
-    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
     <ProjectGuid>{AEBF0036-DA76-4341-B651-A3F2856AB2FA}</ProjectGuid>
     <ProjectGuid>{AEBF0036-DA76-4341-B651-A3F2856AB2FA}</ProjectGuid>
-    <OutputType>Library</OutputType>
     <OutputPath>bin/$(Configuration)</OutputPath>
     <OutputPath>bin/$(Configuration)</OutputPath>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <RootNamespace>Godot</RootNamespace>
     <RootNamespace>Godot</RootNamespace>
-    <AssemblyName>GodotSharp</AssemblyName>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <TargetFramework>netstandard2.0</TargetFramework>
     <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile>
     <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile>
-    <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath>
+    <EnableDefaultItems>false</EnableDefaultItems>
   </PropertyGroup>
   </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <DebugSymbols>true</DebugSymbols>
-    <DebugType>portable</DebugType>
-    <Optimize>false</Optimize>
-    <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <DebugType>portable</DebugType>
-    <Optimize>true</Optimize>
-    <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
+  <PropertyGroup>
+    <DefineConstants>$(DefineConstants);GODOT</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
-    <Reference Include="System" />
-  </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <Compile Include="Core\AABB.cs" />
     <Compile Include="Core\AABB.cs" />
     <Compile Include="Core\Array.cs" />
     <Compile Include="Core\Array.cs" />
@@ -82,5 +60,4 @@
   Fortunately code completion, go to definition and such still work.
   Fortunately code completion, go to definition and such still work.
   -->
   -->
   <Import Project="Generated\GeneratedIncludes.props" />
   <Import Project="Generated\GeneratedIncludes.props" />
-  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
 </Project>
 </Project>

+ 0 - 24
modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs

@@ -1,27 +1,3 @@
-using System.Reflection;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 
 
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("GodotSharp")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("1.0.*")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
 [assembly: InternalsVisibleTo("GodotSharpEditor")]
 [assembly: InternalsVisibleTo("GodotSharpEditor")]

+ 14 - 34
modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj

@@ -1,46 +1,26 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
-    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
-    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
     <ProjectGuid>{8FBEC238-D944-4074-8548-B3B524305905}</ProjectGuid>
     <ProjectGuid>{8FBEC238-D944-4074-8548-B3B524305905}</ProjectGuid>
-    <OutputType>Library</OutputType>
     <OutputPath>bin/$(Configuration)</OutputPath>
     <OutputPath>bin/$(Configuration)</OutputPath>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <RootNamespace>Godot</RootNamespace>
     <RootNamespace>Godot</RootNamespace>
-    <AssemblyName>GodotSharpEditor</AssemblyName>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <TargetFramework>netstandard2.0</TargetFramework>
     <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile>
     <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile>
-    <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath>
+    <EnableDefaultItems>false</EnableDefaultItems>
   </PropertyGroup>
   </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <DebugSymbols>true</DebugSymbols>
-    <DebugType>portable</DebugType>
-    <Optimize>false</Optimize>
-    <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <DebugType>portable</DebugType>
-    <Optimize>true</Optimize>
-    <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
+  <PropertyGroup>
+    <DefineConstants>$(DefineConstants);GODOT</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
-    <Reference Include="System" />
-  </ItemGroup>
-  <ItemGroup>
-    <Compile Include="Properties\AssemblyInfo.cs" />
-  </ItemGroup>
-  <Import Project="Generated\GeneratedIncludes.props" />
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\GodotSharp\GodotSharp.csproj">
     <ProjectReference Include="..\GodotSharp\GodotSharp.csproj">
-      <Private>False</Private>
+      <Private>false</Private>
     </ProjectReference>
     </ProjectReference>
   </ItemGroup>
   </ItemGroup>
-  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <!--
+  We import a props file with auto-generated includes. This works well with Rider.
+  However, Visual Studio and MonoDevelop won't list them in the solution explorer.
+  We can't use wildcards as there may be undesired old files still hanging around.
+  Fortunately code completion, go to definition and such still work.
+  -->
+  <Import Project="Generated\GeneratedIncludes.props" />
 </Project>
 </Project>

+ 0 - 25
modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs

@@ -1,25 +0,0 @@
-using System.Reflection;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("GodotSharpEditor")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("1.0.*")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]

+ 17 - 14
modules/mono/mono_gd/gd_mono.cpp

@@ -521,8 +521,8 @@ void GDMono::add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly) {
 
 
 GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
 GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
 
 
-	if (p_name == "mscorlib")
-		return get_corlib_assembly();
+	if (p_name == "mscorlib" && corlib_assembly)
+		return corlib_assembly;
 
 
 	MonoDomain *domain = mono_domain_get();
 	MonoDomain *domain = mono_domain_get();
 	uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0;
 	uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0;
@@ -532,7 +532,9 @@ GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
 
 
 bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly) {
 bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly) {
 
 
+#ifdef DEBUG_ENABLED
 	CRASH_COND(!r_assembly);
 	CRASH_COND(!r_assembly);
+#endif
 
 
 	MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
 	MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
 	bool result = load_assembly(p_name, aname, r_assembly, p_refonly);
 	bool result = load_assembly(p_name, aname, r_assembly, p_refonly);
@@ -544,26 +546,27 @@ bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bo
 
 
 bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly) {
 bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly) {
 
 
+#ifdef DEBUG_ENABLED
 	CRASH_COND(!r_assembly);
 	CRASH_COND(!r_assembly);
+#endif
 
 
-	print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "...");
-
-	MonoImageOpenStatus status = MONO_IMAGE_OK;
-	MonoAssembly *assembly = mono_assembly_load_full(p_aname, NULL, &status, p_refonly);
+	return load_assembly(p_name, p_aname, r_assembly, p_refonly, GDMonoAssembly::get_default_search_dirs());
+}
 
 
-	if (!assembly)
-		return false;
+bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs) {
 
 
-	ERR_FAIL_COND_V(status != MONO_IMAGE_OK, false);
+#ifdef DEBUG_ENABLED
+	CRASH_COND(!r_assembly);
+#endif
 
 
-	uint32_t domain_id = mono_domain_get_id(mono_domain_get());
+	print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "...");
 
 
-	GDMonoAssembly **stored_assembly = assemblies[domain_id].getptr(p_name);
+	GDMonoAssembly *assembly = GDMonoAssembly::load(p_name, p_aname, p_refonly, p_search_dirs);
 
 
-	ERR_FAIL_COND_V(stored_assembly == NULL, false);
-	ERR_FAIL_COND_V((*stored_assembly)->get_assembly() != assembly, false);
+	if (!assembly)
+		return false;
 
 
-	*r_assembly = *stored_assembly;
+	*r_assembly = assembly;
 
 
 	print_verbose("Mono: Assembly " + p_name + (p_refonly ? " (refonly)" : "") + " loaded from path: " + (*r_assembly)->get_path());
 	print_verbose("Mono: Assembly " + p_name + (p_refonly ? " (refonly)" : "") + " loaded from path: " + (*r_assembly)->get_path());
 
 

+ 1 - 0
modules/mono/mono_gd/gd_mono.h

@@ -241,6 +241,7 @@ public:
 
 
 	bool load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly = false);
 	bool load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly = false);
 	bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly = false);
 	bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly = false);
+	bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs);
 	bool load_assembly_from(const String &p_name, const String &p_path, GDMonoAssembly **r_assembly, bool p_refonly = false);
 	bool load_assembly_from(const String &p_name, const String &p_path, GDMonoAssembly **r_assembly, bool p_refonly = false);
 
 
 	Error finalize_and_unload_domain(MonoDomain *p_domain);
 	Error finalize_and_unload_domain(MonoDomain *p_domain);

+ 58 - 15
modules/mono/mono_gd/gd_mono_assembly.cpp

@@ -33,6 +33,7 @@
 #include <mono/metadata/mono-debug.h>
 #include <mono/metadata/mono-debug.h>
 #include <mono/metadata/tokentype.h>
 #include <mono/metadata/tokentype.h>
 
 
+#include "core/io/file_access_pack.h"
 #include "core/list.h"
 #include "core/list.h"
 #include "core/os/file_access.h"
 #include "core/os/file_access.h"
 #include "core/os/os.h"
 #include "core/os/os.h"
@@ -99,7 +100,7 @@ void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const Strin
 // - The 'load' hook is called after the assembly has been loaded. Its job is to add the
 // - The 'load' hook is called after the assembly has been loaded. Its job is to add the
 //   assembly to the list of loaded assemblies so that the 'search' hook can look it up.
 //   assembly to the list of loaded assemblies so that the 'search' hook can look it up.
 
 
-void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) {
+void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, [[maybe_unused]] void *user_data) {
 
 
 	String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
 	String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
 
 
@@ -133,9 +134,7 @@ MonoAssembly *GDMonoAssembly::assembly_refonly_preload_hook(MonoAssemblyName *an
 	return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, true);
 	return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, true);
 }
 }
 
 
-MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly) {
-
-	(void)user_data; // UNUSED
+MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, [[maybe_unused]] void *user_data, bool refonly) {
 
 
 	String name = String::utf8(mono_assembly_name_get_name(aname));
 	String name = String::utf8(mono_assembly_name_get_name(aname));
 	bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
 	bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
@@ -147,15 +146,13 @@ MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_d
 	return NULL;
 	return NULL;
 }
 }
 
 
-MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, void *user_data, bool refonly) {
-
-	(void)user_data; // UNUSED
+MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, [[maybe_unused]] void *user_data, bool refonly) {
 
 
 	String name = String::utf8(mono_assembly_name_get_name(aname));
 	String name = String::utf8(mono_assembly_name_get_name(aname));
-	return _load_assembly_search(name, search_dirs, refonly);
+	return _load_assembly_search(name, aname, refonly, search_dirs);
 }
 }
 
 
-MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) {
+MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
 
 
 	MonoAssembly *res = NULL;
 	MonoAssembly *res = NULL;
 	String path;
 	String path;
@@ -168,21 +165,21 @@ MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const
 		if (has_extension) {
 		if (has_extension) {
 			path = search_dir.plus_file(p_name);
 			path = search_dir.plus_file(p_name);
 			if (FileAccess::exists(path)) {
 			if (FileAccess::exists(path)) {
-				res = _real_load_assembly_from(path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly, p_aname);
 				if (res != NULL)
 				if (res != NULL)
 					return res;
 					return res;
 			}
 			}
 		} else {
 		} else {
 			path = search_dir.plus_file(p_name + ".dll");
 			path = search_dir.plus_file(p_name + ".dll");
 			if (FileAccess::exists(path)) {
 			if (FileAccess::exists(path)) {
-				res = _real_load_assembly_from(path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly, p_aname);
 				if (res != NULL)
 				if (res != NULL)
 					return res;
 					return res;
 			}
 			}
 
 
 			path = search_dir.plus_file(p_name + ".exe");
 			path = search_dir.plus_file(p_name + ".exe");
 			if (FileAccess::exists(path)) {
 			if (FileAccess::exists(path)) {
-				res = _real_load_assembly_from(path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly, p_aname);
 				if (res != NULL)
 				if (res != NULL)
 					return res;
 					return res;
 			}
 			}
@@ -230,7 +227,7 @@ void GDMonoAssembly::initialize() {
 	mono_install_assembly_load_hook(&assembly_load_hook, NULL);
 	mono_install_assembly_load_hook(&assembly_load_hook, NULL);
 }
 }
 
 
-MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly) {
+MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname) {
 
 
 	Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
 	Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
 	ERR_FAIL_COND_V_MSG(data.empty(), NULL, "Could read the assembly in the specified location");
 	ERR_FAIL_COND_V_MSG(data.empty(), NULL, "Could read the assembly in the specified location");
@@ -255,7 +252,33 @@ MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, boo
 			true, &status, p_refonly,
 			true, &status, p_refonly,
 			image_filename.utf8());
 			image_filename.utf8());
 
 
-	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, NULL, "Failed to open assembly image from the loaded data");
+	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, NULL, "Failed to open assembly image from memory: '" + p_path + "'.");
+
+	if (p_aname != nullptr) {
+		// Check assembly version
+		const MonoTableInfo *table = mono_image_get_table_info(image, MONO_TABLE_ASSEMBLY);
+
+		ERR_FAIL_NULL_V(table, nullptr);
+
+		if (mono_table_info_get_rows(table)) {
+			uint32_t cols[MONO_ASSEMBLY_SIZE];
+			mono_metadata_decode_row(table, 0, cols, MONO_ASSEMBLY_SIZE);
+
+			// Not sure about .NET's policy. We will only ensure major and minor are equal, and ignore build and revision.
+			uint16_t major = cols[MONO_ASSEMBLY_MAJOR_VERSION];
+			uint16_t minor = cols[MONO_ASSEMBLY_MINOR_VERSION];
+
+			uint16_t required_minor;
+			uint16_t required_major = mono_assembly_name_get_version(p_aname, &required_minor, nullptr, nullptr);
+
+			if (required_major != 0) {
+				if (major != required_major && minor != required_minor) {
+					mono_image_close(image);
+					return nullptr;
+				}
+			}
+		}
+	}
 
 
 #ifdef DEBUG_ENABLED
 #ifdef DEBUG_ENABLED
 	Vector<uint8_t> pdb_data;
 	Vector<uint8_t> pdb_data;
@@ -283,7 +306,7 @@ no_pdb:
 
 
 	MonoAssembly *assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, p_refonly);
 	MonoAssembly *assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, p_refonly);
 
 
-	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !assembly, NULL, "Failed to load assembly for image");
+	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !assembly, nullptr, "Failed to load assembly for image");
 
 
 	if (need_manual_load_hook) {
 	if (need_manual_load_hook) {
 		// For some reason if an assembly survived domain reloading (maybe because it's referenced somewhere else),
 		// For some reason if an assembly survived domain reloading (maybe because it's referenced somewhere else),
@@ -425,6 +448,26 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class)
 	return match;
 	return match;
 }
 }
 
 
+GDMonoAssembly *GDMonoAssembly::load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
+
+	if (GDMono::get_singleton()->get_corlib_assembly() && (p_name == "mscorlib" || p_name == "mscorlib.dll"))
+		return GDMono::get_singleton()->get_corlib_assembly();
+
+	// We need to manually call the search hook in this case, as it won't be called in the next step
+	MonoAssembly *assembly = mono_assembly_invoke_search_hook(p_aname);
+
+	if (!assembly) {
+		assembly = _load_assembly_search(p_name, p_aname, p_refonly, p_search_dirs);
+		ERR_FAIL_NULL_V(assembly, nullptr);
+	}
+
+	GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
+	ERR_FAIL_NULL_V_MSG(loaded_asm, nullptr, "Loaded assembly missing from table. Did we not receive the load hook?");
+	ERR_FAIL_COND_V(loaded_asm->get_assembly() != assembly, nullptr);
+
+	return loaded_asm;
+}
+
 GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
 GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
 
 
 	if (p_name == "mscorlib" || p_name == "mscorlib.dll")
 	if (p_name == "mscorlib" || p_name == "mscorlib.dll")

+ 4 - 2
modules/mono/mono_gd/gd_mono_assembly.h

@@ -93,8 +93,8 @@ class GDMonoAssembly {
 	static MonoAssembly *_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly);
 	static MonoAssembly *_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly);
 	static MonoAssembly *_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data, bool refonly);
 	static MonoAssembly *_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data, bool refonly);
 
 
-	static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly);
-	static MonoAssembly *_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly);
+	static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname = nullptr);
+	static MonoAssembly *_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs);
 
 
 	friend class GDMono;
 	friend class GDMono;
 	static void initialize();
 	static void initialize();
@@ -120,7 +120,9 @@ public:
 	static String find_assembly(const String &p_name);
 	static String find_assembly(const String &p_name);
 
 
 	static void fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config = String(), const String &p_custom_bcl_dir = String());
 	static void fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config = String(), const String &p_custom_bcl_dir = String());
+	static const Vector<String> &get_default_search_dirs() { return search_dirs; }
 
 
+	static GDMonoAssembly *load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs);
 	static GDMonoAssembly *load_from(const String &p_name, const String &p_path, bool p_refonly);
 	static GDMonoAssembly *load_from(const String &p_name, const String &p_path, bool p_refonly);
 
 
 	GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly);
 	GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly);

+ 1 - 1
modules/mono/mono_gd/gd_mono_log.cpp

@@ -175,7 +175,7 @@ void GDMonoLog::initialize() {
 	log_level_id = get_log_level_id(log_level.get_data());
 	log_level_id = get_log_level_id(log_level.get_data());
 
 
 	if (log_file) {
 	if (log_file) {
-		OS::get_singleton()->print("Mono: Logfile is: %s\n", log_file_path.utf8().get_data());
+		OS::get_singleton()->print("Mono: Log file is: '%s'\n", log_file_path.utf8().get_data());
 		mono_trace_set_log_handler(mono_log_callback, this);
 		mono_trace_set_log_handler(mono_log_callback, this);
 	} else {
 	} else {
 		OS::get_singleton()->printerr("Mono: No log file, using default log handler\n");
 		OS::get_singleton()->printerr("Mono: No log file, using default log handler\n");

+ 21 - 0
modules/mono/utils/macros.h

@@ -81,4 +81,25 @@
 	} while (true);
 	} while (true);
 #endif
 #endif
 
 
+namespace gdmono {
+
+template <typename F>
+struct ScopeExit {
+	ScopeExit(F p_exit_func) :
+			exit_func(p_exit_func) {}
+	~ScopeExit() { exit_func(); }
+	F exit_func;
+};
+
+class ScopeExitAux {
+public:
+	template <typename F>
+	ScopeExit<F> operator+(F p_exit_func) { return ScopeExit<F>(p_exit_func); }
+};
+
+} // namespace gdmono
+
+#define SCOPE_EXIT \
+	auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]() -> void
+
 #endif // UTIL_MACROS_H
 #endif // UTIL_MACROS_H