Переглянути джерело

C#: Add Ide Connection library and server for the editor

This will be used for communicating between the Godot editor and external IDEs/editors, for things like opening files, triggering hot-reload and running the game with a debugger attached.
Ignacio Etcheverry 6 роки тому
батько
коміт
0b94203a79
44 змінених файлів з 1636 додано та 239 видалено
  1. 9 0
      editor/editor_node.cpp
  2. 3 0
      editor/editor_node.h
  3. 8 2
      modules/mono/build_scripts/godot_tools_build.py
  4. 33 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
  5. 94 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
  6. 219 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
  7. 207 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
  8. 24 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs
  9. 24 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs
  10. 45 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs
  11. 52 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
  12. 13 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs
  13. 21 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
  14. 46 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
  15. 88 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
  16. 35 0
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs
  17. 6 0
      modules/mono/editor/GodotTools/GodotTools.sln
  18. 15 15
      modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs
  19. 8 8
      modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs
  20. 7 7
      modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
  21. 4 4
      modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs
  22. 34 53
      modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
  23. 10 10
      modules/mono/editor/GodotTools/GodotTools/BuildTab.cs
  24. 1 1
      modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs
  25. 11 0
      modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs
  26. 48 73
      modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
  27. 2 2
      modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs
  28. 15 9
      modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
  29. 166 0
      modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
  30. 208 0
      modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs
  31. 8 0
      modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs
  32. 14 29
      modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
  33. 15 0
      modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
  34. 8 0
      modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs
  35. 64 0
      modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs
  36. 18 1
      modules/mono/editor/editor_internal_calls.cpp
  37. 13 0
      modules/mono/glue/Managed/Files/Dispatcher.cs
  38. 3 4
      modules/mono/glue/Managed/Files/GodotSynchronizationContext.cs
  39. 3 6
      modules/mono/glue/Managed/Files/GodotTaskScheduler.cs
  40. 2 2
      modules/mono/glue/Managed/Managed.csproj
  41. 7 0
      modules/mono/glue/gd_glue.cpp
  42. 2 0
      modules/mono/glue/gd_glue.h
  43. 22 12
      modules/mono/mono_gd/gd_mono.cpp
  44. 1 1
      modules/mono/mono_gd/gd_mono.h

+ 9 - 0
editor/editor_node.cpp

@@ -4339,6 +4339,15 @@ bool EditorNode::ensure_main_scene(bool p_from_native) {
 	return true;
 }
 
+void EditorNode::run_play() {
+	_menu_option_confirm(RUN_STOP, true);
+	_run(false);
+}
+
+void EditorNode::run_stop() {
+	_menu_option_confirm(RUN_STOP, false);
+}
+
 int EditorNode::get_current_tab() {
 	return scene_tabs->get_current_tab();
 }

+ 3 - 0
editor/editor_node.h

@@ -867,6 +867,9 @@ public:
 	static void add_build_callback(EditorBuildCallback p_callback);
 
 	bool ensure_main_scene(bool p_from_native);
+
+	void run_play();
+	void run_stop();
 };
 
 struct EditorProgress {

+ 8 - 2
modules/mono/build_scripts/godot_tools_build.py

@@ -84,10 +84,16 @@ def build(env_mono):
     source_filenames = ['GodotSharp.dll', 'GodotSharpEditor.dll']
     sources = [os.path.join(editor_api_dir, filename) for filename in source_filenames]
 
-    target_filenames = ['GodotTools.dll', 'GodotTools.BuildLogger.dll', 'GodotTools.ProjectEditor.dll', 'DotNet.Glob.dll', 'GodotTools.Core.dll']
+    target_filenames = [
+        'GodotTools.dll', 'GodotTools.IdeConnection.dll', 'GodotTools.BuildLogger.dll',
+        'GodotTools.ProjectEditor.dll', 'DotNet.Glob.dll', 'GodotTools.Core.dll'
+    ]
 
     if env_mono['target'] == 'debug':
-        target_filenames += ['GodotTools.pdb', 'GodotTools.BuildLogger.pdb', 'GodotTools.ProjectEditor.pdb', 'GodotTools.Core.pdb']
+        target_filenames += [
+            'GodotTools.pdb', 'GodotTools.IdeConnection.pdb', 'GodotTools.BuildLogger.pdb',
+            'GodotTools.ProjectEditor.pdb', 'GodotTools.Core.pdb'
+        ]
 
     targets = [os.path.join(editor_tools_dir, filename) for filename in target_filenames]
 

+ 33 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs

@@ -0,0 +1,33 @@
+using System;
+
+namespace GodotTools.IdeConnection
+{
+    public class ConsoleLogger : ILogger
+    {
+        public void LogDebug(string message)
+        {
+            Console.WriteLine("DEBUG: " + message);
+        }
+
+        public void LogInfo(string message)
+        {
+            Console.WriteLine("INFO: " + message);
+        }
+
+        public void LogWarning(string message)
+        {
+            Console.WriteLine("WARN: " + message);
+        }
+
+        public void LogError(string message)
+        {
+            Console.WriteLine("ERROR: " + message);
+        }
+
+        public void LogError(string message, Exception e)
+        {
+            Console.WriteLine("EXCEPTION: " + message);
+            Console.WriteLine(e);
+        }
+    }
+}

+ 94 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs

@@ -0,0 +1,94 @@
+using System;
+using Path = System.IO.Path;
+
+namespace GodotTools.IdeConnection
+{
+    public class GodotIdeBase : IDisposable
+    {
+        private ILogger logger;
+
+        public ILogger Logger
+        {
+            get => logger ?? (logger = new ConsoleLogger());
+            set => logger = value;
+        }
+
+        private readonly string projectMetadataDir;
+
+        protected const string MetaFileName = "ide_server_meta.txt";
+        protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName);
+
+        private GodotIdeConnection connection;
+        protected readonly object ConnectionLock = new object();
+
+        public bool IsDisposed { get; private set; } = false;
+
+        public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected;
+
+        public event Action Connected
+        {
+            add
+            {
+                if (connection != null && !connection.IsDisposed)
+                    connection.Connected += value;
+            }
+            remove
+            {
+                if (connection != null && !connection.IsDisposed)
+                    connection.Connected -= value;
+            }
+        }
+
+        protected GodotIdeConnection Connection
+        {
+            get => connection;
+            set
+            {
+                connection?.Dispose();
+                connection = value;
+            }
+        }
+
+        protected GodotIdeBase(string projectMetadataDir)
+        {
+            this.projectMetadataDir = projectMetadataDir;
+        }
+
+        protected void DisposeConnection()
+        {
+            lock (ConnectionLock)
+            {
+                connection?.Dispose();
+            }
+        }
+
+        ~GodotIdeBase()
+        {
+            Dispose(disposing: false);
+        }
+
+        public void Dispose()
+        {
+            if (IsDisposed)
+                return;
+
+            lock (ConnectionLock)
+            {
+                if (IsDisposed) // lock may not be fair
+                    return;
+                IsDisposed = true;
+            }
+
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                connection?.Dispose();
+            }
+        }
+    }
+}

+ 219 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs

@@ -0,0 +1,219 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace GodotTools.IdeConnection
+{
+    public abstract class GodotIdeClient : GodotIdeBase
+    {
+        protected GodotIdeMetadata GodotIdeMetadata;
+
+        private readonly FileSystemWatcher fsWatcher;
+
+        protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir)
+        {
+            messageHandlers = InitializeMessageHandlers();
+
+            // FileSystemWatcher requires an existing directory
+            if (!File.Exists(projectMetadataDir))
+                Directory.CreateDirectory(projectMetadataDir);
+
+            fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName);
+        }
+
+        private void OnMetaFileChanged(object sender, FileSystemEventArgs e)
+        {
+            if (IsDisposed)
+                return;
+
+            lock (ConnectionLock)
+            {
+                if (IsDisposed)
+                    return;
+
+                if (!File.Exists(MetaFilePath))
+                    return;
+
+                var metadata = ReadMetadataFile();
+
+                if (metadata != null && metadata != GodotIdeMetadata)
+                {
+                    GodotIdeMetadata = metadata.Value;
+                    ConnectToServer();
+                }
+            }
+        }
+
+        private void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
+        {
+            if (IsDisposed)
+                return;
+
+            if (IsConnected)
+                DisposeConnection();
+
+            // The file may have been re-created
+
+            lock (ConnectionLock)
+            {
+                if (IsDisposed)
+                    return;
+
+                if (IsConnected || !File.Exists(MetaFilePath))
+                    return;
+
+                var metadata = ReadMetadataFile();
+
+                if (metadata != null)
+                {
+                    GodotIdeMetadata = metadata.Value;
+                    ConnectToServer();
+                }
+            }
+        }
+
+        private GodotIdeMetadata? ReadMetadataFile()
+        {
+            using (var reader = File.OpenText(MetaFilePath))
+            {
+                string portStr = reader.ReadLine();
+
+                if (portStr == null)
+                    return null;
+
+                string editorExecutablePath = reader.ReadLine();
+
+                if (editorExecutablePath == null)
+                    return null;
+
+                if (!int.TryParse(portStr, out int port))
+                    return null;
+
+                return new GodotIdeMetadata(port, editorExecutablePath);
+            }
+        }
+
+        private void ConnectToServer()
+        {
+            var tcpClient = new TcpClient();
+
+            Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage);
+            Connection.Logger = Logger;
+
+            try
+            {
+                Logger.LogInfo("Connecting to Godot Ide Server");
+                
+                tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port);
+
+                Logger.LogInfo("Connection open with Godot Ide Server");
+
+                var clientThread = new Thread(Connection.Start)
+                {
+                    IsBackground = true,
+                    Name = "Godot Ide Connection Client"
+                };
+                clientThread.Start();
+            }
+            catch (SocketException e)
+            {
+                if (e.SocketErrorCode == SocketError.ConnectionRefused)
+                    Logger.LogError("The connection to the Godot Ide Server was refused");
+                else
+                    throw;
+            }
+        }
+
+        public void Start()
+        {
+            Logger.LogInfo("Starting Godot Ide Client");
+            
+            fsWatcher.Changed += OnMetaFileChanged;
+            fsWatcher.Deleted += OnMetaFileDeleted;
+            fsWatcher.EnableRaisingEvents = true;
+
+            lock (ConnectionLock)
+            {
+                if (IsDisposed)
+                    return;
+
+                if (!File.Exists(MetaFilePath))
+                {
+                    Logger.LogInfo("There is no Godot Ide Server running");
+                    return;
+                }
+
+                var metadata = ReadMetadataFile();
+
+                if (metadata != null)
+                {
+                    GodotIdeMetadata = metadata.Value;
+                    ConnectToServer();
+                }
+                else
+                {
+                    Logger.LogError("Failed to read Godot Ide metadata file");
+                }
+            }
+        }
+
+        public bool WriteMessage(Message message)
+        {
+            return Connection.WriteMessage(message);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            base.Dispose(disposing);
+
+            if (disposing)
+            {
+                fsWatcher?.Dispose();
+            }
+        }
+
+        protected virtual bool HandleMessage(Message message)
+        {
+            if (messageHandlers.TryGetValue(message.Id, out var action))
+            {
+                action(message.Arguments);
+                return true;
+            }
+
+            return false;
+        }
+
+        private readonly Dictionary<string, Action<string[]>> messageHandlers;
+
+        private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
+        {
+            return new Dictionary<string, Action<string[]>>
+            {
+                ["OpenFile"] = args =>
+                {
+                    switch (args.Length)
+                    {
+                        case 1:
+                            OpenFile(file: args[0]);
+                            return;
+                        case 2:
+                            OpenFile(file: args[0], line: int.Parse(args[1]));
+                            return;
+                        case 3:
+                            OpenFile(file: args[0], line: int.Parse(args[1]), column: int.Parse(args[2]));
+                            return;
+                        default:
+                            throw new ArgumentException();
+                    }
+                }
+            };
+        }
+
+        protected abstract void OpenFile(string file);
+        protected abstract void OpenFile(string file, int line);
+        protected abstract void OpenFile(string file, int line, int column);
+    }
+}

+ 207 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs

@@ -0,0 +1,207 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+
+namespace GodotTools.IdeConnection
+{
+    public abstract class GodotIdeConnection : IDisposable
+    {
+        protected const string Version = "1.0";
+
+        protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}";
+        protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}";
+
+        private const int ClientWriteTimeout = 8000;
+        private readonly TcpClient tcpClient;
+
+        private TextReader clientReader;
+        private TextWriter clientWriter;
+
+        private readonly object writeLock = new object();
+
+        private readonly Func<Message, bool> messageHandler;
+
+        public event Action Connected;
+
+        private ILogger logger;
+
+        public ILogger Logger
+        {
+            get => logger ?? (logger = new ConsoleLogger());
+            set => logger = value;
+        }
+
+        public bool IsDisposed { get; private set; } = false;
+
+        public bool IsConnected => tcpClient.Client != null && tcpClient.Client.Connected;
+
+        protected GodotIdeConnection(TcpClient tcpClient, Func<Message, bool> messageHandler)
+        {
+            this.tcpClient = tcpClient;
+            this.messageHandler = messageHandler;
+        }
+
+        public void Start()
+        {
+            try
+            {
+                if (!StartConnection())
+                    return;
+
+                string messageLine;
+                while ((messageLine = ReadLine()) != null)
+                {
+                    if (!MessageParser.TryParse(messageLine, out Message msg))
+                    {
+                        Logger.LogError($"Received message with invalid format: {messageLine}");
+                        continue;
+                    }
+
+                    Logger.LogDebug($"Received message: {msg}");
+
+                    if (msg.Id == "close")
+                    {
+                        Logger.LogInfo("Closing connection");
+                        return;
+                    }
+
+                    try
+                    {
+                        try
+                        {
+                            Debug.Assert(messageHandler != null);
+
+                            if (!messageHandler(msg))
+                                Logger.LogError($"Received unknown message: {msg}");
+                        }
+                        catch (Exception e)
+                        {
+                            Logger.LogError($"Message handler for '{msg}' failed with exception", e);
+                        }
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e);
+            }
+            finally
+            {
+                Dispose();
+            }
+        }
+
+        private bool StartConnection()
+        {
+            NetworkStream clientStream = tcpClient.GetStream();
+
+            clientReader = new StreamReader(clientStream, Encoding.UTF8);
+
+            lock (writeLock)
+                clientWriter = new StreamWriter(clientStream, Encoding.UTF8);
+
+            clientStream.WriteTimeout = ClientWriteTimeout;
+
+            if (!WriteHandshake())
+            {
+                Logger.LogError("Could not write handshake");
+                return false;
+            }
+
+            if (!IsValidResponseHandshake(ReadLine()))
+            {
+                Logger.LogError("Received invalid handshake");
+                return false;
+            }
+
+            Connected?.Invoke();
+
+            Logger.LogInfo("Godot Ide connection started");
+
+            return true;
+        }
+
+        private string ReadLine()
+        {
+            try
+            {
+                return clientReader?.ReadLine();
+            }
+            catch (Exception e)
+            {
+                if (IsDisposed)
+                {
+                    var se = e as SocketException ?? e.InnerException as SocketException;
+                    if (se != null && se.SocketErrorCode == SocketError.Interrupted)
+                        return null;
+                }
+
+                throw;
+            }
+        }
+
+        public bool WriteMessage(Message message)
+        {
+            Logger.LogDebug($"Sending message {message}");
+            
+            var messageComposer = new MessageComposer();
+
+            messageComposer.AddArgument(message.Id);
+            foreach (string argument in message.Arguments)
+                messageComposer.AddArgument(argument);
+
+            return WriteLine(messageComposer.ToString());
+        }
+
+        protected bool WriteLine(string text)
+        {
+            if (clientWriter == null || IsDisposed || !IsConnected)
+                return false;
+
+            lock (writeLock)
+            {
+                try
+                {
+                    clientWriter.WriteLine(text);
+                    clientWriter.Flush();
+                }
+                catch (Exception e)
+                {
+                    if (!IsDisposed)
+                    {
+                        var se = e as SocketException ?? e.InnerException as SocketException;
+                        if (se != null && se.SocketErrorCode == SocketError.Shutdown)
+                            Logger.LogInfo("Client disconnected ungracefully");
+                        else
+                            Logger.LogError("Exception thrown when trying to write to client", e);
+
+                        Dispose();
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        protected abstract bool WriteHandshake();
+        protected abstract bool IsValidResponseHandshake(string handshakeLine);
+
+        public void Dispose()
+        {
+            if (IsDisposed)
+                return;
+
+            IsDisposed = true;
+
+            clientReader?.Dispose();
+            clientWriter?.Dispose();
+            ((IDisposable) tcpClient)?.Dispose();
+        }
+    }
+}

+ 24 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeConnection
+{
+    public class GodotIdeConnectionClient : GodotIdeConnection
+    {
+        public GodotIdeConnectionClient(TcpClient tcpClient, Func<Message, bool> messageHandler)
+            : base(tcpClient, messageHandler)
+        {
+        }
+
+        protected override bool WriteHandshake()
+        {
+            return WriteLine(ClientHandshake);
+        }
+
+        protected override bool IsValidResponseHandshake(string handshakeLine)
+        {
+            return handshakeLine == ServerHandshake;
+        }
+    }
+}

+ 24 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeConnection
+{
+    public class GodotIdeConnectionServer : GodotIdeConnection
+    {
+        public GodotIdeConnectionServer(TcpClient tcpClient, Func<Message, bool> messageHandler)
+            : base(tcpClient, messageHandler)
+        {
+        }
+
+        protected override bool WriteHandshake()
+        {
+            return WriteLine(ServerHandshake);
+        }
+
+        protected override bool IsValidResponseHandshake(string handshakeLine)
+        {
+            return handshakeLine == ClientHandshake;
+        }
+    }
+}

+ 45 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs

@@ -0,0 +1,45 @@
+namespace GodotTools.IdeConnection
+{
+    public struct GodotIdeMetadata
+    {
+        public int Port { get; }
+        public string EditorExecutablePath { get; }
+
+        public GodotIdeMetadata(int port, string editorExecutablePath)
+        {
+            Port = port;
+            EditorExecutablePath = editorExecutablePath;
+        }
+
+        public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b)
+        {
+            return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath;
+        }
+
+        public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b)
+        {
+            return !(a == b);
+        }
+
+        public override bool Equals(object obj)
+        {
+            if (obj is GodotIdeMetadata metadata)
+                return metadata == this;
+
+            return false;
+        }
+
+        public bool Equals(GodotIdeMetadata other)
+        {
+            return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath;
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0);
+            }
+        }
+    }
+}

+ 52 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>GodotTools.IdeConnection</RootNamespace>
+    <AssemblyName>GodotTools.IdeConnection</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>portable</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>portable</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ConsoleLogger.cs" />
+    <Compile Include="GodotIdeMetadata.cs" />
+    <Compile Include="GodotIdeBase.cs" />
+    <Compile Include="GodotIdeClient.cs" />
+    <Compile Include="GodotIdeConnection.cs" />
+    <Compile Include="GodotIdeConnectionClient.cs" />
+    <Compile Include="GodotIdeConnectionServer.cs" />
+    <Compile Include="ILogger.cs" />
+    <Compile Include="Message.cs" />
+    <Compile Include="MessageComposer.cs" />
+    <Compile Include="MessageParser.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>

+ 13 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs

@@ -0,0 +1,13 @@
+using System;
+
+namespace GodotTools.IdeConnection
+{
+    public interface ILogger
+    {
+        void LogDebug(string message);
+        void LogInfo(string message);
+        void LogWarning(string message);
+        void LogError(string message);
+        void LogError(string message, Exception e);
+    }
+}

+ 21 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs

@@ -0,0 +1,21 @@
+using System.Linq;
+
+namespace GodotTools.IdeConnection
+{
+    public struct Message
+    {
+        public string Id { get; set; }
+        public string[] Arguments { get; set; }
+
+        public Message(string id, params string[] arguments)
+        {
+            Id = id;
+            Arguments = arguments;
+        }
+
+        public override string ToString()
+        {
+            return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')";
+        }
+    }
+}

+ 46 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs

@@ -0,0 +1,46 @@
+using System.Linq;
+using System.Text;
+
+namespace GodotTools.IdeConnection
+{
+    public class MessageComposer
+    {
+        private readonly StringBuilder stringBuilder = new StringBuilder();
+
+        private static readonly char[] CharsToEscape = { '\\', '"' };
+
+        public void AddArgument(string argument)
+        {
+            AddArgument(argument, quoted: argument.Contains(","));
+        }
+
+        public void AddArgument(string argument, bool quoted)
+        {
+            if (stringBuilder.Length > 0)
+                stringBuilder.Append(',');
+
+            if (quoted)
+            {
+                stringBuilder.Append('"');
+                
+                foreach (char @char in argument)
+                {
+                    if (CharsToEscape.Contains(@char))
+                        stringBuilder.Append('\\');
+                    stringBuilder.Append(@char);
+                }
+                
+                stringBuilder.Append('"');
+            }
+            else
+            {
+                stringBuilder.Append(argument);
+            }
+        }
+
+        public override string ToString()
+        {
+            return stringBuilder.ToString();
+        }
+    }
+}

+ 88 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs

@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace GodotTools.IdeConnection
+{
+    public static class MessageParser
+    {
+        public static bool TryParse(string messageLine, out Message message)
+        {
+            var arguments = new List<string>();
+            var stringBuilder = new StringBuilder();
+
+            bool expectingArgument = true;
+
+            for (int i = 0; i < messageLine.Length; i++)
+            {
+                char @char = messageLine[i];
+
+                if (@char == ',')
+                {
+                    if (expectingArgument)
+                        arguments.Add(string.Empty);
+
+                    expectingArgument = true;
+                    continue;
+                }
+
+                bool quoted = false;
+
+                if (messageLine[i] == '"')
+                {
+                    quoted = true;
+                    i++;
+                }
+
+                while (i < messageLine.Length)
+                {
+                    @char = messageLine[i];
+                    
+                    if (quoted && @char == '"')
+                    {
+                        i++;
+                        break;
+                    }
+
+                    if (@char == '\\')
+                    {
+                        i++;
+                        if (i < messageLine.Length)
+                            break;
+
+                        stringBuilder.Append(messageLine[i]);
+                    }
+                    else if (!quoted && @char == ',')
+                    {
+                        break; // We don't increment the counter to allow the colon to be parsed after this
+                    }
+                    else
+                    {
+                        stringBuilder.Append(@char);
+                    }
+                    
+                    i++;
+                }
+                
+                arguments.Add(stringBuilder.ToString());
+                stringBuilder.Clear();
+
+                expectingArgument = false;
+            }
+
+            if (arguments.Count == 0)
+            {
+                message = new Message();
+                return false;
+            }
+
+            message = new Message
+            {
+                Id = arguments[0],
+                Arguments = arguments.Skip(1).ToArray()
+            };
+
+            return true;
+        }
+    }
+}

+ 35 - 0
modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs

@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("GodotTools.IdeConnection")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("Godot Engine contributors")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("92600954-25F0-4291-8E11-1FEE9FC4BE20")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 6 - 0
modules/mono/editor/GodotTools/GodotTools.sln

@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotToo
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeConnection", "GodotTools.IdeConnection\GodotTools.IdeConnection.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -31,5 +33,9 @@ Global
 		{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 EndGlobal

+ 15 - 15
modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs → modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs

@@ -9,7 +9,7 @@ using Path = System.IO.Path;
 
 namespace GodotTools
 {
-    public class MonoBottomPanel : VBoxContainer
+    public class BottomPanel : VBoxContainer
     {
         private EditorInterface editorInterface;
 
@@ -34,7 +34,7 @@ namespace GodotTools
 
             for (int i = 0; i < buildTabs.GetChildCount(); i++)
             {
-                var tab = (MonoBuildTab) buildTabs.GetChild(i);
+                var tab = (BuildTab) buildTabs.GetChild(i);
 
                 if (tab == null)
                     continue;
@@ -49,11 +49,11 @@ namespace GodotTools
                 itemTooltip += "\nStatus: ";
 
                 if (tab.BuildExited)
-                    itemTooltip += tab.BuildResult == MonoBuildTab.BuildResults.Success ? "Succeeded" : "Errored";
+                    itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored";
                 else
                     itemTooltip += "Running";
 
-                if (!tab.BuildExited || tab.BuildResult == MonoBuildTab.BuildResults.Error)
+                if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error)
                     itemTooltip += $"\nErrors: {tab.ErrorCount}";
 
                 itemTooltip += $"\nWarnings: {tab.WarningCount}";
@@ -68,15 +68,15 @@ namespace GodotTools
             }
         }
 
-        public MonoBuildTab GetBuildTabFor(MonoBuildInfo buildInfo)
+        public BuildTab GetBuildTabFor(BuildInfo buildInfo)
         {
-            foreach (var buildTab in new Array<MonoBuildTab>(buildTabs.GetChildren()))
+            foreach (var buildTab in new Array<BuildTab>(buildTabs.GetChildren()))
             {
                 if (buildTab.BuildInfo.Equals(buildInfo))
                     return buildTab;
             }
 
-            var newBuildTab = new MonoBuildTab(buildInfo);
+            var newBuildTab = new BuildTab(buildInfo);
             AddBuildTab(newBuildTab);
 
             return newBuildTab;
@@ -120,7 +120,7 @@ namespace GodotTools
             if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
                 throw new InvalidOperationException("No tab selected");
 
-            var buildTab = (MonoBuildTab) buildTabs.GetChild(currentTab);
+            var buildTab = (BuildTab) buildTabs.GetChild(currentTab);
             buildTab.WarningsVisible = pressed;
             buildTab.UpdateIssuesList();
         }
@@ -132,7 +132,7 @@ namespace GodotTools
             if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
                 throw new InvalidOperationException("No tab selected");
 
-            var buildTab = (MonoBuildTab) buildTabs.GetChild(currentTab);
+            var buildTab = (BuildTab) buildTabs.GetChild(currentTab);
             buildTab.ErrorsVisible = pressed;
             buildTab.UpdateIssuesList();
         }
@@ -145,7 +145,7 @@ namespace GodotTools
             string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
             string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
 
-            CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
+            CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
 
             if (File.Exists(editorScriptsMetadataPath))
             {
@@ -166,7 +166,7 @@ namespace GodotTools
                 Internal.GodotIs32Bits() ? "32" : "64"
             };
 
-            bool buildSuccess = GodotSharpBuilds.BuildProjectBlocking("Tools", godotDefines);
+            bool buildSuccess = BuildManager.BuildProjectBlocking("Tools", godotDefines);
 
             if (!buildSuccess)
                 return;
@@ -193,9 +193,9 @@ namespace GodotTools
 
             int selectedItem = selectedItems[0];
 
-            var buildTab = (MonoBuildTab) buildTabs.GetTabControl(selectedItem);
+            var buildTab = (BuildTab) buildTabs.GetTabControl(selectedItem);
 
-            OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, GodotSharpBuilds.MsBuildLogFileName));
+            OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, BuildManager.MsBuildLogFileName));
         }
 
         public override void _Notification(int what)
@@ -211,13 +211,13 @@ namespace GodotTools
             }
         }
 
-        public void AddBuildTab(MonoBuildTab buildTab)
+        public void AddBuildTab(BuildTab buildTab)
         {
             buildTabs.AddChild(buildTab);
             RaiseBuildTab(buildTab);
         }
 
-        public void RaiseBuildTab(MonoBuildTab buildTab)
+        public void RaiseBuildTab(BuildTab buildTab)
         {
             if (buildTab.GetParent() != buildTabs)
                 throw new InvalidOperationException("Build tab is not in the tabs list");

+ 8 - 8
modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs

@@ -46,8 +46,8 @@ namespace GodotTools.Build
             {
                 if (OS.IsWindows())
                 {
-                    return (GodotSharpBuilds.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool")
-                           == GodotSharpBuilds.BuildTool.MsBuildMono;
+                    return (BuildManager.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool")
+                           == BuildManager.BuildTool.MsBuildMono;
                 }
 
                 return false;
@@ -103,16 +103,16 @@ namespace GodotTools.Build
             return process;
         }
 
-        public static int Build(MonoBuildInfo monoBuildInfo)
+        public static int Build(BuildInfo buildInfo)
         {
-            return Build(monoBuildInfo.Solution, monoBuildInfo.Configuration,
-                monoBuildInfo.LogsDirPath, monoBuildInfo.CustomProperties);
+            return Build(buildInfo.Solution, buildInfo.Configuration,
+                buildInfo.LogsDirPath, buildInfo.CustomProperties);
         }
 
-        public static async Task<int> BuildAsync(MonoBuildInfo monoBuildInfo)
+        public static async Task<int> BuildAsync(BuildInfo buildInfo)
         {
-            return await BuildAsync(monoBuildInfo.Solution, monoBuildInfo.Configuration,
-                monoBuildInfo.LogsDirPath, monoBuildInfo.CustomProperties);
+            return await BuildAsync(buildInfo.Solution, buildInfo.Configuration,
+                buildInfo.LogsDirPath, buildInfo.CustomProperties);
         }
 
         public static int Build(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)

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

@@ -19,13 +19,13 @@ namespace GodotTools.Build
         public static string FindMsBuild()
         {
             var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
-            var buildTool = (GodotSharpBuilds.BuildTool) editorSettings.GetSetting("mono/builds/build_tool");
+            var buildTool = (BuildManager.BuildTool) editorSettings.GetSetting("mono/builds/build_tool");
 
             if (OS.IsWindows())
             {
                 switch (buildTool)
                 {
-                    case GodotSharpBuilds.BuildTool.MsBuildVs:
+                    case BuildManager.BuildTool.MsBuildVs:
                     {
                         if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath))
                         {
@@ -34,7 +34,7 @@ namespace GodotTools.Build
 
                             if (_msbuildToolsPath.Empty())
                             {
-                                throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameMsbuildVs}'. Tried with path: {_msbuildToolsPath}");
+                                throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildVs}'. Tried with path: {_msbuildToolsPath}");
                             }
                         }
 
@@ -43,13 +43,13 @@ namespace GodotTools.Build
 
                         return Path.Combine(_msbuildToolsPath, "MSBuild.exe");
                     }
-                    case GodotSharpBuilds.BuildTool.MsBuildMono:
+                    case BuildManager.BuildTool.MsBuildMono:
                     {
                         string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
 
                         if (!File.Exists(msbuildPath))
                         {
-                            throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameMsbuildMono}'. Tried with path: {msbuildPath}");
+                            throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildMono}'. Tried with path: {msbuildPath}");
                         }
 
                         return msbuildPath;
@@ -61,7 +61,7 @@ namespace GodotTools.Build
 
             if (OS.IsUnix())
             {
-                if (buildTool == GodotSharpBuilds.BuildTool.MsBuildMono)
+                if (buildTool == BuildManager.BuildTool.MsBuildMono)
                 {
                     if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath))
                     {
@@ -71,7 +71,7 @@ namespace GodotTools.Build
 
                     if (_msbuildUnixPath.Empty())
                     {
-                        throw new FileNotFoundException($"Cannot find binary for '{GodotSharpBuilds.PropNameMsbuildMono}'");
+                        throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'");
                     }
 
                     return _msbuildUnixPath;

+ 4 - 4
modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs → modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs

@@ -7,7 +7,7 @@ using Path = System.IO.Path;
 namespace GodotTools
 {
     [Serializable]
-    public sealed class MonoBuildInfo : Reference // TODO Remove Reference once we have proper serialization
+    public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
     {
         public string Solution { get; }
         public string Configuration { get; }
@@ -17,7 +17,7 @@ namespace GodotTools
 
         public override bool Equals(object obj)
         {
-            if (obj is MonoBuildInfo other)
+            if (obj is BuildInfo other)
                 return other.Solution == Solution && other.Configuration == Configuration;
 
             return false;
@@ -34,11 +34,11 @@ namespace GodotTools
             }
         }
 
-        private MonoBuildInfo()
+        private BuildInfo()
         {
         }
 
-        public MonoBuildInfo(string solution, string configuration)
+        public BuildInfo(string solution, string configuration)
         {
             Solution = solution;
             Configuration = configuration;

+ 34 - 53
modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs → modules/mono/editor/GodotTools/GodotTools/BuildManager.cs

@@ -6,15 +6,13 @@ using GodotTools.Build;
 using GodotTools.Internals;
 using GodotTools.Utils;
 using static GodotTools.Internals.Globals;
-using Error = Godot.Error;
 using File = GodotTools.Utils.File;
-using Directory = GodotTools.Utils.Directory;
 
 namespace GodotTools
 {
-    public static class GodotSharpBuilds
+    public static class BuildManager
     {
-        private static readonly List<MonoBuildInfo> BuildsInProgress = new List<MonoBuildInfo>();
+        private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>();
 
         public const string PropNameMsbuildMono = "MSBuild (Mono)";
         public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)";
@@ -28,7 +26,7 @@ namespace GodotTools
             MsBuildVs
         }
 
-        private static void RemoveOldIssuesFile(MonoBuildInfo buildInfo)
+        private static void RemoveOldIssuesFile(BuildInfo buildInfo)
         {
             var issuesFile = GetIssuesFilePath(buildInfo);
 
@@ -38,29 +36,21 @@ namespace GodotTools
             File.Delete(issuesFile);
         }
 
-        private static string _ApiFolderName(ApiAssemblyType apiType)
-        {
-            ulong apiHash = apiType == ApiAssemblyType.Core ?
-                Internal.GetCoreApiHash() :
-                Internal.GetEditorApiHash();
-            return $"{apiHash}_{BindingsGenerator.Version}_{BindingsGenerator.CsGlueVersion}";
-        }
-
         private static void ShowBuildErrorDialog(string message)
         {
             GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error");
-            GodotSharpEditor.Instance.MonoBottomPanel.ShowBuildTab();
+            GodotSharpEditor.Instance.BottomPanel.ShowBuildTab();
         }
 
-        public static void RestartBuild(MonoBuildTab buildTab) => throw new NotImplementedException();
-        public static void StopBuild(MonoBuildTab buildTab) => throw new NotImplementedException();
+        public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException();
+        public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException();
 
-        private static string GetLogFilePath(MonoBuildInfo buildInfo)
+        private static string GetLogFilePath(BuildInfo buildInfo)
         {
             return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName);
         }
 
-        private static string GetIssuesFilePath(MonoBuildInfo buildInfo)
+        private static string GetIssuesFilePath(BuildInfo buildInfo)
         {
             return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName);
         }
@@ -71,7 +61,7 @@ namespace GodotTools
                 Godot.GD.Print(text);
         }
 
-        public static bool Build(MonoBuildInfo buildInfo)
+        public static bool Build(BuildInfo buildInfo)
         {
             if (BuildsInProgress.Contains(buildInfo))
                 throw new InvalidOperationException("A build is already in progress");
@@ -80,7 +70,7 @@ namespace GodotTools
 
             try
             {
-                MonoBuildTab buildTab = GodotSharpEditor.Instance.MonoBottomPanel.GetBuildTabFor(buildInfo);
+                BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
                 buildTab.OnBuildStart();
 
                 // Required in order to update the build tasks list
@@ -103,7 +93,7 @@ namespace GodotTools
                     if (exitCode != 0)
                         PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
 
-                    buildTab.OnBuildExit(exitCode == 0 ? MonoBuildTab.BuildResults.Success : MonoBuildTab.BuildResults.Error);
+                    buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
 
                     return exitCode == 0;
                 }
@@ -120,7 +110,7 @@ namespace GodotTools
             }
         }
 
-        public static async Task<bool> BuildAsync(MonoBuildInfo buildInfo)
+        public static async Task<bool> BuildAsync(BuildInfo buildInfo)
         {
             if (BuildsInProgress.Contains(buildInfo))
                 throw new InvalidOperationException("A build is already in progress");
@@ -129,7 +119,7 @@ namespace GodotTools
 
             try
             {
-                MonoBuildTab buildTab = GodotSharpEditor.Instance.MonoBottomPanel.GetBuildTabFor(buildInfo);
+                BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
 
                 try
                 {
@@ -148,7 +138,7 @@ namespace GodotTools
                     if (exitCode != 0)
                         PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
 
-                    buildTab.OnBuildExit(exitCode == 0 ? MonoBuildTab.BuildResults.Success : MonoBuildTab.BuildResults.Error);
+                    buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
 
                     return exitCode == 0;
                 }
@@ -165,32 +155,6 @@ namespace GodotTools
             }
         }
 
-        public static bool BuildApiSolution(string apiSlnDir, string config)
-        {
-            string apiSlnFile = Path.Combine(apiSlnDir, $"{ApiAssemblyNames.SolutionName}.sln");
-
-            string coreApiAssemblyDir = Path.Combine(apiSlnDir, ApiAssemblyNames.Core, "bin", config);
-            string coreApiAssemblyFile = Path.Combine(coreApiAssemblyDir, $"{ApiAssemblyNames.Core}.dll");
-
-            string editorApiAssemblyDir = Path.Combine(apiSlnDir, ApiAssemblyNames.Editor, "bin", config);
-            string editorApiAssemblyFile = Path.Combine(editorApiAssemblyDir, $"{ApiAssemblyNames.Editor}.dll");
-
-            if (File.Exists(coreApiAssemblyFile) && File.Exists(editorApiAssemblyFile))
-                return true; // The assemblies are in the output folder; assume the solution is already built
-
-            var apiBuildInfo = new MonoBuildInfo(apiSlnFile, config);
-
-            // TODO Replace this global NoWarn with '#pragma warning' directives on generated files,
-            // once we start to actively document manually maintained C# classes
-            apiBuildInfo.CustomProperties.Add("NoWarn=1591"); // Ignore missing documentation warnings
-
-            if (Build(apiBuildInfo))
-                return true;
-
-            ShowBuildErrorDialog($"Failed to build {ApiAssemblyNames.SolutionName} solution.");
-            return false;
-        }
-
         public static bool BuildProjectBlocking(string config, IEnumerable<string> godotDefines)
         {
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
@@ -201,13 +165,13 @@ namespace GodotTools
             Internal.UpdateApiAssembliesFromPrebuilt();
 
             var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
-            var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool");
+            var buildTool = (BuildTool) editorSettings.GetSetting("mono/builds/build_tool");
 
             using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1))
             {
                 pr.Step("Building project solution", 0);
 
-                var buildInfo = new MonoBuildInfo(GodotSharpDirs.ProjectSlnPath, config);
+                var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, config);
 
                 // Add Godot defines
                 string constants = buildTool == BuildTool.MsBuildVs ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\"";
@@ -240,11 +204,28 @@ namespace GodotTools
             string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
             string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
 
-            CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
+            CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
 
             if (File.Exists(editorScriptsMetadataPath))
                 File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
 
+            var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest;
+
+            if (currentPlayRequest != null)
+            {
+                if (currentPlayRequest.Value.HasDebugger)
+                {
+                    // Set the environment variable that will tell the player to connect to the IDE debugger
+                    // TODO: We should probably add a better way to do this
+                    Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT",
+                        "--debugger-agent=transport=dt_socket" +
+                        $",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" +
+                        ",server=n");
+                }
+
+                return true; // Requested play from an external editor/IDE which already built the project
+            }
+
             var godotDefines = new[]
             {
                 Godot.OS.GetName(),

+ 10 - 10
modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs → modules/mono/editor/GodotTools/GodotTools/BuildTab.cs

@@ -7,7 +7,7 @@ using Path = System.IO.Path;
 
 namespace GodotTools
 {
-    public class MonoBuildTab : VBoxContainer
+    public class BuildTab : VBoxContainer
     {
         public enum BuildResults
         {
@@ -55,7 +55,7 @@ namespace GodotTools
             }
         }
 
-        public MonoBuildInfo BuildInfo { get; private set; }
+        public BuildInfo BuildInfo { get; private set; }
 
         private void _LoadIssuesFromFile(string csvFile)
         {
@@ -199,7 +199,7 @@ namespace GodotTools
             ErrorCount = 0;
             UpdateIssuesList();
 
-            GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this);
+            GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
         }
 
         public void OnBuildExit(BuildResults result)
@@ -207,10 +207,10 @@ namespace GodotTools
             BuildExited = true;
             BuildResult = result;
 
-            _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, GodotSharpBuilds.MsBuildIssuesFileName));
+            _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
             UpdateIssuesList();
 
-            GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this);
+            GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
         }
 
         public void OnBuildExecFailed(string cause)
@@ -227,7 +227,7 @@ namespace GodotTools
 
             UpdateIssuesList();
 
-            GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this);
+            GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
         }
 
         public void RestartBuild()
@@ -235,7 +235,7 @@ namespace GodotTools
             if (!BuildExited)
                 throw new InvalidOperationException("Build already started");
 
-            GodotSharpBuilds.RestartBuild(this);
+            BuildManager.RestartBuild(this);
         }
 
         public void StopBuild()
@@ -243,7 +243,7 @@ namespace GodotTools
             if (!BuildExited)
                 throw new InvalidOperationException("Build is not in progress");
 
-            GodotSharpBuilds.StopBuild(this);
+            BuildManager.StopBuild(this);
         }
 
         public override void _Ready()
@@ -255,11 +255,11 @@ namespace GodotTools
             AddChild(issuesList);
         }
 
-        private MonoBuildTab()
+        private BuildTab()
         {
         }
 
-        public MonoBuildTab(MonoBuildInfo buildInfo)
+        public BuildTab(BuildInfo buildInfo)
         {
             BuildInfo = buildInfo;
         }

+ 1 - 1
modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs → modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs

@@ -9,7 +9,7 @@ using Directory = GodotTools.Utils.Directory;
 
 namespace GodotTools
 {
-    public static class CSharpProject
+    public static class CsProjOperations
     {
         public static string GenerateGameProject(string dir, string name)
         {

+ 11 - 0
modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs

@@ -0,0 +1,11 @@
+namespace GodotTools
+{
+    public enum ExternalEditorId
+    {
+        None,
+        VisualStudio, // TODO (Windows-only)
+        VisualStudioForMac, // Mac-only
+        MonoDevelop,
+        VsCode
+    }
+}

+ 48 - 73
modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

@@ -2,16 +2,18 @@ using Godot;
 using GodotTools.Utils;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using GodotTools.Ides;
 using GodotTools.Internals;
 using GodotTools.ProjectEditor;
 using static GodotTools.Internals.Globals;
 using File = GodotTools.Utils.File;
-using Path = System.IO.Path;
 using OS = GodotTools.Utils.OS;
 
 namespace GodotTools
 {
+    [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
     public class GodotSharpEditor : EditorPlugin, ISerializationListener
     {
         private EditorSettings editorSettings;
@@ -24,12 +26,11 @@ namespace GodotTools
 
         private ToolButton bottomPanelBtn;
 
-        private MonoDevelopInstance monoDevelopInstance;
-        private MonoDevelopInstance visualStudioForMacInstance;
+        public GodotIdeManager GodotIdeManager { get; private set; }
 
         private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization
 
-        public MonoBottomPanel MonoBottomPanel { get; private set; }
+        public BottomPanel BottomPanel { get; private set; }
 
         private bool CreateProjectSolution()
         {
@@ -44,7 +45,7 @@ namespace GodotTools
                 if (name.Empty())
                     name = "UnnamedProject";
 
-                string guid = CSharpProject.GenerateGameProject(path, name);
+                string guid = CsProjOperations.GenerateGameProject(path, name);
 
                 if (guid.Length > 0)
                 {
@@ -133,7 +134,7 @@ namespace GodotTools
                     return; // Failed to create solution
             }
 
-            Instance.MonoBottomPanel.BuildProjectPressed();
+            Instance.BottomPanel.BuildProjectPressed();
         }
 
         public override void _Notification(int what)
@@ -153,21 +154,12 @@ namespace GodotTools
             }
         }
 
-        public enum MenuOptions
+        private enum MenuOptions
         {
             CreateSln,
             AboutCSharp,
         }
 
-        public enum ExternalEditor
-        {
-            None,
-            VisualStudio, // TODO (Windows-only)
-            VisualStudioForMac, // Mac-only
-            MonoDevelop,
-            VsCode
-        }
-
         public void ShowErrorDialog(string message, string title = "Error")
         {
             errorDialog.WindowTitle = title;
@@ -184,11 +176,30 @@ namespace GodotTools
 
         public Error OpenInExternalEditor(Script script, int line, int col)
         {
-            var editor = (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor");
+            var editor = (ExternalEditorId) editorSettings.GetSetting("mono/editor/external_editor");
 
             switch (editor)
             {
-                case ExternalEditor.VsCode:
+                case ExternalEditorId.None:
+                    // Tells the caller to fallback to the global external editor settings or the built-in editor
+                    return Error.Unavailable;
+                case ExternalEditorId.VisualStudio:
+                    throw new NotSupportedException();
+                case ExternalEditorId.VisualStudioForMac:
+                    goto case ExternalEditorId.MonoDevelop;
+                case ExternalEditorId.MonoDevelop:
+                {
+                    string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
+
+                    if (line >= 0)
+                        GodotIdeManager.SendOpenFile(scriptPath, line + 1, col);
+                    else
+                        GodotIdeManager.SendOpenFile(scriptPath);
+
+                    break;
+                }
+
+                case ExternalEditorId.VsCode:
                 {
                     if (_vsCodePath.Empty() || !File.Exists(_vsCodePath))
                     {
@@ -273,47 +284,6 @@ namespace GodotTools
                     break;
                 }
 
-                case ExternalEditor.VisualStudioForMac:
-                    goto case ExternalEditor.MonoDevelop;
-                case ExternalEditor.MonoDevelop:
-                {
-                    MonoDevelopInstance GetMonoDevelopInstance(string solutionPath)
-                    {
-                        if (OS.IsOSX() && editor == ExternalEditor.VisualStudioForMac)
-                        {
-                            if (visualStudioForMacInstance == null)
-                                visualStudioForMacInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.VisualStudioForMac);
-
-                            return visualStudioForMacInstance;
-                        }
-
-                        if (monoDevelopInstance == null)
-                            monoDevelopInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.MonoDevelop);
-
-                        return monoDevelopInstance;
-                    }
-
-                    string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
-
-                    if (line >= 0)
-                        scriptPath += $";{line + 1};{col}";
-
-                    try
-                    {
-                        GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath).Execute(scriptPath);
-                    }
-                    catch (FileNotFoundException)
-                    {
-                        string editorName = editor == ExternalEditor.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
-                        GD.PushError($"Cannot find code editor: {editorName}");
-                        return Error.FileNotFound;
-                    }
-
-                    break;
-                }
-
-                case ExternalEditor.None:
-                    return Error.Unavailable;
                 default:
                     throw new ArgumentOutOfRangeException();
             }
@@ -323,12 +293,12 @@ namespace GodotTools
 
         public bool OverridesExternalEditor()
         {
-            return (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditor.None;
+            return (ExternalEditorId) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditorId.None;
         }
 
         public override bool Build()
         {
-            return GodotSharpBuilds.EditorBuildCallback();
+            return BuildManager.EditorBuildCallback();
         }
 
         public override void EnablePlugin()
@@ -347,9 +317,9 @@ namespace GodotTools
             errorDialog = new AcceptDialog();
             editorBaseControl.AddChild(errorDialog);
 
-            MonoBottomPanel = new MonoBottomPanel();
+            BottomPanel = new BottomPanel();
 
-            bottomPanelBtn = AddControlToBottomPanel(MonoBottomPanel, "Mono".TTR());
+            bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR());
 
             AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"});
 
@@ -407,7 +377,7 @@ namespace GodotTools
             if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
             {
                 // Make sure the existing project has Api assembly references configured correctly
-                CSharpProject.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath);
+                CsProjOperations.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath);
             }
             else
             {
@@ -427,25 +397,25 @@ namespace GodotTools
             AddControlToContainer(CustomControlContainer.Toolbar, buildButton);
 
             // External editor settings
-            EditorDef("mono/editor/external_editor", ExternalEditor.None);
+            EditorDef("mono/editor/external_editor", ExternalEditorId.None);
 
             string settingsHintStr = "Disabled";
 
             if (OS.IsWindows())
             {
-                settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" +
-                                   $",Visual Studio Code:{(int) ExternalEditor.VsCode}";
+                settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" +
+                                   $",Visual Studio Code:{(int) ExternalEditorId.VsCode}";
             }
             else if (OS.IsOSX())
             {
-                settingsHintStr += $",Visual Studio:{(int) ExternalEditor.VisualStudioForMac}" +
-                                   $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" +
-                                   $",Visual Studio Code:{(int) ExternalEditor.VsCode}";
+                settingsHintStr += $",Visual Studio:{(int) ExternalEditorId.VisualStudioForMac}" +
+                                   $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" +
+                                   $",Visual Studio Code:{(int) ExternalEditorId.VsCode}";
             }
             else if (OS.IsUnix())
             {
-                settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" +
-                                   $",Visual Studio Code:{(int) ExternalEditor.VsCode}";
+                settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" +
+                                   $",Visual Studio Code:{(int) ExternalEditorId.VsCode}";
             }
 
             editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
@@ -461,7 +431,10 @@ namespace GodotTools
             AddExportPlugin(exportPlugin);
             exportPluginWeak = WeakRef(exportPlugin);
 
-            GodotSharpBuilds.Initialize();
+            BuildManager.Initialize();
+
+            GodotIdeManager = new GodotIdeManager();
+            AddChild(GodotIdeManager);
         }
 
         protected override void Dispose(bool disposing)
@@ -478,6 +451,8 @@ namespace GodotTools
 
                 exportPluginWeak.Dispose();
             }
+
+            GodotIdeManager?.Dispose();
         }
 
         public void OnBeforeSerialize()

+ 2 - 2
modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs

@@ -65,14 +65,14 @@ namespace GodotTools
                 string buildConfig = isDebug ? "Debug" : "Release";
 
                 string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
-                CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
+                CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
 
                 AddFile(scriptsMetadataPath, scriptsMetadataPath);
 
                 // Turn export features into defines
                 var godotDefines = features;
 
-                if (!GodotSharpBuilds.BuildProjectBlocking(buildConfig, godotDefines))
+                if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines))
                 {
                     GD.PushError("Failed to build project");
                     return;

+ 15 - 9
modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj

@@ -39,26 +39,31 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Build\MsBuildFinder.cs" />
+    <Compile Include="ExternalEditorId.cs" />
+    <Compile Include="Ides\GodotIdeManager.cs" />
+    <Compile Include="Ides\GodotIdeServer.cs" />
+    <Compile Include="Ides\MonoDevelop\EditorId.cs" />
+    <Compile Include="Ides\MonoDevelop\Instance.cs" />
     <Compile Include="Internals\BindingsGenerator.cs" />
     <Compile Include="Internals\EditorProgress.cs" />
     <Compile Include="Internals\GodotSharpDirs.cs" />
     <Compile Include="Internals\Internal.cs" />
     <Compile Include="Internals\ScriptClassParser.cs" />
     <Compile Include="Internals\Globals.cs" />
-    <Compile Include="MonoDevelopInstance.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Build\BuildSystem.cs" />
     <Compile Include="Utils\Directory.cs" />
     <Compile Include="Utils\File.cs" />
+    <Compile Include="Utils\NotifyAwaiter.cs" />
     <Compile Include="Utils\OS.cs" />
     <Compile Include="GodotSharpEditor.cs" />
-    <Compile Include="GodotSharpBuilds.cs" />
+    <Compile Include="BuildManager.cs" />
     <Compile Include="HotReloadAssemblyWatcher.cs" />
-    <Compile Include="MonoBuildInfo.cs" />
-    <Compile Include="MonoBuildTab.cs" />
-    <Compile Include="MonoBottomPanel.cs" />
+    <Compile Include="BuildInfo.cs" />
+    <Compile Include="BuildTab.cs" />
+    <Compile Include="BottomPanel.cs" />
     <Compile Include="GodotSharpExport.cs" />
-    <Compile Include="CSharpProject.cs" />
+    <Compile Include="CsProjOperations.cs" />
     <Compile Include="Utils\CollectionExtensions.cs" />
   </ItemGroup>
   <ItemGroup>
@@ -66,6 +71,10 @@
       <Project>{6ce9a984-37b1-4f8a-8fe9-609f05f071b3}</Project>
       <Name>GodotTools.BuildLogger</Name>
     </ProjectReference>
+    <ProjectReference Include="..\GodotTools.IdeConnection\GodotTools.IdeConnection.csproj">
+      <Project>{92600954-25f0-4291-8e11-1fee9fc4be20}</Project>
+      <Name>GodotTools.IdeConnection</Name>
+    </ProjectReference>
     <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj">
       <Project>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</Project>
       <Name>GodotTools.ProjectEditor</Name>
@@ -75,8 +84,5 @@
       <Name>GodotTools.Core</Name>
     </ProjectReference>
   </ItemGroup>
-  <ItemGroup>
-    <Folder Include="Editor" />
-  </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
 </Project>

+ 166 - 0
modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs

@@ -0,0 +1,166 @@
+using System;
+using System.IO;
+using Godot;
+using GodotTools.IdeConnection;
+using GodotTools.Internals;
+
+namespace GodotTools.Ides
+{
+    public class GodotIdeManager : Node, ISerializationListener
+    {
+        public GodotIdeServer GodotIdeServer { get; private set; }
+
+        private MonoDevelop.Instance monoDevelInstance;
+        private MonoDevelop.Instance vsForMacInstance;
+
+        private GodotIdeServer GetRunningServer()
+        {
+            if (GodotIdeServer != null && !GodotIdeServer.IsDisposed)
+                return GodotIdeServer;
+            StartServer();
+            return GodotIdeServer;
+        }
+
+        public override void _Ready()
+        {
+            StartServer();
+        }
+
+        public void OnBeforeSerialize()
+        {
+            GodotIdeServer?.Dispose();
+        }
+
+        public void OnAfterDeserialize()
+        {
+            StartServer();
+        }
+
+        private ILogger logger;
+
+        protected ILogger Logger
+        {
+            get => logger ?? (logger = new ConsoleLogger());
+            set => logger = value;
+        }
+
+        private void StartServer()
+        {
+            GodotIdeServer?.Dispose();
+            GodotIdeServer = new GodotIdeServer(LaunchIde,
+                OS.GetExecutablePath(),
+                ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir));
+
+            GodotIdeServer.Logger = Logger;
+
+            GodotIdeServer.StartServer();
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            base.Dispose(disposing);
+
+            GodotIdeServer?.Dispose();
+        }
+
+        private void LaunchIde()
+        {
+            var editor = (ExternalEditorId) GodotSharpEditor.Instance.GetEditorInterface()
+                .GetEditorSettings().GetSetting("mono/editor/external_editor");
+
+            switch (editor)
+            {
+                case ExternalEditorId.None:
+                case ExternalEditorId.VisualStudio:
+                case ExternalEditorId.VsCode:
+                    throw new NotSupportedException();
+                case ExternalEditorId.VisualStudioForMac:
+                    goto case ExternalEditorId.MonoDevelop;
+                case ExternalEditorId.MonoDevelop:
+                {
+                    MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
+                    {
+                        if (Utils.OS.IsOSX() && editor == ExternalEditorId.VisualStudioForMac)
+                        {
+                            vsForMacInstance = vsForMacInstance ??
+                                               new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
+                            return vsForMacInstance;
+                        }
+
+                        monoDevelInstance = monoDevelInstance ??
+                                            new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
+                        return monoDevelInstance;
+                    }
+
+                    try
+                    {
+                        var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
+
+                        if (!instance.IsRunning)
+                            instance.Execute();
+                    }
+                    catch (FileNotFoundException)
+                    {
+                        string editorName = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
+                        GD.PushError($"Cannot find code editor: {editorName}");
+                    }
+
+                    break;
+                }
+
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+        }
+
+        private void WriteMessage(string id, params string[] arguments)
+        {
+            GetRunningServer().WriteMessage(new Message(id, arguments));
+        }
+
+        public void SendOpenFile(string file)
+        {
+            WriteMessage("OpenFile", file);
+        }
+
+        public void SendOpenFile(string file, int line)
+        {
+            WriteMessage("OpenFile", file, line.ToString());
+        }
+
+        public void SendOpenFile(string file, int line, int column)
+        {
+            WriteMessage("OpenFile", file, line.ToString(), column.ToString());
+        }
+
+        private class GodotLogger : ILogger
+        {
+            public void LogDebug(string message)
+            {
+                if (OS.IsStdoutVerbose())
+                    Console.WriteLine(message);
+            }
+
+            public void LogInfo(string message)
+            {
+                if (OS.IsStdoutVerbose())
+                    Console.WriteLine(message);
+            }
+
+            public void LogWarning(string message)
+            {
+                GD.PushWarning(message);
+            }
+
+            public void LogError(string message)
+            {
+                GD.PushError(message);
+            }
+
+            public void LogError(string message, Exception e)
+            {
+                GD.PushError(message + "\n" + e);
+            }
+        }
+    }
+}

+ 208 - 0
modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs

@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeConnection;
+using GodotTools.Internals;
+using GodotTools.Utils;
+using File = System.IO.File;
+using Thread = System.Threading.Thread;
+
+namespace GodotTools.Ides
+{
+    public class GodotIdeServer : GodotIdeBase
+    {
+        private readonly TcpListener listener;
+        private readonly FileStream metaFile;
+        private readonly Action launchIdeAction;
+        private readonly NotifyAwaiter<bool> clientConnectedAwaiter = new NotifyAwaiter<bool>();
+
+        private async Task<bool> AwaitClientConnected()
+        {
+            return await clientConnectedAwaiter.Reset();
+        }
+
+        public GodotIdeServer(Action launchIdeAction, string editorExecutablePath, string projectMetadataDir)
+            : base(projectMetadataDir)
+        {
+            messageHandlers = InitializeMessageHandlers();
+
+            this.launchIdeAction = launchIdeAction;
+
+            // The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing...
+            const FileShare metaFileShare = FileShare.ReadWrite;
+
+            metaFile = File.Open(MetaFilePath, FileMode.Create, FileAccess.Write, metaFileShare);
+
+            listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0));
+            listener.Start();
+
+            int port = ((IPEndPoint) listener.Server.LocalEndPoint).Port;
+            using (var metaFileWriter = new StreamWriter(metaFile, Encoding.UTF8))
+            {
+                metaFileWriter.WriteLine(port);
+                metaFileWriter.WriteLine(editorExecutablePath);
+            }
+
+            StartServer();
+        }
+
+        public void StartServer()
+        {
+            var serverThread = new Thread(RunServerThread) {Name = "Godot Ide Connection Server"};
+            serverThread.Start();
+        }
+
+        private void RunServerThread()
+        {
+            SynchronizationContext.SetSynchronizationContext(Godot.Dispatcher.SynchronizationContext);
+
+            try
+            {
+                while (!IsDisposed)
+                {
+                    TcpClient tcpClient = listener.AcceptTcpClient();
+
+                    Logger.LogInfo("Connection open with Ide Client");
+
+                    lock (ConnectionLock)
+                    {
+                        Connection = new GodotIdeConnectionServer(tcpClient, HandleMessage);
+                        Connection.Logger = Logger;
+                    }
+
+                    Connected += () => clientConnectedAwaiter.SetResult(true);
+
+                    Connection.Start();
+                }
+            }
+            catch (Exception e)
+            {
+                if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
+                    throw;
+            }
+        }
+
+        public async void WriteMessage(Message message)
+        {
+            async Task LaunchIde()
+            {
+                if (IsConnected)
+                    return;
+
+                launchIdeAction();
+                await Task.WhenAny(Task.Delay(10000), AwaitClientConnected());
+            }
+
+            await LaunchIde();
+
+            if (!IsConnected)
+            {
+                Logger.LogError("Cannot write message: Godot Ide Server not connected");
+                return;
+            }
+
+            Connection.WriteMessage(message);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            base.Dispose(disposing);
+
+            if (disposing)
+            {
+                listener?.Stop();
+
+                metaFile?.Dispose();
+
+                File.Delete(MetaFilePath);
+            }
+        }
+
+        protected virtual bool HandleMessage(Message message)
+        {
+            if (messageHandlers.TryGetValue(message.Id, out var action))
+            {
+                action(message.Arguments);
+                return true;
+            }
+
+            return false;
+        }
+
+        private readonly Dictionary<string, Action<string[]>> messageHandlers;
+
+        private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
+        {
+            return new Dictionary<string, Action<string[]>>
+            {
+                ["Play"] = args =>
+                {
+                    switch (args.Length)
+                    {
+                        case 0:
+                            Play();
+                            return;
+                        case 2:
+                            Play(debuggerHost: args[0], debuggerPort: int.Parse(args[1]));
+                            return;
+                        default:
+                            throw new ArgumentException();
+                    }
+                },
+                ["ReloadScripts"] = args => ReloadScripts()
+            };
+        }
+
+        private void DispatchToMainThread(Action action)
+        {
+            var d = new SendOrPostCallback(state => action());
+            Godot.Dispatcher.SynchronizationContext.Post(d, null);
+        }
+
+        private void Play()
+        {
+            DispatchToMainThread(() =>
+            {
+                CurrentPlayRequest = new PlayRequest();
+                Internal.EditorRunPlay();
+                CurrentPlayRequest = null;
+            });
+        }
+
+        private void Play(string debuggerHost, int debuggerPort)
+        {
+            DispatchToMainThread(() =>
+            {
+                CurrentPlayRequest = new PlayRequest(debuggerHost, debuggerPort);
+                Internal.EditorRunPlay();
+                CurrentPlayRequest = null;
+            });
+        }
+
+        private void ReloadScripts()
+        {
+            DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
+        }
+
+        public PlayRequest? CurrentPlayRequest { get; private set; }
+
+        public struct PlayRequest
+        {
+            public bool HasDebugger { get; }
+            public string DebuggerHost { get; }
+            public int DebuggerPort { get; }
+
+            public PlayRequest(string debuggerHost, int debuggerPort)
+            {
+                HasDebugger = true;
+                DebuggerHost = debuggerHost;
+                DebuggerPort = debuggerPort;
+            }
+        }
+    }
+}

+ 8 - 0
modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs

@@ -0,0 +1,8 @@
+namespace GodotTools.Ides.MonoDevelop
+{
+    public enum EditorId
+    {
+        MonoDevelop = 0,
+        VisualStudioForMac = 1
+    }
+}

+ 14 - 29
modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs → modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs

@@ -1,4 +1,3 @@
-using GodotTools.Core;
 using System;
 using System.IO;
 using System.Collections.Generic;
@@ -6,22 +5,18 @@ using System.Diagnostics;
 using GodotTools.Internals;
 using GodotTools.Utils;
 
-namespace GodotTools
+namespace GodotTools.Ides.MonoDevelop
 {
-    public class MonoDevelopInstance
+    public class Instance
     {
-        public enum EditorId
-        {
-            MonoDevelop = 0,
-            VisualStudioForMac = 1
-        }
-
         private readonly string solutionFile;
         private readonly EditorId editorId;
 
         private Process process;
 
-        public void Execute(params string[] files)
+        public bool IsRunning => process != null && !process.HasExited;
+
+        public void Execute()
         {
             bool newWindow = process == null || process.HasExited;
 
@@ -29,7 +24,7 @@ namespace GodotTools
 
             string command;
 
-            if (Utils.OS.IsOSX())
+            if (OS.IsOSX())
             {
                 string bundleId = BundleIds[editorId];
 
@@ -61,16 +56,6 @@ namespace GodotTools
             if (newWindow)
                 args.Add("\"" + Path.GetFullPath(solutionFile) + "\"");
 
-            foreach (var file in files)
-            {
-                int semicolonIndex = file.IndexOf(';');
-
-                string filePath = semicolonIndex < 0 ? file : file.Substring(0, semicolonIndex);
-                string cursor = semicolonIndex < 0 ? string.Empty : file.Substring(semicolonIndex);
-
-                args.Add("\"" + Path.GetFullPath(filePath.NormalizePath()) + cursor + "\"");
-            }
-
             if (command == null)
                 throw new FileNotFoundException();
 
@@ -80,7 +65,7 @@ namespace GodotTools
                 {
                     FileName = command,
                     Arguments = string.Join(" ", args),
-                    UseShellExecute = false
+                    UseShellExecute = true
                 });
             }
             else
@@ -89,14 +74,14 @@ namespace GodotTools
                 {
                     FileName = command,
                     Arguments = string.Join(" ", args),
-                    UseShellExecute = false
+                    UseShellExecute = true
                 })?.Dispose();
             }
         }
 
-        public MonoDevelopInstance(string solutionFile, EditorId editorId)
+        public Instance(string solutionFile, EditorId editorId)
         {
-            if (editorId == EditorId.VisualStudioForMac && !Utils.OS.IsOSX())
+            if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX())
                 throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform");
 
             this.solutionFile = solutionFile;
@@ -106,9 +91,9 @@ namespace GodotTools
         private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
         private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
 
-        static MonoDevelopInstance()
+        static Instance()
         {
-            if (Utils.OS.IsOSX())
+            if (OS.IsOSX())
             {
                 ExecutableNames = new Dictionary<EditorId, string>
                 {
@@ -122,7 +107,7 @@ namespace GodotTools
                     {EditorId.VisualStudioForMac, "com.microsoft.visual-studio"}
                 };
             }
-            else if (Utils.OS.IsWindows())
+            else if (OS.IsWindows())
             {
                 ExecutableNames = new Dictionary<EditorId, string>
                 {
@@ -133,7 +118,7 @@ namespace GodotTools
                     {EditorId.MonoDevelop, "MonoDevelop.exe"}
                 };
             }
-            else if (Utils.OS.IsUnix())
+            else if (OS.IsUnix())
             {
                 ExecutableNames = new Dictionary<EditorId, string>
                 {

+ 15 - 0
modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs

@@ -46,6 +46,12 @@ namespace GodotTools.Internals
 
         public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot();
 
+        public static void EditorRunPlay() => internal_EditorRunPlay();
+
+        public static void EditorRunStop() => internal_EditorRunStop();
+
+        public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
+
         // Internal Calls
 
         [MethodImpl(MethodImplOptions.InternalCall)]
@@ -95,5 +101,14 @@ namespace GodotTools.Internals
 
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern string internal_MonoWindowsInstallRoot();
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void internal_EditorRunPlay();
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void internal_EditorRunStop();
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern void internal_ScriptEditorDebugger_ReloadScripts();
     }
 }

+ 8 - 0
modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
 
 namespace GodotTools.Utils
 {
@@ -17,5 +18,12 @@ namespace GodotTools.Utils
 
             return orElse;
         }
+
+        public static IEnumerable<string> EnumerateLines(this TextReader textReader)
+        {
+            string line;
+            while ((line = textReader.ReadLine()) != null)
+                yield return line;
+        }
     }
 }

+ 64 - 0
modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace GodotTools.Utils
+{
+    public sealed class NotifyAwaiter<T> : INotifyCompletion
+    {
+        private Action continuation;
+        private Exception exception;
+        private T result;
+
+        public bool IsCompleted { get; private set; }
+
+        public T GetResult()
+        {
+            if (exception != null)
+                throw exception;
+            return result;
+        }
+
+        public void OnCompleted(Action continuation)
+        {
+            if (this.continuation != null)
+                throw new InvalidOperationException("This awaiter has already been listened");
+            this.continuation = continuation;
+        }
+
+        public void SetResult(T result)
+        {
+            if (IsCompleted)
+                throw new InvalidOperationException("This awaiter is already completed");
+
+            IsCompleted = true;
+            this.result = result;
+
+            continuation?.Invoke();
+        }
+
+        public void SetException(Exception exception)
+        {
+            if (IsCompleted)
+                throw new InvalidOperationException("This awaiter is already completed");
+
+            IsCompleted = true;
+            this.exception = exception;
+
+            continuation?.Invoke();
+        }
+
+        public NotifyAwaiter<T> Reset()
+        {
+            continuation = null;
+            exception = null;
+            result = default;
+            IsCompleted = false;
+            return this;
+        }
+
+        public NotifyAwaiter<T> GetAwaiter()
+        {
+            return this;
+        }
+    }
+}

+ 18 - 1
modules/mono/editor/editor_internal_calls.cpp

@@ -350,6 +350,21 @@ MonoString *godot_icall_Internal_MonoWindowsInstallRoot() {
 #endif
 }
 
+void godot_icall_Internal_EditorRunPlay() {
+	EditorNode::get_singleton()->run_play();
+}
+
+void godot_icall_Internal_EditorRunStop() {
+	EditorNode::get_singleton()->run_stop();
+}
+
+void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() {
+	ScriptEditorDebugger *sed = ScriptEditor::get_singleton()->get_debugger();
+	if (sed) {
+		sed->reload_scripts();
+	}
+}
+
 MonoString *godot_icall_Utils_OS_GetPlatformName() {
 	String os_name = OS::get_singleton()->get_name();
 	return GDMonoMarshal::mono_string_from_godot(os_name);
@@ -414,7 +429,9 @@ void register_editor_internal_calls() {
 	mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorEdit", (void *)godot_icall_Internal_ScriptEditorEdit);
 	mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorNodeShowScriptScreen", (void *)godot_icall_Internal_EditorNodeShowScriptScreen);
 	mono_add_internal_call("GodotTools.Internals.Internal::internal_GetScriptsMetadataOrNothing", (void *)godot_icall_Internal_GetScriptsMetadataOrNothing);
-	mono_add_internal_call("GodotTools.Internals.Internal::internal_MonoWindowsInstallRoot", (void *)godot_icall_Internal_MonoWindowsInstallRoot);
+	mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", (void *)godot_icall_Internal_EditorRunPlay);
+	mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", (void *)godot_icall_Internal_EditorRunStop);
+	mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts);
 
 	// Globals
 	mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale);

+ 13 - 0
modules/mono/glue/Managed/Files/Dispatcher.cs

@@ -0,0 +1,13 @@
+using System.Runtime.CompilerServices;
+
+namespace Godot
+{
+    public static class Dispatcher
+    {
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern GodotTaskScheduler godot_icall_DefaultGodotTaskScheduler();
+
+        public static GodotSynchronizationContext SynchronizationContext =>
+            godot_icall_DefaultGodotTaskScheduler().Context;
+    }
+}

+ 3 - 4
modules/mono/glue/Managed/Files/GodotSynchronizationContext.cs

@@ -6,17 +6,16 @@ namespace Godot
 {
     public class GodotSynchronizationContext : SynchronizationContext
     {
-        private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> queue = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
+        private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _queue = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
 
         public override void Post(SendOrPostCallback d, object state)
         {
-            queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
+            _queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
         }
 
         public void ExecutePendingContinuations()
         {
-            KeyValuePair<SendOrPostCallback, object> workItem;
-            while (queue.TryTake(out workItem))
+            while (_queue.TryTake(out var workItem))
             {
                 workItem.Key(workItem.Value);
             }

+ 3 - 6
modules/mono/glue/Managed/Files/GodotTaskScheduler.cs

@@ -8,7 +8,7 @@ namespace Godot
 {
     public class GodotTaskScheduler : TaskScheduler
     {
-        private GodotSynchronizationContext Context { get; set; }
+        internal GodotSynchronizationContext Context { get; }
         private readonly LinkedList<Task> _tasks = new LinkedList<Task>();
 
         public GodotTaskScheduler()
@@ -28,14 +28,10 @@ namespace Godot
         protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
         {
             if (SynchronizationContext.Current != Context)
-            {
                 return false;
-            }
 
             if (taskWasPreviouslyQueued)
-            {
                 TryDequeue(task);
-            }
 
             return TryExecuteTask(task);
         }
@@ -52,7 +48,8 @@ namespace Godot
         {
             lock (_tasks)
             {
-                return _tasks.ToArray();
+                foreach (Task task in _tasks)
+                    yield return task;
             }
         }
 

+ 2 - 2
modules/mono/glue/Managed/Managed.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -37,4 +37,4 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
-</Project>
+</Project>

+ 7 - 0
modules/mono/glue/gd_glue.cpp

@@ -211,6 +211,10 @@ MonoString *godot_icall_GD_var2str(MonoObject *p_var) {
 	return GDMonoMarshal::mono_string_from_godot(vars);
 }
 
+MonoObject *godot_icall_DefaultGodotTaskScheduler() {
+	return GDMonoUtils::mono_cache.task_scheduler_handle->get_target();
+}
+
 void godot_register_gd_icalls() {
 	mono_add_internal_call("Godot.GD::godot_icall_GD_bytes2var", (void *)godot_icall_GD_bytes2var);
 	mono_add_internal_call("Godot.GD::godot_icall_GD_convert", (void *)godot_icall_GD_convert);
@@ -234,6 +238,9 @@ void godot_register_gd_icalls() {
 	mono_add_internal_call("Godot.GD::godot_icall_GD_type_exists", (void *)godot_icall_GD_type_exists);
 	mono_add_internal_call("Godot.GD::godot_icall_GD_var2bytes", (void *)godot_icall_GD_var2bytes);
 	mono_add_internal_call("Godot.GD::godot_icall_GD_var2str", (void *)godot_icall_GD_var2str);
+
+	// Dispatcher
+	mono_add_internal_call("Godot.Dispatcher::godot_icall_DefaultGodotTaskScheduler", (void *)godot_icall_DefaultGodotTaskScheduler);
 }
 
 #endif // MONO_GLUE_ENABLED

+ 2 - 0
modules/mono/glue/gd_glue.h

@@ -75,6 +75,8 @@ MonoArray *godot_icall_GD_var2bytes(MonoObject *p_var, MonoBoolean p_full_object
 
 MonoString *godot_icall_GD_var2str(MonoObject *p_var);
 
+MonoObject *godot_icall_DefaultGodotTaskScheduler();
+
 // Register internal calls
 
 void godot_register_gd_icalls();

+ 22 - 12
modules/mono/mono_gd/gd_mono.cpp

@@ -561,14 +561,14 @@ bool GDMono::_load_corlib_assembly() {
 }
 
 #ifdef TOOLS_ENABLED
-bool GDMono::copy_prebuilt_api_assembly(APIAssembly::Type p_api_type) {
+bool GDMono::copy_prebuilt_api_assembly(APIAssembly::Type p_api_type, const String &p_config) {
 
 	bool &api_assembly_out_of_sync = (p_api_type == APIAssembly::API_CORE) ?
 											 GDMono::get_singleton()->core_api_assembly_out_of_sync :
 											 GDMono::get_singleton()->editor_api_assembly_out_of_sync;
 
-	String src_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file("Debug");
-	String dst_dir = GodotSharpDirs::get_res_assemblies_dir();
+	String src_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file(p_config);
+	String dst_dir = GodotSharpDirs::get_res_assemblies_base_dir().plus_file(p_config);
 
 	String assembly_name = p_api_type == APIAssembly::API_CORE ? CORE_API_ASSEMBLY_NAME : EDITOR_API_ASSEMBLY_NAME;
 
@@ -631,18 +631,28 @@ String GDMono::update_api_assemblies_from_prebuilt() {
 	if (!api_assembly_out_of_sync && FileAccess::exists(core_assembly_path) && FileAccess::exists(editor_assembly_path))
 		return String(); // No update needed
 
-	print_verbose("Updating API assemblies");
+	const int CONFIGS_LEN = 2;
+	String configs[CONFIGS_LEN] = { String("Debug"), String("Release") };
 
-	String prebuilt_api_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file("Debug");
-	String prebuilt_core_dll_path = prebuilt_api_dir.plus_file(CORE_API_ASSEMBLY_NAME ".dll");
-	String prebuilt_editor_dll_path = prebuilt_api_dir.plus_file(EDITOR_API_ASSEMBLY_NAME ".dll");
+	for (int i = 0; i < CONFIGS_LEN; i++) {
+		String config = configs[i];
 
-	if (!FileAccess::exists(prebuilt_core_dll_path) || !FileAccess::exists(prebuilt_editor_dll_path))
-		return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ false);
+		print_verbose("Updating '" + config + "' API assemblies");
 
-	// Copy the prebuilt Api
-	if (!copy_prebuilt_api_assembly(APIAssembly::API_CORE) || !copy_prebuilt_api_assembly(APIAssembly::API_EDITOR))
-		return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ true);
+		String prebuilt_api_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file(config);
+		String prebuilt_core_dll_path = prebuilt_api_dir.plus_file(CORE_API_ASSEMBLY_NAME ".dll");
+		String prebuilt_editor_dll_path = prebuilt_api_dir.plus_file(EDITOR_API_ASSEMBLY_NAME ".dll");
+
+		if (!FileAccess::exists(prebuilt_core_dll_path) || !FileAccess::exists(prebuilt_editor_dll_path)) {
+			return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ false);
+		}
+
+		// Copy the prebuilt Api
+		if (!copy_prebuilt_api_assembly(APIAssembly::API_CORE, config) ||
+				!copy_prebuilt_api_assembly(APIAssembly::API_EDITOR, config)) {
+			return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ true);
+		}
+	}
 
 	return String(); // Updated successfully
 

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

@@ -165,7 +165,7 @@ public:
 #endif
 
 #ifdef TOOLS_ENABLED
-	bool copy_prebuilt_api_assembly(APIAssembly::Type p_api_type);
+	bool copy_prebuilt_api_assembly(APIAssembly::Type p_api_type, const String &p_config);
 	String update_api_assemblies_from_prebuilt();
 #endif