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.
This commit is contained in:
Ignacio Etcheverry 2019-07-18 04:08:24 +02:00
parent 4b7b1b0d4a
commit 0b94203a79
44 changed files with 1636 additions and 239 deletions

View file

@ -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();
}

View file

@ -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 {

View file

@ -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]

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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)}')";
}
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}
}

View file

@ -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")]

View file

@ -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

View file

@ -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");

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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(),

View file

@ -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;
}

View file

@ -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)
{

View file

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

View file

@ -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()

View file

@ -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;

View file

@ -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>

View file

@ -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);
}
}
}
}

View file

@ -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;
}
}
}
}

View file

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

View file

@ -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>
{

View file

@ -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();
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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