Initial commit

This commit is contained in:
Ignacio Etcheverry
2019-10-31 12:24:41 +01:00
commit 60dd167788
31 changed files with 2196 additions and 0 deletions

356
.gitignore vendored Normal file
View File

@@ -0,0 +1,356 @@
# Rider
.idea/
# Visual Studio Code
.vscode/
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/

23
GodotAddin.sln Normal file
View File

@@ -0,0 +1,23 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotAddin", "GodotAddin\GodotAddin.csproj", "{EE86BFF1-F6B6-42AA-9121-EE2FAC123A12}"
EndProject
Project("{9344BDBB-3E7F-41FC-A0DD-8665D75EE146}") = "GodotTools.IdeConnection", "GodotTools.IdeConnection\GodotTools.IdeConnection.csproj", "{12E66B0C-89B7-4E95-AE75-7E92A4B90EB9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EE86BFF1-F6B6-42AA-9121-EE2FAC123A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE86BFF1-F6B6-42AA-9121-EE2FAC123A12}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE86BFF1-F6B6-42AA-9121-EE2FAC123A12}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE86BFF1-F6B6-42AA-9121-EE2FAC123A12}.Release|Any CPU.Build.0 = Release|Any CPU
{12E66B0C-89B7-4E95-AE75-7E92A4B90EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12E66B0C-89B7-4E95-AE75-7E92A4B90EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12E66B0C-89B7-4E95-AE75-7E92A4B90EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12E66B0C-89B7-4E95-AE75-7E92A4B90EB9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net471</TargetFramework>
</PropertyGroup>
<ItemGroup>
<AddinReference Include="MonoDevelop.Debugger" />
<AddinReference Include="MonoDevelop.Debugger.Soft" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoDevelop.Addins" Version="0.4.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.IdeConnection\GodotTools.IdeConnection.csproj">
</ProjectReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,80 @@
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using Mono.Debugging.Client;
using Mono.Debugging.Soft;
using MonoDevelop.Core.Execution;
using MonoDevelop.Debugger;
using MonoDevelop.Ide;
namespace GodotAddin
{
public class GodotDebuggerEngine : DebuggerEngineBackend
{
public GodotDebuggerEngine()
{
IdeApp.ProjectOperations.EndBuild += OnProjectOperationsEndBuild;
}
private void OnProjectOperationsEndBuild(object sender, MonoDevelop.Projects.BuildEventArgs args)
{
foreach (var session in DebuggingService.GetSessions())
{
if (session is GodotDebuggerSession godotSession)
godotSession.SendReloadScipts();
}
}
public override bool CanDebugCommand(ExecutionCommand cmd)
{
return cmd is GodotExecutionCommand;
}
public override DebuggerStartInfo CreateDebuggerStartInfo(ExecutionCommand cmd)
{
var godotCmd = (GodotExecutionCommand)cmd;
var godotProjectPath = godotCmd.GodotProjectPath;
int attachPort = 23685; // Default if not modified
// Try read the debugger agent port from the 'project.godot' file
if (File.Exists(godotProjectPath))
{
// [mono] "debugger_agent/port"
var regex = new Regex(@"debugger_agent/port=([0-9]+)");
foreach (string line in File.ReadAllLines(godotProjectPath))
{
var match = regex.Match(line);
if (match.Success)
{
attachPort = int.Parse(match.Groups[1].Value);
break;
}
}
}
SoftDebuggerRemoteArgs args;
if (godotCmd.ExecutionType == ExecutionType.Launch)
args = new SoftDebuggerListenArgs("Godot", IPAddress.Loopback, 0);
else
args = new SoftDebuggerConnectArgs("Godot", IPAddress.Loopback, attachPort);
return new GodotDebuggerStartInfo(godotCmd, args)
{
WorkingDirectory = godotCmd.WorkingDirectory
};
}
public override DebuggerSession CreateSession()
{
return new GodotDebuggerSession();
}
public override bool IsDefaultDebugger(ExecutionCommand cmd)
{
return true;
}
}
}

View File

@@ -0,0 +1,199 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Mono.Debugging.Client;
using Mono.Debugging.Soft;
using GodotAddin.Utils;
using System.Collections.Generic;
namespace GodotAddin
{
public class GodotDebuggerSession : SoftDebuggerSession
{
bool attached;
private NetworkStream godotRemoteDebuggerStream;
private GodotExecutionCommand GodotCmd;
public void SendReloadScipts()
{
switch (GodotCmd.ExecutionType)
{
case ExecutionType.Launch:
GodotVariantEncoder.Encode(
new List<GodotVariant> { "reload_scripts" },
godotRemoteDebuggerStream
);
break;
case ExecutionType.PlayInEditor:
case ExecutionType.Attach:
GodotCmd.GodotIdeClient.SendReloadScripts();
break;
default:
throw new NotImplementedException(GodotCmd.ExecutionType.ToString());
}
}
private struct ThreadStartArgs
{
public bool IsStdErr;
public StreamReader Stream;
}
private string GetGodotExecutablePath()
{
if (Settings.AlwaysUseConfiguredExecutable)
return Settings.GodotExecutablePath;
string godotPath = GodotCmd.GodotIdeClient.GodotEditorExecutablePath;
if (string.IsNullOrEmpty(godotPath) || !File.Exists(godotPath))
return Settings.GodotExecutablePath;
return godotPath;
}
protected override void OnRun(DebuggerStartInfo startInfo)
{
var godotStartInfo = (GodotDebuggerStartInfo)startInfo;
GodotCmd = godotStartInfo.GodotCmd;
switch (GodotCmd.ExecutionType)
{
case ExecutionType.PlayInEditor:
{
attached = false;
StartListening(godotStartInfo, out var assignedDebugPort);
string host = "127.0.0.1";
GodotCmd.GodotIdeClient.SendPlay(host, assignedDebugPort);
// TODO: Read the editor player stdout and stderr somehow
break;
}
case ExecutionType.Launch:
{
attached = false;
StartListening(godotStartInfo, out var assignedDebugPort);
// Listener to replace the Godot editor remote debugger.
// We use it to notify the game when assemblies should be reloaded.
var remoteDebugListener = new TcpListener(IPAddress.Any, 0);
remoteDebugListener.Start();
remoteDebugListener.AcceptTcpClientAsync().ContinueWith(OnGodotRemoteDebuggerConnected);
string workingDir = startInfo.WorkingDirectory;
string host = "127.0.0.1";
int remoteDebugPort = ((IPEndPoint)remoteDebugListener.LocalEndpoint).Port;
// Launch Godot to run the game and connect to our remote debugger
var processStartInfo = new ProcessStartInfo(GetGodotExecutablePath())
{
Arguments = $"--path {workingDir} --remote-debug {host}:{remoteDebugPort}",
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
// Tells Godot to connect to the mono debugger we just started
processStartInfo.EnvironmentVariables["GODOT_MONO_DEBUGGER_AGENT"] =
"--debugger-agent=transport=dt_socket" +
$",address={host}:{assignedDebugPort}" +
",server=n";
var process = Process.Start(processStartInfo);
// Listen for StdOut and StdErr
var stdOutThread = new Thread(OutputReader)
{
Name = "Godot StandardOutput Reader",
IsBackground = true
};
stdOutThread.Start(new ThreadStartArgs {
IsStdErr = false, Stream = process.StandardOutput
});
var stdErrThread = new Thread(OutputReader)
{
Name = "Godot StandardError Reader",
IsBackground = true
};
stdErrThread.Start(new ThreadStartArgs {
IsStdErr = true, Stream = process.StandardError
});
OnDebuggerOutput(false, $"Godot PID:{process.Id}{Environment.NewLine}");
break;
}
case ExecutionType.Attach:
{
attached = true;
StartConnecting(godotStartInfo);
break;
}
default:
throw new NotImplementedException(GodotCmd.ExecutionType.ToString());
}
}
private async Task OnGodotRemoteDebuggerConnected(Task<TcpClient> task)
{
var tcpClient = task.Result;
godotRemoteDebuggerStream = tcpClient.GetStream();
byte[] buffer = new byte[1000];
while (tcpClient.Connected)
{
// There is no library to decode this messages, so
// we just pump buffer so it doesn't go out of memory
var readBytes = await godotRemoteDebuggerStream.ReadAsync(buffer, 0, buffer.Length);
}
}
protected override bool HandleException(Exception ex)
{
// When we attach to running Mono process it sends us AssemblyLoad, ThreadStart and other
// delayed events, problem is, that we send VM_START command back since we have to do that on
// every event, but in this case when Mono is sending delayed events when we attach
// runtime is not really suspended, hence it's throwing this exceptions, just ignore...
if (attached && ex is Mono.Debugger.Soft.VMNotSuspendedException)
return true;
return base.HandleException(ex);
}
protected override void OnExit()
{
if (attached)
base.OnDetach();
else
base.OnExit();
}
private void OutputReader(object args)
{
var startArgs = (ThreadStartArgs)args;
foreach (string line in startArgs.Stream.EnumerateLines())
{
try
{
OnTargetOutput(startArgs.IsStdErr, line + Environment.NewLine);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
using Mono.Debugging.Soft;
namespace GodotAddin
{
public class GodotDebuggerStartInfo : SoftDebuggerStartInfo
{
public GodotExecutionCommand GodotCmd { get; }
public GodotDebuggerStartInfo(GodotExecutionCommand godotCmd, SoftDebuggerRemoteArgs softDebuggerConnectArgs) :
base(softDebuggerConnectArgs)
{
GodotCmd = godotCmd;
}
}
}

View File

@@ -0,0 +1,28 @@
using MonoDevelop.Core.Execution;
namespace GodotAddin
{
public class GodotExecutionCommand : ExecutionCommand
{
public GodotExecutionCommand(string godotProjectPath, ExecutionType executionType,
string workingDirectory, GodotMonoDevelopClient godotIdeClient)
{
GodotProjectPath = godotProjectPath;
ExecutionType = executionType;
WorkingDirectory = workingDirectory;
GodotIdeClient = godotIdeClient;
}
public string GodotProjectPath { get; }
public ExecutionType ExecutionType { get; }
public string WorkingDirectory { get; }
public GodotMonoDevelopClient GodotIdeClient { get; }
}
public enum ExecutionType
{
PlayInEditor,
Launch,
Attach
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Threading;
using GodotTools.IdeConnection;
using MonoDevelop.Core;
using MonoDevelop.Ide;
using MonoDevelop.Ide.Gui;
namespace GodotAddin
{
public class GodotMonoDevelopClient : GodotIdeClient
{
private class MonoDevelopLogger : ILogger
{
public void LogDebug(string message)
{
LoggingService.LogDebug(message);
}
public void LogInfo(string message)
{
LoggingService.LogInfo(message);
}
public void LogWarning(string message)
{
LoggingService.LogWarning(message);
}
public void LogError(string message)
{
LoggingService.LogError(message);
}
public void LogError(string message, Exception e)
{
LoggingService.LogError(message, e);
}
}
public GodotMonoDevelopClient(string projectMetadataDir) : base(projectMetadataDir)
{
Logger = new MonoDevelopLogger();
}
public string GodotEditorExecutablePath => GodotIdeMetadata.EditorExecutablePath;
private static void DispatchToGuiThread(Action action)
{
var d = new SendOrPostCallback((target) => action());
DispatchService.SynchronizationContext.Send(d, null);
}
protected override void OpenFile(string file)
{
OpenFile(file, line: 0, column: 0);
}
protected override void OpenFile(string file, int line)
{
OpenFile(file, line, column: 0);
}
protected override void OpenFile(string file, int line, int column)
{
DispatchToGuiThread(() =>
{
var fileOpenInfo = new FileOpenInformation(new FilePath(file),
project: null /* autodetect */,
line: line,
column: column,
options: OpenDocumentOptions.Default
);
IdeApp.OpenFiles(new[] { fileOpenInfo });
// Make the Ide window grab focus
IdeApp.Workbench.Present();
});
}
public bool SendPlay()
{
return WriteMessage(new Message("Play"));
}
public bool SendPlay(string debuggerHost, int debuggerPort)
{
return WriteMessage(new Message("Play", debuggerHost, debuggerPort.ToString()));
}
public bool SendReloadScripts()
{
return WriteMessage(new Message("ReloadScripts"));
}
}
}

View File

@@ -0,0 +1,64 @@
using MonoDevelop.Ide.Gui.Dialogs;
using MonoDevelop.Components;
using MonoDevelop.Core;
namespace GodotAddin
{
public class GodotOptionsPanel : OptionsPanel
{
FileEntry godotExeFileEntry = new FileEntry();
Gtk.CheckButton alwaysUseExeCheckButton = new Gtk.CheckButton();
public override Control CreatePanelWidget()
{
var vbox = new Gtk.VBox { Spacing = 6 };
var generalSectionLabel = new Gtk.Label
{
Text = $"<b>{GettextCatalog.GetString("General")}</b>",
UseMarkup = true,
Xalign = 0
};
vbox.PackStart(generalSectionLabel, false, false, 0);
var godotExeHBox = new Gtk.HBox { BorderWidth = 10, Spacing = 6 };
var godotExeLabel = new Gtk.Label
{
Text = GettextCatalog.GetString("Godot executable:"),
Xalign = 0
};
godotExeHBox.PackStart(godotExeLabel, false, false, 0);
godotExeFileEntry.Path = Settings.GodotExecutablePath.Value;
godotExeHBox.PackStart(godotExeFileEntry, true, true, 0);
vbox.PackStart(godotExeHBox, false, false, 0);
var alwaysUseExeHBox = new Gtk.HBox { BorderWidth = 10, Spacing = 6 };
var alwaysUseExeLabel = new Gtk.Label
{
Text = GettextCatalog.GetString("Always use this executable:"),
Xalign = 0
};
alwaysUseExeHBox.PackStart(alwaysUseExeLabel, false, false, 0);
alwaysUseExeCheckButton.Active = Settings.AlwaysUseConfiguredExecutable.Value;
alwaysUseExeHBox.PackStart(alwaysUseExeCheckButton, true, true, 0);
vbox.PackStart(alwaysUseExeHBox, false, false, 0);
vbox.ShowAll();
return vbox;
}
public override void ApplyChanges()
{
Settings.GodotExecutablePath.Value = godotExeFileEntry.Path;
Settings.GodotExecutablePath.Value = godotExeFileEntry.Path;
}
}
}

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using MonoDevelop.Core;
using MonoDevelop.Core.Execution;
using MonoDevelop.Ide;
using MonoDevelop.Projects;
namespace GodotAddin
{
[ExportProjectModelExtension]
public class GodotProjectExtension : DotNetProjectExtension, IDisposable
{
private static readonly SolutionItemRunConfiguration[] runConfigurations =
{
new ProjectRunConfiguration("Play in Editor"),
new ProjectRunConfiguration("Launch"),
new ProjectRunConfiguration("Attach")
};
private static readonly ExecutionType[] executionTypes =
{
ExecutionType.PlayInEditor,
ExecutionType.Launch,
ExecutionType.Attach
};
private static SolutionItemRunConfiguration GetRunConfiguration(ExecutionType type)
{
switch (type)
{
case ExecutionType.PlayInEditor:
return runConfigurations[0];
case ExecutionType.Launch:
return runConfigurations[1];
case ExecutionType.Attach:
return runConfigurations[2];
default:
throw new NotSupportedException();
}
}
private GodotMonoDevelopClient godotIdeClient;
protected override void OnItemReady()
{
base.OnItemReady();
if (!IsGodotProject())
return;
string godotProjectDir = Path.GetDirectoryName(GetGodotProjectPath());
string godotProjectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata");
LoggingService.LogInfo($"Godot project directory is: {godotProjectDir}");
try
{
godotIdeClient?.Dispose();
godotIdeClient = new GodotMonoDevelopClient(godotProjectMetadataDir);
godotIdeClient.Start();
}
catch (Exception e)
{
LoggingService.LogError("Exception when initializing Godot Ide Client", e);
}
}
protected override IEnumerable<SolutionItemRunConfiguration> OnGetRunConfigurations(OperationContext ctx)
{
if (IsGodotProject())
return runConfigurations;
return base.OnGetRunConfigurations(ctx);
}
protected override ExecutionCommand OnCreateExecutionCommand(ConfigurationSelector configSel, DotNetProjectConfiguration configuration, ProjectRunConfiguration runConfiguration)
{
if (IsGodotProject())
{
var runConfigurationIndex = runConfigurations.IndexOf(runConfiguration);
if (runConfigurationIndex == -1)
LoggingService.LogError($"Unexpected RunConfiguration {runConfiguration.Id} {runConfiguration.GetType().FullName}");
var executionType = executionTypes[runConfigurationIndex];
if (executionType == ExecutionType.PlayInEditor && !godotIdeClient.IsConnected)
LoggingService.LogError($"Cannot launch editor player because the Godot Ide Client is not connected");
string godotProjectPath = GetGodotProjectPath();
return new GodotExecutionCommand(
godotProjectPath,
executionType,
Path.GetDirectoryName(godotProjectPath),
godotIdeClient
);
}
return base.OnCreateExecutionCommand(configSel, configuration, runConfiguration);
}
private string GetGodotProjectPath()
{
return Path.Combine(Path.GetDirectoryName(Project.ParentSolution.FileName), "project.godot");
}
private bool? cachedIsGodotProject;
private bool IsGodotProject()
{
if (!cachedIsGodotProject.HasValue)
cachedIsGodotProject = File.Exists(GetGodotProjectPath());
return cachedIsGodotProject.Value;
}
protected override ProjectFeatures OnGetSupportedFeatures()
{
var features = base.OnGetSupportedFeatures();
if (IsGodotProject())
features |= ProjectFeatures.Execute;
return features;
}
protected override bool OnGetCanExecute(ExecutionContext context, ConfigurationSelector configuration, SolutionItemRunConfiguration runConfiguration)
{
if (IsGodotProject())
{
if (runConfiguration == GetRunConfiguration(ExecutionType.PlayInEditor))
{
// 'Play in Editor' requires the Godot Ide Client to be connected
// to a server and the selected run configuration to be 'Tools'.
if (!godotIdeClient.IsConnected || IdeApp.Workspace.ActiveConfigurationId != "Tools")
return false;
}
return true;
}
return base.OnGetCanExecute(context, configuration, runConfiguration);
}
protected override async Task OnExecuteCommand(ProgressMonitor monitor, ExecutionContext context, ConfigurationSelector configuration, ExecutionCommand executionCommand)
{
if (executionCommand is GodotExecutionCommand godotCmd)
{
if (godotCmd.ExecutionType == ExecutionType.Launch)
{
if (!File.Exists(Settings.GodotExecutablePath))
{
// Delay for 1 sec so it's not overriden by build message
await Task.Delay(1000);
monitor.ReportError(GettextCatalog.GetString($"Godot executable \"{Settings.GodotExecutablePath.Value}\" not found. Update Godot executable setting in perferences."));
return;
}
}
}
await base.OnExecuteCommand(monitor, context, configuration, executionCommand);
}
public override void Dispose()
{
base.Dispose();
godotIdeClient?.Dispose();
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Net;
using Mono.Debugging.Soft;
namespace GodotAddin
{
public class GodotSoftDebuggerArgs : SoftDebuggerRemoteArgs
{
public GodotSoftDebuggerArgs(string appName, IPAddress address, int debugPort, int outputPort) : base(appName, address, debugPort, outputPort)
{
}
public override ISoftDebuggerConnectionProvider ConnectionProvider => null;
}
}

128
GodotAddin/GodotVariant.cs Normal file
View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace GodotAddin
{
// Incomplete implementation of the Godot's Variant encoder. Add missing parts as needed.
public class GodotVariantEncoder
{
private List<byte> buffer = new List<byte>();
public int Length => buffer.Count;
public byte[] ToArray() => buffer.ToArray();
public void AddBytes(params byte[] bytes) =>
buffer.AddRange(bytes);
public void AddInt(int value) =>
AddBytes(BitConverter.GetBytes(value));
public void AddType(GodotVariant.Type type) =>
AddInt((int)type);
public void AddString(string value)
{
byte[] utf8Bytes = Encoding.UTF8.GetBytes(value);
AddType(GodotVariant.Type.String);
AddInt(utf8Bytes.Length);
AddBytes(utf8Bytes);
AddBytes(0); // Godot's UTF8 converter adds a trailing whitespace
while (buffer.Count % 4 != 0)
buffer.Add(0);
}
public void AddArray(List<GodotVariant> array)
{
AddType(GodotVariant.Type.Array);
AddInt((int)array.Count);
foreach (object element in array)
{
if (element is string str)
AddString(str);
else
throw new NotImplementedException();
}
}
public static void Encode(GodotVariant variant, Stream stream)
{
using (var writer = new BinaryWriter(stream, new UTF8Encoding(false, true), leaveOpen: true))
{
var encoder = new GodotVariantEncoder();
switch (variant.VariantType)
{
case GodotVariant.Type.String:
encoder.AddString((string)variant.Value);
break;
case GodotVariant.Type.Array:
encoder.AddArray((List<GodotVariant>)variant.Value);
break;
default:
throw new NotImplementedException();
}
writer.Write((int)encoder.Length);
writer.Write(encoder.ToArray());
}
}
}
public class GodotVariant
{
public enum Type
{
Nil = 0,
Bool = 1,
Int = 2,
Real = 3,
String = 4,
Vector2 = 5,
Rect2 = 6,
Vector3 = 7,
Transform2d = 8,
Quat = 10,
Aabb = 11,
Basis = 12,
Transform = 13,
Color = 14,
NodePath = 15,
Rid = 16,
Object = 17,
Dictionary = 18,
Array = 19,
RawArray = 20,
IntArray = 21,
RealArray = 22,
StringArray = 23,
Vector2Array = 24,
Vector3Array = 25,
ColorArray = 26,
Max = 27
}
public object Value { get; }
public Type VariantType { get; }
public GodotVariant(string value)
{
Value = value;
VariantType = Type.String;
}
public GodotVariant(List<GodotVariant> value)
{
Value = value;
VariantType = Type.Array;
}
public static implicit operator GodotVariant(string value) => new GodotVariant(value);
public static implicit operator GodotVariant(List<GodotVariant> value) => new GodotVariant(value);
}
}

View File

@@ -0,0 +1,13 @@
using Mono.Addins;
using Mono.Addins.Description;
[assembly: Addin(
"GodotAddin",
Namespace = "GodotAddin",
Version = "1.0"
)]
[assembly: AddinName("Godot Addin")]
[assembly: AddinCategory("IDE extensions")]
[assembly: AddinDescription("Extension for the Godot game engine")]
[assembly: AddinAuthor("Godot Engine contributors")]

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionModel>
<Extension path="/MonoDevelop/Debugging/DebuggerEngines">
<DebuggerEngine
id="MonoDevelop.MicroFramework.Debugger"
name="Debugger for Godot"
features="Breakpoints, Pause, Stepping, DebugFile, ConditionalBreakpoints, Tracepoints, Catchpoints, Disassembly"
type="GodotAddin.GodotDebuggerEngine" />
</Extension>
<Extension path="/MonoDevelop/Ide/GlobalOptionsDialog">
<Section id="Godot" _label="Godot Engine" insertafter="VersionControl">
<Section id="GodotGeneral" _label="General" fill="true" class="GodotAddin.GodotOptionsPanel" />
</Section>
</Extension>
</ExtensionModel>

30
GodotAddin/Settings.cs Normal file
View File

@@ -0,0 +1,30 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using MonoDevelop.Core;
namespace GodotAddin
{
public static class Settings
{
private static string DetermineDefaultGodotPath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return "/Applications/Godot_mono.app/Contents/MacOS/Godot";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? string.Empty, "Godot_x11");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE") ?? string.Empty, "Godot_win32.exe");
return string.Empty;
}
public static readonly ConfigurationProperty<string> GodotExecutablePath =
ConfigurationProperty.Create("Godot.GodotExecutable", DetermineDefaultGodotPath());
public static readonly ConfigurationProperty<bool> AlwaysUseConfiguredExecutable =
ConfigurationProperty.Create("Godot.AlwaysUseConfiguredExecutable", false);
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.IO;
namespace GodotAddin.Utils
{
public static class ExtensionMethods
{
public static IEnumerable<string> EnumerateLines(this TextReader textReader)
{
string line;
while ((line = textReader.ReadLine()) != null)
yield return line;
}
}
}

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,53 @@
<?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>
<LangVersion>7</LangVersion>
</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")]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 Godot Engine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
README.md Executable file
View File

@@ -0,0 +1,11 @@
# Godot Add-in for MonoDevelop and Visual Studio for Mac
MonoDevelop and Visual Studio for Mac add-in for seamless integration with the Godot game engine.
This add-in is based on [GodotExtension](https://github.com/DavidKarlas/GodotExtension).
## Features
- Handling file open requests from the Godot editor.
- Launching and debugging the Godot editor player.
- Launching and debugging a Godot executable.
- Attach the debugger to a running Godot instance.