Added godot's mono module.

This commit is contained in:
Relintai 2023-05-23 17:54:30 +02:00
commit 5c461069b3
229 changed files with 49934 additions and 0 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
[*.sln]
indent_style = tab
[*.{csproj,props,targets,nuspec,resx}]
indent_style = space
indent_size = 2
[*.cs]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120
csharp_indent_case_contents_when_block = false

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Do not ignore solution files inside the mono module. Overrides Godot's global gitignore.
!*.sln

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2023-present Péter Magyar.
Copyright (c) 2014-2023 Godot Engine contributors (cf. AUTHORS.md).
Copyright (c) 2007-2023 Juan Linietsky, Ariel Manzur.
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.

64
SCsub Normal file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
import build_scripts.tls_configure as tls_configure
import build_scripts.mono_configure as mono_configure
Import("env")
Import("env_modules")
env_mono = env_modules.Clone()
if env_mono["tools"]:
# NOTE: It is safe to generate this file here, since this is still executed serially
import build_scripts.gen_cs_glue_version as gen_cs_glue_version
gen_cs_glue_version.generate_header("glue/GodotSharp", "glue/cs_glue_version.gen.h")
# Glue sources
if env_mono["mono_glue"]:
env_mono.Append(CPPDEFINES=["MONO_GLUE_ENABLED"])
import os.path
if not os.path.isfile("glue/mono_glue.gen.cpp"):
raise RuntimeError("Mono glue sources not found. Did you forget to run '--generate-mono-glue'?")
# Configure Thread Local Storage
conf = Configure(env_mono)
tls_configure.configure(conf)
env_mono = conf.Finish()
# Configure Mono
mono_configure.configure(env, env_mono)
if env_mono["tools"] and env_mono["mono_glue"] and env_mono["build_cil"]:
# Build Godot API solution
import build_scripts.api_solution_build as api_solution_build
api_sln_cmd = api_solution_build.build(env_mono)
# Build GodotTools
import build_scripts.godot_tools_build as godot_tools_build
godot_tools_build.build(env_mono, api_sln_cmd)
# Add sources
env_mono.add_source_files(env.modules_sources, "*.cpp")
env_mono.add_source_files(env.modules_sources, "glue/*.cpp")
env_mono.add_source_files(env.modules_sources, "glue/mono_glue.gen.cpp")
env_mono.add_source_files(env.modules_sources, "mono_gd/*.cpp")
env_mono.add_source_files(env.modules_sources, "utils/*.cpp")
env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.cpp")
if env["platform"] in ["osx", "iphone"]:
env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.mm")
env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.m")
elif env["platform"] == "android":
env_mono.add_source_files(env.modules_sources, "mono_gd/android_mono_config.gen.cpp")
if env["tools"]:
env_mono.add_source_files(env.modules_sources, "editor/*.cpp")

0
__init__.py Normal file
View File

View File

View File

@ -0,0 +1,80 @@
# Build the Godot API solution
import os
from SCons.Script import Dir
def build_api_solution(source, target, env):
# source and target elements are of type SCons.Node.FS.File, hence why we convert them to str
module_dir = env["module_dir"]
solution_path = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
build_config = env["solution_build_config"]
extra_msbuild_args = ["/p:NoWarn=1591"] # Ignore missing documentation warnings
from .solution_builder import build_solution
build_solution(env, solution_path, build_config, extra_msbuild_args=extra_msbuild_args)
# Copy targets
core_src_dir = os.path.abspath(os.path.join(solution_path, os.pardir, "GodotSharp", "bin", build_config))
editor_src_dir = os.path.abspath(os.path.join(solution_path, os.pardir, "GodotSharpEditor", "bin", build_config))
dst_dir = os.path.abspath(os.path.join(str(target[0]), os.pardir))
if not os.path.isdir(dst_dir):
assert not os.path.isfile(dst_dir)
os.makedirs(dst_dir)
def copy_target(target_path):
from shutil import copy
filename = os.path.basename(target_path)
src_path = os.path.join(core_src_dir, filename)
if not os.path.isfile(src_path):
src_path = os.path.join(editor_src_dir, filename)
copy(src_path, target_path)
for scons_target in target:
copy_target(str(scons_target))
def build(env_mono):
assert env_mono["tools"]
target_filenames = [
"GodotSharp.dll",
"GodotSharp.pdb",
"GodotSharp.xml",
"GodotSharpEditor.dll",
"GodotSharpEditor.pdb",
"GodotSharpEditor.xml",
]
depend_cmd = []
for build_config in ["Debug", "Release"]:
output_dir = Dir("#bin").abspath
editor_api_dir = os.path.join(output_dir, "GodotSharp", "Api", build_config)
targets = [os.path.join(editor_api_dir, filename) for filename in target_filenames]
cmd = env_mono.CommandNoCache(
targets, depend_cmd, build_api_solution, module_dir=os.getcwd(), solution_build_config=build_config
)
env_mono.AlwaysBuild(cmd)
# Make the Release build of the API solution depend on the Debug build.
# We do this in order to prevent SCons from building them in parallel,
# which can freak out MSBuild. In many cases, one of the builds would
# hang indefinitely requiring a key to be pressed for it to continue.
depend_cmd = cmd
return depend_cmd

View File

@ -0,0 +1,20 @@
def generate_header(solution_dir, version_header_dst):
import os
latest_mtime = 0
for root, dirs, files in os.walk(solution_dir, topdown=True):
dirs[:] = [d for d in dirs if d not in ["Generated"]] # Ignored generated files
files = [f for f in files if f.endswith(".cs")]
for file in files:
filepath = os.path.join(root, file)
mtime = os.path.getmtime(filepath)
latest_mtime = mtime if mtime > latest_mtime else latest_mtime
glue_version = int(latest_mtime) # The latest modified time will do for now
with open(version_header_dst, "w") as version_header:
version_header.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n")
version_header.write("#ifndef CS_GLUE_VERSION_H\n")
version_header.write("#define CS_GLUE_VERSION_H\n\n")
version_header.write("#define CS_GLUE_VERSION UINT32_C(" + str(glue_version) + ")\n")
version_header.write("\n#endif // CS_GLUE_VERSION_H\n")

View File

@ -0,0 +1,38 @@
# Build GodotTools solution
import os
from SCons.Script import Dir
def build_godot_tools(source, target, env):
# source and target elements are of type SCons.Node.FS.File, hence why we convert them to str
module_dir = env["module_dir"]
solution_path = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln")
build_config = "Debug" if env["target"] == "debug" else "Release"
from .solution_builder import build_solution
extra_msbuild_args = ["/p:GodotPlatform=" + env["platform"]]
build_solution(env, solution_path, build_config, extra_msbuild_args)
# No need to copy targets. The GodotTools csproj takes care of copying them.
def build(env_mono, api_sln_cmd):
assert env_mono["tools"]
output_dir = Dir("#bin").abspath
editor_tools_dir = os.path.join(output_dir, "GodotSharp", "Tools")
target_filenames = ["GodotTools.dll"]
if env_mono["target"] == "debug":
target_filenames += ["GodotTools.pdb"]
targets = [os.path.join(editor_tools_dir, filename) for filename in target_filenames]
cmd = env_mono.CommandNoCache(targets, api_sln_cmd, build_godot_tools, module_dir=os.getcwd())
env_mono.AlwaysBuild(cmd)

View File

@ -0,0 +1,55 @@
def generate_compressed_config(config_src, output_dir):
import os.path
from compat import byte_to_str
# Source file
with open(os.path.join(output_dir, "android_mono_config.gen.cpp"), "w") as cpp:
with open(config_src, "rb") as f:
buf = f.read()
decompr_size = len(buf)
import zlib
buf = zlib.compress(buf)
compr_size = len(buf)
bytes_seq_str = ""
for i, buf_idx in enumerate(range(compr_size)):
if i > 0:
bytes_seq_str += ", "
bytes_seq_str += byte_to_str(buf[buf_idx])
cpp.write(
"""/* THIS FILE IS GENERATED DO NOT EDIT */
#include "android_mono_config.h"
#ifdef ANDROID_ENABLED
#include "core/io/compression.h"
#include "core/pool_vector.h"
namespace {
// config
static const int config_compressed_size = %d;
static const int config_uncompressed_size = %d;
static const unsigned char config_compressed_data[] = { %s };
} // namespace
String get_godot_android_mono_config() {
PoolVector<uint8_t> data;
data.resize(config_uncompressed_size);
PoolVector<uint8_t>::Write w = data.write();
Compression::decompress(w.ptr(), config_uncompressed_size, config_compressed_data,
config_compressed_size, Compression::MODE_DEFLATE);
String s;
if (s.parse_utf8((const char *)w.ptr(), data.size())) {
ERR_FAIL_V(String());
}
return s;
}
#endif // ANDROID_ENABLED
"""
% (compr_size, decompr_size, bytes_seq_str)
)

View File

@ -0,0 +1,28 @@
<configuration>
<dllmap wordsize="32" dll="i:cygwin1.dll" target="/system/lib/libc.so" />
<dllmap wordsize="64" dll="i:cygwin1.dll" target="/system/lib64/libc.so" />
<dllmap wordsize="32" dll="libc" target="/system/lib/libc.so" />
<dllmap wordsize="64" dll="libc" target="/system/lib64/libc.so" />
<dllmap wordsize="32" dll="intl" target="/system/lib/libc.so" />
<dllmap wordsize="64" dll="intl" target="/system/lib64/libc.so" />
<dllmap wordsize="32" dll="libintl" target="/system/lib/libc.so" />
<dllmap wordsize="64" dll="libintl" target="/system/lib64/libc.so" />
<dllmap dll="MonoPosixHelper" target="libMonoPosixHelper.so" />
<dllmap dll="System.Native" target="libmono-native.so" />
<dllmap wordsize="32" dll="i:msvcrt" target="/system/lib/libc.so" />
<dllmap wordsize="64" dll="i:msvcrt" target="/system/lib64/libc.so" />
<dllmap wordsize="32" dll="i:msvcrt.dll" target="/system/lib/libc.so" />
<dllmap wordsize="64" dll="i:msvcrt.dll" target="/system/lib64/libc.so" />
<dllmap wordsize="32" dll="sqlite" target="/system/lib/libsqlite.so" />
<dllmap wordsize="64" dll="sqlite" target="/system/lib64/libsqlite.so" />
<dllmap wordsize="32" dll="sqlite3" target="/system/lib/libsqlite.so" />
<dllmap wordsize="64" dll="sqlite3" target="/system/lib64/libsqlite.so" />
<dllmap wordsize="32" dll="liblog" target="/system/lib/liblog.so" />
<dllmap wordsize="64" dll="liblog" target="/system/lib64/liblog.so" />
<dllmap dll="i:kernel32.dll">
<dllentry dll="__Internal" name="CopyMemory" target="mono_win32_compat_CopyMemory"/>
<dllentry dll="__Internal" name="FillMemory" target="mono_win32_compat_FillMemory"/>
<dllentry dll="__Internal" name="MoveMemory" target="mono_win32_compat_MoveMemory"/>
<dllentry dll="__Internal" name="ZeroMemory" target="mono_win32_compat_ZeroMemory"/>
</dllmap>
</configuration>

View File

@ -0,0 +1,585 @@
import os
import os.path
import sys
import subprocess
from SCons.Script import Dir, Environment
if os.name == "nt":
from . import mono_reg_utils as monoreg
android_arch_dirs = {"armv7": "armeabi-v7a", "arm64v8": "arm64-v8a", "x86": "x86", "x86_64": "x86_64"}
def get_android_out_dir(env):
return os.path.join(
Dir("#platform/android/java/lib/libs").abspath,
"release" if env["target"] == "release" else "debug",
android_arch_dirs[env["android_arch"]],
)
def find_name_in_dir_files(directory, names, prefixes=[""], extensions=[""]):
for extension in extensions:
if extension and not extension.startswith("."):
extension = "." + extension
for prefix in prefixes:
for curname in names:
if os.path.isfile(os.path.join(directory, prefix + curname + extension)):
return curname
return ""
def find_file_in_dir(directory, names, prefixes=[""], extensions=[""]):
for extension in extensions:
if extension and not extension.startswith("."):
extension = "." + extension
for prefix in prefixes:
for curname in names:
filename = prefix + curname + extension
if os.path.isfile(os.path.join(directory, filename)):
return filename
return ""
def copy_file(src_dir, dst_dir, src_name, dst_name=""):
from shutil import copy
src_path = os.path.join(Dir(src_dir).abspath, src_name)
dst_dir = Dir(dst_dir).abspath
if not os.path.isdir(dst_dir):
os.makedirs(dst_dir)
if dst_name:
copy(src_path, os.path.join(dst_dir, dst_name))
else:
copy(src_path, dst_dir)
def is_desktop(platform):
return platform in ["windows", "osx", "x11", "server", "uwp", "haiku"]
def is_unix_like(platform):
return platform in ["osx", "x11", "server", "android", "haiku", "iphone"]
def module_supports_tools_on(platform):
return platform not in ["android", "javascript", "iphone"]
def find_wasm_src_dir(mono_root):
hint_dirs = [
os.path.join(mono_root, "src"),
os.path.join(mono_root, "../src"),
]
for hint_dir in hint_dirs:
if os.path.isfile(os.path.join(hint_dir, "driver.c")):
return hint_dir
return ""
def configure(env, env_mono):
bits = env["bits"]
is_android = env["platform"] == "android"
is_javascript = env["platform"] == "javascript"
is_ios = env["platform"] == "iphone"
is_ios_sim = is_ios and env["ios_simulator"]
tools_enabled = env["tools"]
mono_static = env["mono_static"]
copy_mono_root = env["copy_mono_root"]
mono_prefix = env["mono_prefix"]
mono_bcl = env["mono_bcl"]
mono_lib_names = ["mono-2.0-sgen", "monosgen-2.0"]
if is_android and not env["android_arch"] in android_arch_dirs:
raise RuntimeError("This module does not support the specified 'android_arch': " + env["android_arch"])
if tools_enabled and not module_supports_tools_on(env["platform"]):
# TODO:
# Android: We have to add the data directory to the apk, concretely the Api and Tools folders.
raise RuntimeError("This module does not currently support building for this platform with tools enabled")
if is_android and mono_static:
# FIXME: When static linking and doing something that requires libmono-native, we get a dlopen error as 'libmono-native'
# seems to depend on 'libmonosgen-2.0'. Could be fixed by re-directing to '__Internal' with a dllmap or in the dlopen hook.
raise RuntimeError("Statically linking Mono is not currently supported for this platform")
if not mono_static and (is_javascript or is_ios):
raise RuntimeError("Dynamically linking Mono is not currently supported for this platform")
if not mono_prefix and (os.getenv("MONO32_PREFIX") or os.getenv("MONO64_PREFIX")):
print(
"WARNING: The environment variables 'MONO32_PREFIX' and 'MONO64_PREFIX' are deprecated; use the 'mono_prefix' SCons parameter instead"
)
# Although we don't support building with tools for any platform where we currently use static AOT,
# if these are supported in the future, we won't be using static AOT for them as that would be
# too restrictive for the editor. These builds would probably be made to only use the interpreter.
mono_aot_static = (is_ios and not is_ios_sim) and not env["tools"]
# Static AOT is only supported on the root domain
mono_single_appdomain = mono_aot_static
if mono_single_appdomain:
env_mono.Append(CPPDEFINES=["GD_MONO_SINGLE_APPDOMAIN"])
if (env["tools"] or env["target"] != "release") and not mono_single_appdomain:
env_mono.Append(CPPDEFINES=["GD_MONO_HOT_RELOAD"])
if env["platform"] == "windows":
mono_root = mono_prefix
if not mono_root and os.name == "nt":
mono_root = monoreg.find_mono_root_dir(bits)
if not mono_root:
raise RuntimeError(
"Mono installation directory not found; specify one manually with the 'mono_prefix' SCons parameter"
)
print("Found Mono root directory: " + mono_root)
mono_lib_path = os.path.join(mono_root, "lib")
env.Append(LIBPATH=mono_lib_path)
env_mono.Prepend(CPPPATH=os.path.join(mono_root, "include", "mono-2.0"))
lib_suffixes = [".lib"]
if not env.msvc:
# MingW supports both '.a' and '.lib'
lib_suffixes.insert(0, ".a")
if mono_static:
if env.msvc:
mono_static_lib_name = "libmono-static-sgen"
else:
mono_static_lib_name = "libmonosgen-2.0"
mono_static_lib_file = find_file_in_dir(mono_lib_path, [mono_static_lib_name], extensions=lib_suffixes)
if not mono_static_lib_file:
raise RuntimeError("Could not find static mono library in: " + mono_lib_path)
if env.msvc:
env.Append(LINKFLAGS=mono_static_lib_file)
env.Append(LINKFLAGS="Mincore.lib")
env.Append(LINKFLAGS="msvcrt.lib")
env.Append(LINKFLAGS="LIBCMT.lib")
env.Append(LINKFLAGS="Psapi.lib")
else:
mono_static_lib_file_path = os.path.join(mono_lib_path, mono_static_lib_file)
env.Append(LINKFLAGS=["-Wl,-whole-archive", mono_static_lib_file_path, "-Wl,-no-whole-archive"])
env.Append(LIBS=["psapi"])
env.Append(LIBS=["version"])
else:
mono_lib_name = find_name_in_dir_files(
mono_lib_path, mono_lib_names, prefixes=["", "lib"], extensions=lib_suffixes
)
if not mono_lib_name:
raise RuntimeError("Could not find mono library in: " + mono_lib_path)
if env.msvc:
env.Append(LINKFLAGS=mono_lib_name + ".lib")
else:
env.Append(LIBS=[mono_lib_name])
mono_bin_path = os.path.join(mono_root, "bin")
mono_dll_file = find_file_in_dir(mono_bin_path, mono_lib_names, prefixes=["", "lib"], extensions=[".dll"])
if not mono_dll_file:
raise RuntimeError("Could not find mono shared library in: " + mono_bin_path)
copy_file(mono_bin_path, "#bin", mono_dll_file)
else:
is_apple = env["platform"] in ["osx", "iphone"]
is_macos = is_apple and not is_ios
sharedlib_ext = ".dylib" if is_apple else ".so"
mono_root = mono_prefix
mono_lib_path = ""
mono_so_file = ""
if not mono_root and (is_android or is_javascript or is_ios):
raise RuntimeError(
"Mono installation directory not found; specify one manually with the 'mono_prefix' SCons parameter"
)
if not mono_root and is_macos:
# Try with some known directories under OSX
hint_dirs = ["/Library/Frameworks/Mono.framework/Versions/Current", "/usr/local/var/homebrew/linked/mono"]
for hint_dir in hint_dirs:
if os.path.isdir(hint_dir):
mono_root = hint_dir
break
# We can't use pkg-config to link mono statically,
# but we can still use it to find the mono root directory
if not mono_root and mono_static:
mono_root = pkgconfig_try_find_mono_root(mono_lib_names, sharedlib_ext)
if not mono_root:
raise RuntimeError(
"Building with mono_static=yes, but failed to find the mono prefix with pkg-config; "
+ "specify one manually with the 'mono_prefix' SCons parameter"
)
if is_ios and not is_ios_sim:
env_mono.Append(CPPDEFINES=["IOS_DEVICE"])
if mono_root:
print("Found Mono root directory: " + mono_root)
mono_lib_path = os.path.join(mono_root, "lib")
env.Append(LIBPATH=[mono_lib_path])
env_mono.Prepend(CPPPATH=os.path.join(mono_root, "include", "mono-2.0"))
mono_lib = find_name_in_dir_files(mono_lib_path, mono_lib_names, prefixes=["lib"], extensions=[".a"])
if not mono_lib:
raise RuntimeError("Could not find mono library in: " + mono_lib_path)
env_mono.Append(CPPDEFINES=["_REENTRANT"])
if mono_static:
if not is_javascript:
env.Append(LINKFLAGS=["-rdynamic"])
mono_lib_file = os.path.join(mono_lib_path, "lib" + mono_lib + ".a")
if is_apple:
if is_macos:
env.Append(LINKFLAGS=["-Wl,-force_load," + mono_lib_file])
else:
arch = env["arch"]
def copy_mono_lib(libname_wo_ext):
if is_ios_sim:
copy_file(
mono_lib_path,
"#bin",
libname_wo_ext + ".a",
"%s.iphone.%s.simulator.a" % (libname_wo_ext, arch),
)
else:
copy_file(
mono_lib_path,
"#bin",
libname_wo_ext + ".a",
"%s.iphone.%s.a" % (libname_wo_ext, arch),
)
# Copy Mono libraries to the output folder. These are meant to be bundled with
# the export templates and added to the Xcode project when exporting a game.
copy_mono_lib("lib" + mono_lib)
copy_mono_lib("libmono-native")
copy_mono_lib("libmono-profiler-log")
if not is_ios_sim:
copy_mono_lib("libmono-ee-interp")
copy_mono_lib("libmono-icall-table")
copy_mono_lib("libmono-ilgen")
else:
assert is_desktop(env["platform"]) or is_android or is_javascript
env.Append(LINKFLAGS=["-Wl,-whole-archive", mono_lib_file, "-Wl,-no-whole-archive"])
if is_javascript:
env.Append(LIBS=["mono-icall-table", "mono-native", "mono-ilgen", "mono-ee-interp"])
wasm_src_dir = os.path.join(mono_root, "src")
if not os.path.isdir(wasm_src_dir):
raise RuntimeError("Could not find mono wasm src directory")
# Ideally this should be defined only for 'driver.c', but I can't fight scons for another 2 hours
env_mono.Append(CPPDEFINES=["CORE_BINDINGS"])
env_mono.add_source_files(
env.modules_sources,
[
os.path.join(wasm_src_dir, "driver.c"),
os.path.join(wasm_src_dir, "zlib-helper.c"),
os.path.join(wasm_src_dir, "corebindings.c"),
],
)
env.Append(
LINKFLAGS=[
"--js-library",
os.path.join(wasm_src_dir, "library_mono.js"),
"--js-library",
os.path.join(wasm_src_dir, "binding_support.js"),
"--js-library",
os.path.join(wasm_src_dir, "dotnet_support.js"),
]
)
else:
env.Append(LIBS=[mono_lib])
if is_macos:
env.Append(LIBS=["iconv", "pthread"])
elif is_android:
pass # Nothing
elif is_ios:
pass # Nothing, linking is delegated to the exported Xcode project
elif is_javascript:
env.Append(LIBS=["m", "rt", "dl", "pthread"])
else:
env.Append(LIBS=["m", "rt", "dl", "pthread"])
if not mono_static:
mono_so_file = find_file_in_dir(
mono_lib_path, mono_lib_names, prefixes=["lib"], extensions=[sharedlib_ext]
)
if not mono_so_file:
raise RuntimeError("Could not find mono shared library in: " + mono_lib_path)
else:
assert not mono_static
# TODO: Add option to force using pkg-config
print("Mono root directory not found. Using pkg-config instead")
env.ParseConfig("pkg-config monosgen-2 --libs")
env_mono.ParseConfig("pkg-config monosgen-2 --cflags")
tmpenv = Environment()
tmpenv.AppendENVPath("PKG_CONFIG_PATH", os.getenv("PKG_CONFIG_PATH"))
tmpenv.ParseConfig("pkg-config monosgen-2 --libs-only-L")
for hint_dir in tmpenv["LIBPATH"]:
file_found = find_file_in_dir(hint_dir, mono_lib_names, prefixes=["lib"], extensions=[sharedlib_ext])
if file_found:
mono_lib_path = hint_dir
mono_so_file = file_found
break
if not mono_so_file:
raise RuntimeError("Could not find mono shared library in: " + str(tmpenv["LIBPATH"]))
if not mono_static:
libs_output_dir = get_android_out_dir(env) if is_android else "#bin"
copy_file(mono_lib_path, libs_output_dir, mono_so_file)
if not tools_enabled:
if is_desktop(env["platform"]):
if not mono_root:
mono_root = (
subprocess.check_output(["pkg-config", "mono-2", "--variable=prefix"]).decode("utf8").strip()
)
make_template_dir(env, mono_root)
elif is_android:
# Compress Android Mono Config
from . import make_android_mono_config
module_dir = os.getcwd()
config_file_path = os.path.join(module_dir, "build_scripts", "mono_android_config.xml")
make_android_mono_config.generate_compressed_config(config_file_path, "mono_gd/")
# Copy the required shared libraries
copy_mono_shared_libs(env, mono_root, None)
elif is_javascript:
pass # No data directory for this platform
elif is_ios:
pass # No data directory for this platform
if copy_mono_root:
if not mono_root:
mono_root = subprocess.check_output(["pkg-config", "mono-2", "--variable=prefix"]).decode("utf8").strip()
if tools_enabled:
# Only supported for editor builds.
copy_mono_root_files(env, mono_root, mono_bcl)
def make_template_dir(env, mono_root):
from shutil import rmtree
platform = env["platform"]
target = env["target"]
template_dir_name = ""
assert is_desktop(platform)
template_dir_name = "data.mono.%s.%s.%s" % (platform, env["bits"], target)
output_dir = Dir("#bin").abspath
template_dir = os.path.join(output_dir, template_dir_name)
template_mono_root_dir = os.path.join(template_dir, "Mono")
if os.path.isdir(template_mono_root_dir):
rmtree(template_mono_root_dir) # Clean first
# Copy etc/mono/
template_mono_config_dir = os.path.join(template_mono_root_dir, "etc", "mono")
copy_mono_etc_dir(mono_root, template_mono_config_dir, platform)
# Copy the required shared libraries
copy_mono_shared_libs(env, mono_root, template_mono_root_dir)
def copy_mono_root_files(env, mono_root, mono_bcl):
from glob import glob
from shutil import copy
from shutil import rmtree
if not mono_root:
raise RuntimeError("Mono installation directory not found")
output_dir = Dir("#bin").abspath
editor_mono_root_dir = os.path.join(output_dir, "GodotSharp", "Mono")
if os.path.isdir(editor_mono_root_dir):
rmtree(editor_mono_root_dir) # Clean first
# Copy etc/mono/
editor_mono_config_dir = os.path.join(editor_mono_root_dir, "etc", "mono")
copy_mono_etc_dir(mono_root, editor_mono_config_dir, env["platform"])
# Copy the required shared libraries
copy_mono_shared_libs(env, mono_root, editor_mono_root_dir)
# Copy framework assemblies
mono_framework_dir = mono_bcl or os.path.join(mono_root, "lib", "mono", "4.5")
mono_framework_facades_dir = os.path.join(mono_framework_dir, "Facades")
editor_mono_framework_dir = os.path.join(editor_mono_root_dir, "lib", "mono", "4.5")
editor_mono_framework_facades_dir = os.path.join(editor_mono_framework_dir, "Facades")
if not os.path.isdir(editor_mono_framework_dir):
os.makedirs(editor_mono_framework_dir)
if not os.path.isdir(editor_mono_framework_facades_dir):
os.makedirs(editor_mono_framework_facades_dir)
for assembly in glob(os.path.join(mono_framework_dir, "*.dll")):
copy(assembly, editor_mono_framework_dir)
for assembly in glob(os.path.join(mono_framework_facades_dir, "*.dll")):
copy(assembly, editor_mono_framework_facades_dir)
def copy_mono_etc_dir(mono_root, target_mono_config_dir, platform):
from distutils.dir_util import copy_tree
from glob import glob
from shutil import copy
if not os.path.isdir(target_mono_config_dir):
os.makedirs(target_mono_config_dir)
mono_etc_dir = os.path.join(mono_root, "etc", "mono")
if not os.path.isdir(mono_etc_dir):
mono_etc_dir = ""
etc_hint_dirs = []
if platform != "windows":
etc_hint_dirs += ["/etc/mono", "/usr/local/etc/mono"]
if "MONO_CFG_DIR" in os.environ:
etc_hint_dirs += [os.path.join(os.environ["MONO_CFG_DIR"], "mono")]
for etc_hint_dir in etc_hint_dirs:
if os.path.isdir(etc_hint_dir):
mono_etc_dir = etc_hint_dir
break
if not mono_etc_dir:
raise RuntimeError("Mono installation etc directory not found")
copy_tree(os.path.join(mono_etc_dir, "2.0"), os.path.join(target_mono_config_dir, "2.0"))
copy_tree(os.path.join(mono_etc_dir, "4.0"), os.path.join(target_mono_config_dir, "4.0"))
copy_tree(os.path.join(mono_etc_dir, "4.5"), os.path.join(target_mono_config_dir, "4.5"))
if os.path.isdir(os.path.join(mono_etc_dir, "mconfig")):
copy_tree(os.path.join(mono_etc_dir, "mconfig"), os.path.join(target_mono_config_dir, "mconfig"))
for file in glob(os.path.join(mono_etc_dir, "*")):
if os.path.isfile(file):
copy(file, target_mono_config_dir)
def copy_mono_shared_libs(env, mono_root, target_mono_root_dir):
from shutil import copy
def copy_if_exists(src, dst):
if os.path.isfile(src):
copy(src, dst)
platform = env["platform"]
if platform == "windows":
src_mono_bin_dir = os.path.join(mono_root, "bin")
target_mono_bin_dir = os.path.join(target_mono_root_dir, "bin")
if not os.path.isdir(target_mono_bin_dir):
os.makedirs(target_mono_bin_dir)
mono_posix_helper_file = find_file_in_dir(
src_mono_bin_dir, ["MonoPosixHelper"], prefixes=["", "lib"], extensions=[".dll"]
)
copy(
os.path.join(src_mono_bin_dir, mono_posix_helper_file),
os.path.join(target_mono_bin_dir, "MonoPosixHelper.dll"),
)
# For newer versions
btls_dll_path = os.path.join(src_mono_bin_dir, "libmono-btls-shared.dll")
if os.path.isfile(btls_dll_path):
copy(btls_dll_path, target_mono_bin_dir)
else:
target_mono_lib_dir = (
get_android_out_dir(env) if platform == "android" else os.path.join(target_mono_root_dir, "lib")
)
if not os.path.isdir(target_mono_lib_dir):
os.makedirs(target_mono_lib_dir)
src_mono_lib_dir = os.path.join(mono_root, "lib")
lib_file_names = []
if platform == "osx":
lib_file_names = [lib_name + ".dylib" for lib_name in ["libmono-btls-shared", "libMonoPosixHelper"]]
if os.path.isfile(os.path.join(src_mono_lib_dir, "libmono-native-compat.dylib")):
lib_file_names += ["libmono-native-compat.dylib"]
else:
lib_file_names += ["libmono-native.dylib"]
elif is_unix_like(platform):
lib_file_names = [
lib_name + ".so"
for lib_name in [
"libmono-btls-shared",
"libmono-ee-interp",
"libmono-native",
"libMonoPosixHelper",
"libmono-profiler-aot",
"libmono-profiler-coverage",
"libmono-profiler-log",
"libMonoSupportW",
]
]
for lib_file_name in lib_file_names:
copy_if_exists(os.path.join(src_mono_lib_dir, lib_file_name), target_mono_lib_dir)
def pkgconfig_try_find_mono_root(mono_lib_names, sharedlib_ext):
tmpenv = Environment()
tmpenv.AppendENVPath("PKG_CONFIG_PATH", os.getenv("PKG_CONFIG_PATH"))
tmpenv.ParseConfig("pkg-config monosgen-2 --libs-only-L")
for hint_dir in tmpenv["LIBPATH"]:
name_found = find_name_in_dir_files(hint_dir, mono_lib_names, prefixes=["lib"], extensions=[sharedlib_ext])
if name_found and os.path.isdir(os.path.join(hint_dir, "..", "include", "mono-2.0")):
return os.path.join(hint_dir, "..")
return ""

View File

@ -0,0 +1,119 @@
import os
import platform
from compat import decode_utf8
if os.name == "nt":
import sys
if sys.version_info < (3,):
import _winreg as winreg
else:
import winreg
def _reg_open_key(key, subkey):
try:
return winreg.OpenKey(key, subkey)
except (WindowsError, OSError):
if platform.architecture()[0] == "32bit":
bitness_sam = winreg.KEY_WOW64_64KEY
else:
bitness_sam = winreg.KEY_WOW64_32KEY
return winreg.OpenKey(key, subkey, 0, winreg.KEY_READ | bitness_sam)
def _reg_open_key_bits(key, subkey, bits):
sam = winreg.KEY_READ
if platform.architecture()[0] == "32bit":
if bits == "64":
# Force 32bit process to search in 64bit registry
sam |= winreg.KEY_WOW64_64KEY
else:
if bits == "32":
# Force 64bit process to search in 32bit registry
sam |= winreg.KEY_WOW64_32KEY
return winreg.OpenKey(key, subkey, 0, sam)
def _find_mono_in_reg(subkey, bits):
try:
with _reg_open_key_bits(winreg.HKEY_LOCAL_MACHINE, subkey, bits) as hKey:
value = winreg.QueryValueEx(hKey, "SdkInstallRoot")[0]
return value
except (WindowsError, OSError):
return None
def _find_mono_in_reg_old(subkey, bits):
try:
with _reg_open_key_bits(winreg.HKEY_LOCAL_MACHINE, subkey, bits) as hKey:
default_clr = winreg.QueryValueEx(hKey, "DefaultCLR")[0]
if default_clr:
return _find_mono_in_reg(subkey + "\\" + default_clr, bits)
return None
except (WindowsError, EnvironmentError):
return None
def find_mono_root_dir(bits):
root_dir = _find_mono_in_reg(r"SOFTWARE\Mono", bits)
if root_dir is not None:
return str(root_dir)
root_dir = _find_mono_in_reg_old(r"SOFTWARE\Novell\Mono", bits)
if root_dir is not None:
return str(root_dir)
return ""
def find_msbuild_tools_path_reg():
import subprocess
vswhere = os.getenv("PROGRAMFILES(X86)")
if not vswhere:
vswhere = os.getenv("PROGRAMFILES")
vswhere += r"\Microsoft Visual Studio\Installer\vswhere.exe"
vswhere_args = ["-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"]
try:
lines = subprocess.check_output([vswhere] + vswhere_args).splitlines()
for line in lines:
parts = decode_utf8(line).split(":", 1)
if len(parts) < 2 or parts[0] != "installationPath":
continue
val = parts[1].strip()
if not val:
raise ValueError("Value of `installationPath` entry is empty")
# Since VS2019, the directory is simply named "Current"
msbuild_dir = os.path.join(val, "MSBuild\\Current\\Bin")
if os.path.isdir(msbuild_dir):
return msbuild_dir
# Directory name "15.0" is used in VS 2017
return os.path.join(val, "MSBuild\\15.0\\Bin")
raise ValueError("Cannot find `installationPath` entry")
except ValueError as e:
print("Error reading output from vswhere: " + e.message)
except subprocess.CalledProcessError as e:
print(e.output)
except OSError as e:
print(e)
# Try to find 14.0 in the Registry
try:
subkey = r"SOFTWARE\Microsoft\MSBuild\ToolsVersions\14.0"
with _reg_open_key(winreg.HKEY_LOCAL_MACHINE, subkey) as hKey:
value = winreg.QueryValueEx(hKey, "MSBuildToolsPath")[0]
return value
except (WindowsError, OSError):
return ""

View File

@ -0,0 +1,148 @@
import os
verbose = False
def find_dotnet_cli():
import os.path
if os.name == "nt":
windows_exts = os.environ["PATHEXT"]
windows_exts = windows_exts.split(os.pathsep) if windows_exts else []
for hint_dir in os.environ["PATH"].split(os.pathsep):
hint_dir = hint_dir.strip('"')
hint_path = os.path.join(hint_dir, "dotnet")
if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
return hint_path
if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
return hint_path + ".exe"
else:
for hint_dir in os.environ["PATH"].split(os.pathsep):
hint_dir = hint_dir.strip('"')
hint_path = os.path.join(hint_dir, "dotnet")
if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
return hint_path
def find_msbuild_unix():
import os.path
import sys
hint_dirs = []
if sys.platform == "darwin":
hint_dirs[:0] = [
"/Library/Frameworks/Mono.framework/Versions/Current/bin",
"/usr/local/var/homebrew/linked/mono/bin",
]
for hint_dir in hint_dirs:
hint_path = os.path.join(hint_dir, "msbuild")
if os.path.isfile(hint_path):
return hint_path
elif os.path.isfile(hint_path + ".exe"):
return hint_path + ".exe"
for hint_dir in os.environ["PATH"].split(os.pathsep):
hint_dir = hint_dir.strip('"')
hint_path = os.path.join(hint_dir, "msbuild")
if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
return hint_path
if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
return hint_path + ".exe"
return None
def find_msbuild_windows(env):
from .mono_reg_utils import find_mono_root_dir, find_msbuild_tools_path_reg
mono_root = env["mono_prefix"] or find_mono_root_dir(env["bits"])
if not mono_root:
raise RuntimeError("Cannot find mono root directory")
mono_bin_dir = os.path.join(mono_root, "bin")
msbuild_mono = os.path.join(mono_bin_dir, "msbuild.bat")
msbuild_tools_path = find_msbuild_tools_path_reg()
if msbuild_tools_path:
return (os.path.join(msbuild_tools_path, "MSBuild.exe"), {})
if os.path.isfile(msbuild_mono):
# The (Csc/Vbc/Fsc)ToolExe environment variables are required when
# building with Mono's MSBuild. They must point to the batch files
# in Mono's bin directory to make sure they are executed with Mono.
mono_msbuild_env = {
"CscToolExe": os.path.join(mono_bin_dir, "csc.bat"),
"VbcToolExe": os.path.join(mono_bin_dir, "vbc.bat"),
"FscToolExe": os.path.join(mono_bin_dir, "fsharpc.bat"),
}
return (msbuild_mono, mono_msbuild_env)
return None
def run_command(command, args, env_override=None, name=None):
def cmd_args_to_str(cmd_args):
return " ".join([arg if not " " in arg else '"%s"' % arg for arg in cmd_args])
args = [command] + args
if name is None:
name = os.path.basename(command)
if verbose:
print("Running '%s': %s" % (name, cmd_args_to_str(args)))
import subprocess
try:
if env_override is None:
subprocess.check_call(args)
else:
subprocess.check_call(args, env=env_override)
except subprocess.CalledProcessError as e:
raise RuntimeError("'%s' exited with error code: %s" % (name, e.returncode))
def build_solution(env, solution_path, build_config, extra_msbuild_args=[]):
global verbose
verbose = env["verbose"]
msbuild_env = os.environ.copy()
# Needed when running from Developer Command Prompt for VS
if "PLATFORM" in msbuild_env:
del msbuild_env["PLATFORM"]
msbuild_args = []
dotnet_cli = find_dotnet_cli()
if dotnet_cli:
msbuild_path = dotnet_cli
msbuild_args += ["msbuild"] # `dotnet msbuild` command
else:
# Find MSBuild
if os.name == "nt":
msbuild_info = find_msbuild_windows(env)
if msbuild_info is None:
raise RuntimeError("Cannot find MSBuild executable")
msbuild_path = msbuild_info[0]
msbuild_env.update(msbuild_info[1])
else:
msbuild_path = find_msbuild_unix()
if msbuild_path is None:
raise RuntimeError("Cannot find MSBuild executable")
print("MSBuild path: " + msbuild_path)
# Build solution
msbuild_args += [solution_path, "/restore", "/t:Build", "/p:Configuration=" + build_config]
msbuild_args += extra_msbuild_args
run_command(msbuild_path, msbuild_args, env_override=msbuild_env, name="msbuild")

View File

@ -0,0 +1,37 @@
from __future__ import print_function
def supported(result):
return "supported" if result else "not supported"
def check_cxx11_thread_local(conf):
print("Checking for `thread_local` support...", end=" ")
result = conf.TryCompile("thread_local int foo = 0; int main() { return foo; }", ".cpp")
print(supported(result))
return bool(result)
def check_declspec_thread(conf):
print("Checking for `__declspec(thread)` support...", end=" ")
result = conf.TryCompile("__declspec(thread) int foo = 0; int main() { return foo; }", ".cpp")
print(supported(result))
return bool(result)
def check_gcc___thread(conf):
print("Checking for `__thread` support...", end=" ")
result = conf.TryCompile("__thread int foo = 0; int main() { return foo; }", ".cpp")
print(supported(result))
return bool(result)
def configure(conf):
if check_cxx11_thread_local(conf):
conf.env.Append(CPPDEFINES=["HAVE_CXX11_THREAD_LOCAL"])
else:
if conf.env.msvc:
if check_declspec_thread(conf):
conf.env.Append(CPPDEFINES=["HAVE_DECLSPEC_THREAD"])
elif check_gcc___thread(conf):
conf.env.Append(CPPDEFINES=["HAVE_GCC___THREAD"])

247
class_db_api_json.cpp Normal file
View File

@ -0,0 +1,247 @@
/**************************************************************************/
/* class_db_api_json.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "class_db_api_json.h"
#ifdef DEBUG_METHODS_ENABLED
#include "core/io/json.h"
#include "core/os/file_access.h"
#include "core/project_settings.h"
#include "core/version.h"
void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) {
Dictionary classes_dict;
List<StringName> names;
const StringName *k = NULL;
while ((k = ClassDB::classes.next(k))) {
names.push_back(*k);
}
//must be alphabetically sorted for hash to compute
names.sort_custom<StringName::AlphCompare>();
for (List<StringName>::Element *E = names.front(); E; E = E->next()) {
ClassDB::ClassInfo *t = ClassDB::classes.getptr(E->get());
ERR_FAIL_COND(!t);
if (t->api != p_api || !t->exposed)
continue;
Dictionary class_dict;
classes_dict[t->name] = class_dict;
class_dict["inherits"] = t->inherits;
{ //methods
List<StringName> snames;
k = NULL;
while ((k = t->method_map.next(k))) {
String name = k->operator String();
ERR_CONTINUE(name.empty());
if (name[0] == '_')
continue; // Ignore non-virtual methods that start with an underscore
snames.push_back(*k);
}
snames.sort_custom<StringName::AlphCompare>();
Array methods;
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
Dictionary method_dict;
methods.push_back(method_dict);
MethodBind *mb = t->method_map[F->get()];
method_dict["name"] = mb->get_name();
method_dict["argument_count"] = mb->get_argument_count();
method_dict["return_type"] = mb->get_argument_type(-1);
Array arguments;
method_dict["arguments"] = arguments;
for (int i = 0; i < mb->get_argument_count(); i++) {
Dictionary argument_dict;
arguments.push_back(argument_dict);
const PropertyInfo info = mb->get_argument_info(i);
argument_dict["type"] = info.type;
argument_dict["name"] = info.name;
argument_dict["hint"] = info.hint;
argument_dict["hint_string"] = info.hint_string;
}
method_dict["default_argument_count"] = mb->get_default_argument_count();
Array default_arguments;
method_dict["default_arguments"] = default_arguments;
for (int i = 0; i < mb->get_default_argument_count(); i++) {
Dictionary default_argument_dict;
default_arguments.push_back(default_argument_dict);
//hash should not change, i hope for tis
Variant da = mb->get_default_argument(i);
default_argument_dict["value"] = da;
}
method_dict["hint_flags"] = mb->get_hint_flags();
}
if (!methods.empty()) {
class_dict["methods"] = methods;
}
}
{ //constants
List<StringName> snames;
k = NULL;
while ((k = t->constant_map.next(k))) {
snames.push_back(*k);
}
snames.sort_custom<StringName::AlphCompare>();
Array constants;
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
Dictionary constant_dict;
constants.push_back(constant_dict);
constant_dict["name"] = F->get();
constant_dict["value"] = t->constant_map[F->get()];
}
if (!constants.empty()) {
class_dict["constants"] = constants;
}
}
{ //signals
List<StringName> snames;
k = NULL;
while ((k = t->signal_map.next(k))) {
snames.push_back(*k);
}
snames.sort_custom<StringName::AlphCompare>();
Array signals;
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
Dictionary signal_dict;
signals.push_back(signal_dict);
MethodInfo &mi = t->signal_map[F->get()];
signal_dict["name"] = F->get();
Array arguments;
signal_dict["arguments"] = arguments;
for (int i = 0; i < mi.arguments.size(); i++) {
Dictionary argument_dict;
arguments.push_back(argument_dict);
argument_dict["type"] = mi.arguments[i].type;
}
}
if (!signals.empty()) {
class_dict["signals"] = signals;
}
}
{ //properties
List<StringName> snames;
k = NULL;
while ((k = t->property_setget.next(k))) {
snames.push_back(*k);
}
snames.sort_custom<StringName::AlphCompare>();
Array properties;
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
Dictionary property_dict;
properties.push_back(property_dict);
ClassDB::PropertySetGet *psg = t->property_setget.getptr(F->get());
property_dict["name"] = F->get();
property_dict["setter"] = psg->setter;
property_dict["getter"] = psg->getter;
}
if (!properties.empty()) {
class_dict["property_setget"] = properties;
}
}
Array property_list;
//property list
for (List<PropertyInfo>::Element *F = t->property_list.front(); F; F = F->next()) {
Dictionary property_dict;
property_list.push_back(property_dict);
property_dict["name"] = F->get().name;
property_dict["type"] = F->get().type;
property_dict["hint"] = F->get().hint;
property_dict["hint_string"] = F->get().hint_string;
property_dict["usage"] = F->get().usage;
}
if (!property_list.empty()) {
class_dict["property_list"] = property_list;
}
}
FileAccessRef f = FileAccess::open(p_output_file, FileAccess::WRITE);
ERR_FAIL_COND_MSG(!f, "Cannot open file '" + p_output_file + "'.");
f->store_string(JSON::print(classes_dict, /*indent: */ "\t"));
f->close();
print_line(String() + "ClassDB API JSON written to: " + ProjectSettings::get_singleton()->globalize_path(p_output_file));
}
#endif // DEBUG_METHODS_ENABLED

46
class_db_api_json.h Normal file
View File

@ -0,0 +1,46 @@
/**************************************************************************/
/* class_db_api_json.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef CLASS_DB_API_JSON_H
#define CLASS_DB_API_JSON_H
// 'core/method_bind.h' defines DEBUG_METHODS_ENABLED, but it looks like we
// cannot include it here. That's why we include it through 'core/class_db.h'.
#include "core/class_db.h"
#ifdef DEBUG_METHODS_ENABLED
#include "core/ustring.h"
void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api);
#endif // DEBUG_METHODS_ENABLED
#endif // CLASS_DB_API_JSON_H

77
config.py Normal file
View File

@ -0,0 +1,77 @@
supported_platforms = ["windows", "osx", "x11", "server", "android", "haiku", "javascript", "iphone"]
def can_build(env, platform):
return not env["arch"].startswith("rv")
def configure(env):
platform = env["platform"]
if platform not in supported_platforms:
raise RuntimeError("This module does not currently support building for this platform")
env.use_ptrcall = True
env.add_module_version_string("mono")
from SCons.Script import BoolVariable, PathVariable, Variables, Help
default_mono_static = platform in ["iphone", "javascript"]
default_mono_bundles_zlib = platform in ["javascript"]
envvars = Variables()
envvars.Add(
PathVariable(
"mono_prefix",
"Path to the Mono installation directory for the target platform and architecture",
"",
PathVariable.PathAccept,
)
)
envvars.Add(
PathVariable(
"mono_bcl",
"Path to a custom Mono BCL (Base Class Library) directory for the target platform",
"",
PathVariable.PathAccept,
)
)
envvars.Add(BoolVariable("mono_static", "Statically link Mono", default_mono_static))
envvars.Add(BoolVariable("mono_glue", "Build with the Mono glue sources", True))
envvars.Add(BoolVariable("build_cil", "Build C# solutions", True))
envvars.Add(
BoolVariable("copy_mono_root", "Make a copy of the Mono installation directory to bundle with the editor", True)
)
# TODO: It would be great if this could be detected automatically instead
envvars.Add(
BoolVariable(
"mono_bundles_zlib", "Specify if the Mono runtime was built with bundled zlib", default_mono_bundles_zlib
)
)
envvars.Update(env)
Help(envvars.GenerateHelpText(env))
if env["mono_bundles_zlib"]:
# Mono may come with zlib bundled for WASM or on newer version when built with MinGW.
print("This Mono runtime comes with zlib bundled. Disabling 'builtin_zlib'...")
env["builtin_zlib"] = False
thirdparty_zlib_dir = "#thirdparty/zlib/"
env.Prepend(CPPPATH=[thirdparty_zlib_dir])
def get_doc_classes():
return [
"CSharpScript",
"GodotSharp",
]
def get_doc_path():
return "doc_classes"
def is_enabled():
# The module is disabled by default. Use module_mono_enabled=yes to enable it.
return False

3482
csharp_script.cpp Normal file

File diff suppressed because it is too large Load Diff

496
csharp_script.h Normal file
View File

@ -0,0 +1,496 @@
/**************************************************************************/
/* csharp_script.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef CSHARP_SCRIPT_H
#define CSHARP_SCRIPT_H
#include "core/io/resource_loader.h"
#include "core/io/resource_saver.h"
#include "core/script_language.h"
#include "core/self_list.h"
#include "mono_gc_handle.h"
#include "mono_gd/gd_mono.h"
#include "mono_gd/gd_mono_header.h"
#include "mono_gd/gd_mono_internals.h"
#ifdef TOOLS_ENABLED
#include "editor/editor_plugin.h"
#endif
class CSharpScript;
class CSharpInstance;
class CSharpLanguage;
#ifdef NO_SAFE_CAST
template <typename TScriptInstance, typename TScriptLanguage>
TScriptInstance *cast_script_instance(ScriptInstance *p_inst) {
if (!p_inst)
return NULL;
return p_inst->get_language() == TScriptLanguage::get_singleton() ? static_cast<TScriptInstance *>(p_inst) : NULL;
}
#else
template <typename TScriptInstance, typename TScriptLanguage>
TScriptInstance *cast_script_instance(ScriptInstance *p_inst) {
return dynamic_cast<TScriptInstance *>(p_inst);
}
#endif
#define CAST_CSHARP_INSTANCE(m_inst) (cast_script_instance<CSharpInstance, CSharpLanguage>(m_inst))
class CSharpScript : public Script {
GDCLASS(CSharpScript, Script);
friend class CSharpInstance;
friend class CSharpLanguage;
friend struct CSharpScriptDepSort;
bool tool;
bool valid;
bool reload_invalidated;
bool builtin;
GDMonoClass *base;
GDMonoClass *native;
GDMonoClass *script_class;
Ref<CSharpScript> base_cache; // TODO what's this for?
Set<Object *> instances;
#ifdef GD_MONO_HOT_RELOAD
struct StateBackup {
// TODO
// Replace with buffer containing the serialized state of managed scripts.
// Keep variant state backup to use only with script instance placeholders.
List<Pair<StringName, Variant>> properties;
};
Set<ObjectID> pending_reload_instances;
Map<ObjectID, StateBackup> pending_reload_state;
StringName tied_class_name_for_reload;
StringName tied_class_namespace_for_reload;
#endif
String source;
StringName name;
SelfList<CSharpScript> script_list;
struct Argument {
String name;
Variant::Type type;
};
Map<StringName, Vector<Argument>> _signals;
bool signals_invalidated;
#ifdef TOOLS_ENABLED
List<PropertyInfo> exported_members_cache; // members_cache
Map<StringName, Variant> exported_members_defval_cache; // member_default_values_cache
Set<PlaceHolderScriptInstance *> placeholders;
bool source_changed_cache;
bool placeholder_fallback_enabled;
bool exports_invalidated;
void _update_exports_values(Map<StringName, Variant> &values, List<PropertyInfo> &propnames);
void _update_member_info_no_exports();
virtual void _placeholder_erased(PlaceHolderScriptInstance *p_placeholder);
#endif
#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED)
Set<StringName> exported_members_names;
#endif
OrderedHashMap<StringName, PropertyInfo> member_info;
void _clear();
void load_script_signals(GDMonoClass *p_class, GDMonoClass *p_native_class);
bool _get_signal(GDMonoClass *p_class, GDMonoClass *p_delegate, Vector<Argument> &params);
bool _update_exports(PlaceHolderScriptInstance *p_instance_to_update = nullptr);
bool _get_member_export(IMonoClassMember *p_member, bool p_inspect_export, PropertyInfo &r_prop_info, bool &r_exported);
#ifdef TOOLS_ENABLED
static int _try_get_member_export_hint(IMonoClassMember *p_member, ManagedType p_type, Variant::Type p_variant_type, bool p_allow_generics, PropertyHint &r_hint, String &r_hint_string);
#endif
CSharpInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_isref, Variant::CallError &r_error);
Variant _new(const Variant **p_args, int p_argcount, Variant::CallError &r_error);
// Do not use unless you know what you are doing
friend void GDMonoInternals::tie_managed_to_unmanaged(MonoObject *, Object *);
static Ref<CSharpScript> create_for_managed_type(GDMonoClass *p_class, GDMonoClass *p_native);
static void initialize_for_managed_type(Ref<CSharpScript> p_script, GDMonoClass *p_class, GDMonoClass *p_native);
protected:
static void _bind_methods();
Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Variant::CallError &r_error);
virtual void _resource_path_changed();
bool _get(const StringName &p_name, Variant &r_ret) const;
bool _set(const StringName &p_name, const Variant &p_value);
void _get_property_list(List<PropertyInfo> *p_properties) const;
public:
virtual bool can_instance() const;
virtual StringName get_instance_base_type() const;
virtual ScriptInstance *instance_create(Object *p_this);
virtual PlaceHolderScriptInstance *placeholder_instance_create(Object *p_this);
virtual bool instance_has(const Object *p_this) const;
virtual bool has_source_code() const;
virtual String get_source_code() const;
virtual void set_source_code(const String &p_code);
virtual Error reload(bool p_keep_state = false);
virtual bool has_script_signal(const StringName &p_signal) const;
virtual void get_script_signal_list(List<MethodInfo> *r_signals) const;
virtual bool get_property_default_value(const StringName &p_property, Variant &r_value) const;
virtual void get_script_property_list(List<PropertyInfo> *p_list) const;
virtual void update_exports();
virtual void get_members(Set<StringName> *p_members);
virtual bool is_tool() const { return tool; }
virtual bool is_valid() const { return valid; }
bool inherits_script(const Ref<Script> &p_script) const;
virtual Ref<Script> get_base_script() const;
virtual ScriptLanguage *get_language() const;
virtual void get_script_method_list(List<MethodInfo> *p_list) const;
bool has_method(const StringName &p_method) const;
MethodInfo get_method_info(const StringName &p_method) const;
virtual int get_member_line(const StringName &p_member) const;
#ifdef TOOLS_ENABLED
virtual bool is_placeholder_fallback_enabled() const { return placeholder_fallback_enabled; }
#endif
Error load_source_code(const String &p_path);
StringName get_script_name() const;
CSharpScript();
~CSharpScript();
};
class CSharpInstance : public ScriptInstance {
friend class CSharpScript;
friend class CSharpLanguage;
Object *owner;
bool base_ref;
bool ref_dying;
bool unsafe_referenced;
bool predelete_notified;
bool destructing_script_instance;
Ref<CSharpScript> script;
Ref<MonoGCHandle> gchandle;
bool _reference_owner_unsafe();
/*
* If true is returned, the caller must memdelete the script instance's owner.
*/
bool _unreference_owner_unsafe();
/*
* If NULL is returned, the caller must destroy the script instance by removing it from its owner.
*/
MonoObject *_internal_new_managed();
// Do not use unless you know what you are doing
friend void GDMonoInternals::tie_managed_to_unmanaged(MonoObject *, Object *);
static CSharpInstance *create_for_managed_type(Object *p_owner, CSharpScript *p_script, const Ref<MonoGCHandle> &p_gchandle);
void _call_multilevel(MonoObject *p_mono_object, const StringName &p_method, const Variant **p_args, int p_argcount);
MultiplayerAPI::RPCMode _member_get_rpc_mode(IMonoClassMember *p_member) const;
void get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state);
public:
MonoObject *get_mono_object() const;
_FORCE_INLINE_ bool is_destructing_script_instance() { return destructing_script_instance; }
virtual Object *get_owner();
virtual bool set(const StringName &p_name, const Variant &p_value);
virtual bool get(const StringName &p_name, Variant &r_ret) const;
virtual void get_property_list(List<PropertyInfo> *p_properties) const;
virtual Variant::Type get_property_type(const StringName &p_name, bool *r_is_valid) const;
virtual void get_method_list(List<MethodInfo> *p_list) const;
virtual bool has_method(const StringName &p_method) const;
virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Variant::CallError &r_error);
virtual void call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount);
virtual void call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount);
void mono_object_disposed(MonoObject *p_obj);
/*
* If 'r_delete_owner' is set to true, the caller must memdelete the script instance's owner. Otherwise, if
* 'r_remove_script_instance' is set to true, the caller must destroy the script instance by removing it from its owner.
*/
void mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance);
virtual void refcount_incremented();
virtual bool refcount_decremented();
virtual MultiplayerAPI::RPCMode get_rpc_mode(const StringName &p_method) const;
virtual MultiplayerAPI::RPCMode get_rset_mode(const StringName &p_variable) const;
virtual void notification(int p_notification);
void _call_notification(int p_notification);
virtual String to_string(bool *r_valid);
virtual Ref<Script> get_script() const;
virtual ScriptLanguage *get_language();
CSharpInstance();
~CSharpInstance();
};
struct CSharpScriptBinding {
bool inited;
StringName type_name;
GDMonoClass *wrapper_class;
Ref<MonoGCHandle> gchandle;
Object *owner;
};
class CSharpLanguage : public ScriptLanguage {
friend class CSharpScript;
friend class CSharpInstance;
static CSharpLanguage *singleton;
bool finalizing;
GDMono *gdmono;
SelfList<CSharpScript>::List script_list;
Mutex script_instances_mutex;
Mutex script_gchandle_release_mutex;
Mutex language_bind_mutex;
Map<Object *, CSharpScriptBinding> script_bindings;
#ifdef DEBUG_ENABLED
// List of unsafe object references
Map<ObjectID, int> unsafe_object_references;
Mutex unsafe_object_references_lock;
#endif
struct StringNameCache {
StringName _signal_callback;
StringName _set;
StringName _get;
StringName _get_property_list;
StringName _notification;
StringName _script_source;
StringName dotctor; // .ctor
StringName on_before_serialize; // OnBeforeSerialize
StringName on_after_deserialize; // OnAfterDeserialize
StringNameCache();
};
int lang_idx;
Dictionary scripts_metadata;
bool scripts_metadata_invalidated;
// For debug_break and debug_break_parse
int _debug_parse_err_line;
String _debug_parse_err_file;
String _debug_error;
void _load_scripts_metadata();
friend class GDMono;
void _on_scripts_domain_unloaded();
#ifdef TOOLS_ENABLED
EditorPlugin *godotsharp_editor;
static void _editor_init_callback();
#endif
public:
StringNameCache string_names;
Mutex &get_language_bind_mutex() { return language_bind_mutex; }
_FORCE_INLINE_ int get_language_index() { return lang_idx; }
void set_language_index(int p_idx);
_FORCE_INLINE_ const StringNameCache &get_string_names() { return string_names; }
_FORCE_INLINE_ static CSharpLanguage *get_singleton() { return singleton; }
#ifdef TOOLS_ENABLED
_FORCE_INLINE_ EditorPlugin *get_godotsharp_editor() const { return godotsharp_editor; }
#endif
static void release_script_gchandle(Ref<MonoGCHandle> &p_gchandle);
static void release_script_gchandle(MonoObject *p_expected_obj, Ref<MonoGCHandle> &p_gchandle);
bool debug_break(const String &p_error, bool p_allow_continue = true);
bool debug_break_parse(const String &p_file, int p_line, const String &p_error);
#ifdef GD_MONO_HOT_RELOAD
bool is_assembly_reloading_needed();
void reload_assemblies(bool p_soft_reload);
#endif
_FORCE_INLINE_ Dictionary get_scripts_metadata_or_nothing() {
return scripts_metadata_invalidated ? Dictionary() : scripts_metadata;
}
_FORCE_INLINE_ const Dictionary &get_scripts_metadata() {
if (scripts_metadata_invalidated)
_load_scripts_metadata();
return scripts_metadata;
}
virtual String get_name() const;
/* LANGUAGE FUNCTIONS */
virtual String get_type() const;
virtual String get_extension() const;
virtual Error execute_file(const String &p_path);
virtual void init();
virtual void finish();
/* EDITOR FUNCTIONS */
virtual void get_reserved_words(List<String> *p_words) const;
virtual bool is_control_flow_keyword(String p_keyword) const;
virtual void get_comment_delimiters(List<String> *p_delimiters) const;
virtual void get_string_delimiters(List<String> *p_delimiters) const;
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
virtual bool is_using_templates();
virtual void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script);
/* TODO */ virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const { return true; }
virtual String validate_path(const String &p_path) const;
virtual Script *create_script() const;
virtual bool has_named_classes() const;
virtual bool supports_builtin_mode() const;
/* TODO? */ virtual int find_function(const String &p_function, const String &p_code) const { return -1; }
virtual String make_function(const String &p_class, const String &p_name, const PoolStringArray &p_args) const;
virtual String _get_indentation() const;
/* TODO? */ virtual void auto_indent_code(String &p_code, int p_from_line, int p_to_line) const {}
/* TODO */ virtual void add_global_constant(const StringName &p_variable, const Variant &p_value) {}
/* DEBUGGER FUNCTIONS */
virtual String debug_get_error() const;
virtual int debug_get_stack_level_count() const;
virtual int debug_get_stack_level_line(int p_level) const;
virtual String debug_get_stack_level_function(int p_level) const;
virtual String debug_get_stack_level_source(int p_level) const;
/* TODO */ virtual void debug_get_stack_level_locals(int p_level, List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) {}
/* TODO */ virtual void debug_get_stack_level_members(int p_level, List<String> *p_members, List<Variant> *p_values, int p_max_subitems, int p_max_depth) {}
/* TODO */ virtual void debug_get_globals(List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) {}
/* TODO */ virtual String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems, int p_max_depth) { return ""; }
virtual Vector<StackInfo> debug_get_current_stack_info();
/* PROFILING FUNCTIONS */
/* TODO */ virtual void profiling_start() {}
/* TODO */ virtual void profiling_stop() {}
/* TODO */ virtual int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) { return 0; }
/* TODO */ virtual int profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_info_max) { return 0; }
virtual void frame();
/* TODO? */ virtual void get_public_functions(List<MethodInfo> *p_functions) const {}
/* TODO? */ virtual void get_public_constants(List<Pair<String, Variant>> *p_constants) const {}
virtual void reload_all_scripts();
virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload);
/* LOADER FUNCTIONS */
virtual void get_recognized_extensions(List<String> *p_extensions) const;
#ifdef TOOLS_ENABLED
virtual Error open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col);
virtual bool overrides_external_editor();
#endif
/* THREAD ATTACHING */
virtual void thread_enter();
virtual void thread_exit();
// Don't use these. I'm watching you
virtual void *alloc_instance_binding_data(Object *p_object);
virtual void free_instance_binding_data(void *p_data);
virtual void refcount_incremented_instance_binding(Object *p_object);
virtual bool refcount_decremented_instance_binding(Object *p_object);
Map<Object *, CSharpScriptBinding>::Element *insert_script_binding(Object *p_object, const CSharpScriptBinding &p_script_binding);
bool setup_csharp_script_binding(CSharpScriptBinding &r_script_binding, Object *p_object);
#ifdef DEBUG_ENABLED
Vector<StackInfo> stack_trace_get_info(MonoObject *p_stack_trace);
#endif
void post_unsafe_reference(Object *p_obj);
void pre_unsafe_unreference(Object *p_obj);
CSharpLanguage();
~CSharpLanguage();
};
class ResourceFormatLoaderCSharpScript : public ResourceFormatLoader {
public:
virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL, bool p_no_subresource_cache = false);
virtual void get_recognized_extensions(List<String> *p_extensions) const;
virtual bool handles_type(const String &p_type) const;
virtual String get_resource_type(const String &p_path) const;
};
class ResourceFormatSaverCSharpScript : public ResourceFormatSaver {
public:
virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0);
virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const;
virtual bool recognize(const RES &p_resource) const;
};
#endif // CSHARP_SCRIPT_H

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="CSharpScript" inherits="Script" version="3.6">
<brief_description>
A script implemented in the C# programming language (Mono-enabled builds only).
</brief_description>
<description>
This class represents a C# script. It is the C# equivalent of the [GDScript] class and is only available in Mono-enabled Godot builds.
See also [GodotSharp].
</description>
<tutorials>
<link>$DOCS_URL/tutorials/scripting/c_sharp/index.html</link>
</tutorials>
<methods>
<method name="new" qualifiers="vararg">
<return type="Variant">
</return>
<description>
Returns a new instance of the script.
</description>
</method>
</methods>
<constants>
</constants>
</class>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="GodotSharp" inherits="Object" category="Core" version="3.6">
<brief_description>
Bridge between Godot and the Mono runtime (Mono-enabled builds only).
</brief_description>
<description>
This class is a bridge between Godot and the Mono runtime. It exposes several low-level operations and is only available in Mono-enabled Godot builds.
See also [CSharpScript].
</description>
<tutorials>
</tutorials>
<methods>
<method name="attach_thread">
<return type="void">
</return>
<description>
Attaches the current thread to the Mono runtime.
</description>
</method>
<method name="detach_thread">
<return type="void">
</return>
<description>
Detaches the current thread from the Mono runtime.
</description>
</method>
<method name="get_domain_id">
<return type="int">
</return>
<description>
Returns the current MonoDomain ID.
[b]Note:[/b] The Mono runtime must be initialized for this method to work (use [method is_runtime_initialized] to check). If the Mono runtime isn't initialized at the time this method is called, the engine will crash.
</description>
</method>
<method name="get_scripts_domain_id">
<return type="int">
</return>
<description>
Returns the scripts MonoDomain's ID. This will be the same MonoDomain ID as [method get_domain_id], unless the scripts domain isn't loaded.
[b]Note:[/b] The Mono runtime must be initialized for this method to work (use [method is_runtime_initialized] to check). If the Mono runtime isn't initialized at the time this method is called, the engine will crash.
</description>
</method>
<method name="is_domain_finalizing_for_unload">
<return type="bool">
</return>
<argument index="0" name="domain_id" type="int">
</argument>
<description>
Returns [code]true[/code] if the domain is being finalized, [code]false[/code] otherwise.
</description>
</method>
<method name="is_runtime_initialized">
<return type="bool">
</return>
<description>
Returns [code]true[/code] if the Mono runtime is initialized, [code]false[/code] otherwise.
</description>
</method>
<method name="is_runtime_shutting_down">
<return type="bool">
</return>
<description>
Returns [code]true[/code] if the Mono runtime is shutting down, [code]false[/code] otherwise.
</description>
</method>
<method name="is_scripts_domain_loaded">
<return type="bool">
</return>
<description>
Returns [code]true[/code] if the scripts domain is loaded, [code]false[/code] otherwise.
</description>
</method>
</methods>
<constants>
</constants>
</class>

View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Godot Engine contributors")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyDescriptionAttribute("MSBuild .NET Sdk for Godot projects.")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("3.3.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("3.3.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Godot.NET.Sdk")]
[assembly: System.Reflection.AssemblyTitleAttribute("Godot.NET.Sdk")]
[assembly: System.Reflection.AssemblyVersionAttribute("3.3.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@ -0,0 +1 @@
7aa02142e9fcaa4587d162d62ea712ca0f3ade57

View File

@ -0,0 +1,3 @@
is_global = true
build_property.RootNamespace = Godot.NET.Sdk
build_property.ProjectDir = /home/relintai/Projects/godot/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/

355
editor/GodotTools/.gitignore vendored Normal file
View File

@ -0,0 +1,355 @@
# 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/

View File

@ -0,0 +1,175 @@
using System;
using System.IO;
using System.Security;
using Microsoft.Build.Framework;
namespace GodotTools.BuildLogger
{
public class GodotBuildLogger : ILogger
{
public static readonly string AssemblyPath = Path.GetFullPath(typeof(GodotBuildLogger).Assembly.Location);
public string Parameters { get; set; }
public LoggerVerbosity Verbosity { get; set; }
private StreamWriter _logStreamWriter;
private StreamWriter _issuesStreamWriter;
private int _indent;
public void Initialize(IEventSource eventSource)
{
if (null == Parameters)
throw new LoggerException("Log directory parameter not specified.");
string[] parameters = Parameters.Split(new[] { ';' });
string logDir = parameters[0];
if (string.IsNullOrEmpty(logDir))
throw new LoggerException("Log directory parameter is empty.");
if (parameters.Length > 1)
throw new LoggerException("Too many parameters passed.");
string logFile = Path.Combine(logDir, "msbuild_log.txt");
string issuesFile = Path.Combine(logDir, "msbuild_issues.csv");
try
{
if (!Directory.Exists(logDir))
Directory.CreateDirectory(logDir);
_logStreamWriter = new StreamWriter(logFile);
_issuesStreamWriter = new StreamWriter(issuesFile);
}
catch (Exception ex)
{
if (ex is UnauthorizedAccessException
|| ex is ArgumentNullException
|| ex is PathTooLongException
|| ex is DirectoryNotFoundException
|| ex is NotSupportedException
|| ex is ArgumentException
|| ex is SecurityException
|| ex is IOException)
{
throw new LoggerException("Failed to create log file: " + ex.Message);
}
// Unexpected failure
throw;
}
eventSource.ProjectStarted += eventSource_ProjectStarted;
eventSource.ProjectFinished += eventSource_ProjectFinished;
eventSource.MessageRaised += eventSource_MessageRaised;
eventSource.WarningRaised += eventSource_WarningRaised;
eventSource.ErrorRaised += eventSource_ErrorRaised;
}
private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
WriteLine(e.Message);
_indent++;
}
private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
_indent--;
WriteLine(e.Message);
}
private void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
{
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}";
if (!string.IsNullOrEmpty(e.ProjectFile))
line += $" [{e.ProjectFile}]";
WriteLine(line);
string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," +
$"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," +
$"{e.ProjectFile?.CsvEscape() ?? string.Empty}";
_issuesStreamWriter.WriteLine(errorLine);
}
private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
{
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}";
if (!string.IsNullOrEmpty(e.ProjectFile))
line += $" [{e.ProjectFile}]";
WriteLine(line);
string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," +
$"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," +
$"{e.ProjectFile?.CsvEscape() ?? string.Empty}";
_issuesStreamWriter.WriteLine(warningLine);
}
private void eventSource_MessageRaised(object sender, BuildMessageEventArgs e)
{
// BuildMessageEventArgs adds Importance to BuildEventArgs
// Let's take account of the verbosity setting we've been passed in deciding whether to log the message
if (e.Importance == MessageImportance.High && IsVerbosityAtLeast(LoggerVerbosity.Minimal)
|| e.Importance == MessageImportance.Normal && IsVerbosityAtLeast(LoggerVerbosity.Normal)
|| e.Importance == MessageImportance.Low && IsVerbosityAtLeast(LoggerVerbosity.Detailed))
{
WriteLineWithSenderAndMessage(string.Empty, e);
}
}
/// <summary>
/// Write a line to the log, adding the SenderName and Message
/// (these parameters are on all MSBuild event argument objects)
/// </summary>
private void WriteLineWithSenderAndMessage(string line, BuildEventArgs e)
{
if (0 == string.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
{
// Well, if the sender name is MSBuild, let's leave it out for prettiness
WriteLine(line + e.Message);
}
else
{
WriteLine(e.SenderName + ": " + line + e.Message);
}
}
private void WriteLine(string line)
{
for (int i = _indent; i > 0; i--)
{
_logStreamWriter.Write("\t");
}
_logStreamWriter.WriteLine(line);
}
public void Shutdown()
{
_logStreamWriter.Close();
_issuesStreamWriter.Close();
}
private bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity)
{
return Verbosity >= checkVerbosity;
}
}
internal static class StringExtensions
{
public static string CsvEscape(this string value, char delimiter = ',')
{
bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1;
if (hasSpecialChar)
return "\"" + value.Replace("\"", "\"\"") + "\"";
return value;
}
}
}

View File

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

View File

@ -0,0 +1,30 @@
using System.IO;
namespace GodotTools.Core
{
public static class FileUtils
{
public static void SaveBackupCopy(string filePath)
{
string backupPathBase = filePath + ".old";
string backupPath = backupPathBase;
const int maxAttempts = 5;
int attempt = 1;
while (File.Exists(backupPath) && attempt <= maxAttempts)
{
backupPath = backupPathBase + "." + (attempt);
attempt++;
}
if (attempt > maxAttempts + 1)
{
// Overwrite the oldest one
backupPath = backupPathBase;
}
File.Copy(filePath, backupPath, overwrite: true);
}
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,38 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace GodotTools.Core
{
public static class ProcessExtensions
{
public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default(CancellationToken))
{
var tcs = new TaskCompletionSource<bool>();
void ProcessExited(object sender, EventArgs e)
{
tcs.TrySetResult(true);
}
process.EnableRaisingEvents = true;
process.Exited += ProcessExited;
try
{
if (process.HasExited)
return;
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
{
await tcs.Task;
}
}
finally
{
process.Exited -= ProcessExited;
}
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
namespace GodotTools.Core
{
public static class StringExtensions
{
private static readonly string _driveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
public static string RelativeToPath(this string path, string dir)
{
// Make sure the directory ends with a path separator
dir = Path.Combine(dir, " ").TrimEnd();
if (Path.DirectorySeparatorChar == '\\')
dir = dir.Replace("/", "\\") + "\\";
var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
// MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
}
public static string NormalizePath(this string path)
{
if (string.IsNullOrEmpty(path))
return path;
bool rooted = path.IsAbsolutePath();
path = path.Replace('\\', '/');
path = path[path.Length - 1] == '/' ? path.Substring(0, path.Length - 1) : path;
string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim();
if (!rooted)
return path;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
string maybeDrive = parts[0];
if (maybeDrive.Length == 2 && maybeDrive[1] == ':')
return path; // Already has drive letter
}
return Path.DirectorySeparatorChar + path;
}
public static bool IsAbsolutePath(this string path)
{
return path.StartsWith("/", StringComparison.Ordinal) ||
path.StartsWith("\\", StringComparison.Ordinal) ||
path.StartsWith(_driveRoot, StringComparison.Ordinal);
}
public static string ToSafeDirName(this string dirName, bool allowDirSeparator = false)
{
var invalidChars = new List<string> {":", "*", "?", "\"", "<", ">", "|"};
if (allowDirSeparator)
{
// Directory separators are allowed, but disallow ".." to avoid going up the filesystem
invalidChars.Add("..");
}
else
{
invalidChars.Add("/");
}
string safeDirName = dirName.Replace("\\", "/").Trim();
foreach (string invalidChar in invalidChars)
safeDirName = safeDirName.Replace(invalidChar, "-");
return safeDirName;
}
}
}

View File

@ -0,0 +1,57 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GodotTools.IdeMessaging.Utils;
namespace GodotTools.IdeMessaging.CLI
{
public class ForwarderMessageHandler : IMessageHandler
{
private readonly StreamWriter outputWriter;
private readonly SemaphoreSlim outputWriteSem = new SemaphoreSlim(1);
public ForwarderMessageHandler(StreamWriter outputWriter)
{
this.outputWriter = outputWriter;
}
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
{
await WriteRequestToOutput(id, content);
return new MessageContent(MessageStatus.RequestNotSupported, "null");
}
private async Task WriteRequestToOutput(string id, MessageContent content)
{
using (await outputWriteSem.UseAsync())
{
await outputWriter.WriteLineAsync("======= Request =======");
await outputWriter.WriteLineAsync(id);
await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
await outputWriter.WriteLineAsync(content.Body);
await outputWriter.WriteLineAsync("=======================");
await outputWriter.FlushAsync();
}
}
public async Task WriteResponseToOutput(string id, MessageContent content)
{
using (await outputWriteSem.UseAsync())
{
await outputWriter.WriteLineAsync("======= Response =======");
await outputWriter.WriteLineAsync(id);
await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
await outputWriter.WriteLineAsync(content.Body);
await outputWriter.WriteLineAsync("========================");
await outputWriter.FlushAsync();
}
}
public async Task WriteLineToOutput(string eventName)
{
using (await outputWriteSem.UseAsync())
await outputWriter.WriteLineAsync($"======= {eventName} =======");
}
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using GodotTools.IdeMessaging.Requests;
using Newtonsoft.Json;
namespace GodotTools.IdeMessaging.CLI
{
internal static class Program
{
private static readonly ILogger Logger = new CustomLogger();
public static int Main(string[] args)
{
try
{
var mainTask = StartAsync(args, Console.OpenStandardInput(), Console.OpenStandardOutput());
mainTask.Wait();
return mainTask.Result;
}
catch (Exception ex)
{
Logger.LogError("Unhandled exception: ", ex);
return 1;
}
}
private static async Task<int> StartAsync(string[] args, Stream inputStream, Stream outputStream)
{
var inputReader = new StreamReader(inputStream, Encoding.UTF8);
var outputWriter = new StreamWriter(outputStream, Encoding.UTF8);
try
{
if (args.Length == 0)
{
Logger.LogError("Expected at least 1 argument");
return 1;
}
string godotProjectDir = args[0];
if (!Directory.Exists(godotProjectDir))
{
Logger.LogError($"The specified Godot project directory does not exist: {godotProjectDir}");
return 1;
}
var forwarder = new ForwarderMessageHandler(outputWriter);
using (var fwdClient = new Client("VisualStudioCode", godotProjectDir, forwarder, Logger))
{
fwdClient.Start();
// ReSharper disable AccessToDisposedClosure
fwdClient.Connected += async () => await forwarder.WriteLineToOutput("Event=Connected");
fwdClient.Disconnected += async () => await forwarder.WriteLineToOutput("Event=Disconnected");
// ReSharper restore AccessToDisposedClosure
// TODO: Await connected with timeout
while (!fwdClient.IsDisposed)
{
string firstLine = await inputReader.ReadLineAsync();
if (firstLine == null || firstLine == "QUIT")
goto ExitMainLoop;
string messageId = firstLine;
string messageArgcLine = await inputReader.ReadLineAsync();
if (messageArgcLine == null)
{
Logger.LogInfo("EOF when expecting argument count");
goto ExitMainLoop;
}
if (!int.TryParse(messageArgcLine, out int messageArgc))
{
Logger.LogError("Received invalid line for argument count: " + firstLine);
continue;
}
var body = new StringBuilder();
for (int i = 0; i < messageArgc; i++)
{
string bodyLine = await inputReader.ReadLineAsync();
if (bodyLine == null)
{
Logger.LogInfo($"EOF when expecting body line #{i + 1}");
goto ExitMainLoop;
}
body.AppendLine(bodyLine);
}
var response = await SendRequest(fwdClient, messageId, new MessageContent(MessageStatus.Ok, body.ToString()));
if (response == null)
{
Logger.LogError($"Failed to write message to the server: {messageId}");
}
else
{
var content = new MessageContent(response.Status, JsonConvert.SerializeObject(response));
await forwarder.WriteResponseToOutput(messageId, content);
}
}
ExitMainLoop:
await forwarder.WriteLineToOutput("Event=Quit");
}
return 0;
}
catch (Exception e)
{
Logger.LogError("Unhandled exception", e);
return 1;
}
}
private static async Task<Response> SendRequest(Client client, string id, MessageContent content)
{
var handlers = new Dictionary<string, Func<Task<Response>>>
{
[PlayRequest.Id] = async () =>
{
var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
return await client.SendRequest<PlayResponse>(request);
},
[DebugPlayRequest.Id] = async () =>
{
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
return await client.SendRequest<DebugPlayResponse>(request);
},
[ReloadScriptsRequest.Id] = async () =>
{
var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
return await client.SendRequest<ReloadScriptsResponse>(request);
},
[CodeCompletionRequest.Id] = async () =>
{
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
return await client.SendRequest<CodeCompletionResponse>(request);
}
};
if (handlers.TryGetValue(id, out var handler))
return await handler();
Console.WriteLine("INVALID REQUEST");
return null;
}
private class CustomLogger : ILogger
{
private static string ThisAppPath => Assembly.GetExecutingAssembly().Location;
private static string ThisAppPathWithoutExtension => Path.ChangeExtension(ThisAppPath, null);
private static readonly string LogPath = $"{ThisAppPathWithoutExtension}.log";
private static StreamWriter NewWriter() => new StreamWriter(LogPath, append: true, encoding: Encoding.UTF8);
private static void Log(StreamWriter writer, string message)
{
writer.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}: {message}");
}
public void LogDebug(string message)
{
using (var writer = NewWriter())
{
Log(writer, "DEBUG: " + message);
}
}
public void LogInfo(string message)
{
using (var writer = NewWriter())
{
Log(writer, "INFO: " + message);
}
}
public void LogWarning(string message)
{
using (var writer = NewWriter())
{
Log(writer, "WARN: " + message);
}
}
public void LogError(string message)
{
using (var writer = NewWriter())
{
Log(writer, "ERROR: " + message);
}
}
public void LogError(string message, Exception e)
{
using (var writer = NewWriter())
{
Log(writer, "EXCEPTION: " + message + '\n' + e);
}
}
}
}
}

View File

@ -0,0 +1,332 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using Newtonsoft.Json;
using System.Threading;
using System.Threading.Tasks;
using GodotTools.IdeMessaging.Requests;
using GodotTools.IdeMessaging.Utils;
namespace GodotTools.IdeMessaging
{
// ReSharper disable once UnusedType.Global
public sealed class Client : IDisposable
{
private readonly ILogger logger;
private readonly string identity;
private string MetaFilePath { get; }
private GodotIdeMetadata godotIdeMetadata;
private readonly FileSystemWatcher fsWatcher;
private readonly IMessageHandler messageHandler;
private Peer peer;
private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);
private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
private readonly Queue<NotifyAwaiter<bool>> clientDisconnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
// ReSharper disable once UnusedMember.Global
public async Task<bool> AwaitConnected()
{
var awaiter = new NotifyAwaiter<bool>();
clientConnectedAwaiters.Enqueue(awaiter);
return await awaiter;
}
// ReSharper disable once UnusedMember.Global
public async Task<bool> AwaitDisconnected()
{
var awaiter = new NotifyAwaiter<bool>();
clientDisconnectedAwaiters.Enqueue(awaiter);
return await awaiter;
}
// ReSharper disable once MemberCanBePrivate.Global
public bool IsDisposed { get; private set; }
// ReSharper disable once MemberCanBePrivate.Global
public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;
// ReSharper disable once EventNeverSubscribedTo.Global
public event Action Connected
{
add
{
if (peer != null && !peer.IsDisposed)
peer.Connected += value;
}
remove
{
if (peer != null && !peer.IsDisposed)
peer.Connected -= value;
}
}
// ReSharper disable once EventNeverSubscribedTo.Global
public event Action Disconnected
{
add
{
if (peer != null && !peer.IsDisposed)
peer.Disconnected += value;
}
remove
{
if (peer != null && !peer.IsDisposed)
peer.Disconnected -= value;
}
}
~Client()
{
Dispose(disposing: false);
}
public async void Dispose()
{
if (IsDisposed)
return;
using (await connectionSem.UseAsync())
{
if (IsDisposed) // lock may not be fair
return;
IsDisposed = true;
}
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
peer?.Dispose();
fsWatcher?.Dispose();
}
}
public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger)
{
this.identity = identity;
this.messageHandler = messageHandler;
this.logger = logger;
string projectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata");
MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
// FileSystemWatcher requires an existing directory
if (!File.Exists(projectMetadataDir))
Directory.CreateDirectory(projectMetadataDir);
fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
}
private async void OnMetaFileChanged(object sender, FileSystemEventArgs e)
{
if (IsDisposed)
return;
using (await connectionSem.UseAsync())
{
if (IsDisposed)
return;
if (!File.Exists(MetaFilePath))
return;
var metadata = ReadMetadataFile();
if (metadata != null && metadata != godotIdeMetadata)
{
godotIdeMetadata = metadata.Value;
_ = Task.Run(ConnectToServer);
}
}
}
private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
{
if (IsDisposed)
return;
if (IsConnected)
{
using (await connectionSem.UseAsync())
peer?.Dispose();
}
// The file may have been re-created
using (await connectionSem.UseAsync())
{
if (IsDisposed)
return;
if (IsConnected || !File.Exists(MetaFilePath))
return;
var metadata = ReadMetadataFile();
if (metadata != null)
{
godotIdeMetadata = metadata.Value;
_ = Task.Run(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 async Task AcceptClient(TcpClient tcpClient)
{
logger.LogDebug("Accept client...");
using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger))
{
// ReSharper disable AccessToDisposedClosure
peer.Connected += () =>
{
logger.LogInfo("Connection open with Ide Client");
while (clientConnectedAwaiters.Count > 0)
clientConnectedAwaiters.Dequeue().SetResult(true);
};
peer.Disconnected += () =>
{
while (clientDisconnectedAwaiters.Count > 0)
clientDisconnectedAwaiters.Dequeue().SetResult(true);
};
// ReSharper restore AccessToDisposedClosure
try
{
if (!await peer.DoHandshake(identity))
{
logger.LogError("Handshake failed");
return;
}
}
catch (Exception e)
{
logger.LogError("Handshake failed with unhandled exception: ", e);
return;
}
await peer.Process();
logger.LogInfo("Connection closed with Ide Client");
}
}
private async Task ConnectToServer()
{
var tcpClient = new TcpClient();
try
{
logger.LogInfo("Connecting to Godot Ide Server");
await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port);
logger.LogInfo("Connection open with Godot Ide Server");
await AcceptClient(tcpClient);
}
catch (SocketException e)
{
if (e.SocketErrorCode == SocketError.ConnectionRefused)
logger.LogError("The connection to the Godot Ide Server was refused");
else
throw;
}
}
// ReSharper disable once UnusedMember.Global
public async void Start()
{
fsWatcher.Changed += OnMetaFileChanged;
fsWatcher.Deleted += OnMetaFileDeleted;
fsWatcher.EnableRaisingEvents = true;
using (await connectionSem.UseAsync())
{
if (IsDisposed)
return;
if (IsConnected)
return;
if (!File.Exists(MetaFilePath))
{
logger.LogInfo("There is no Godot Ide Server running");
return;
}
var metadata = ReadMetadataFile();
if (metadata != null)
{
godotIdeMetadata = metadata.Value;
_ = Task.Run(ConnectToServer);
}
else
{
logger.LogError("Failed to read Godot Ide metadata file");
}
}
}
public async Task<TResponse> SendRequest<TResponse>(Request request)
where TResponse : Response, new()
{
if (!IsConnected)
{
logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
return null;
}
string body = JsonConvert.SerializeObject(request);
return await peer.SendRequest<TResponse>(request.Id, body);
}
public async Task<TResponse> SendRequest<TResponse>(string id, string body)
where TResponse : Response, new()
{
if (!IsConnected)
{
logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
return null;
}
return await peer.SendRequest<TResponse>(id, body);
}
}
}

View File

@ -0,0 +1,44 @@
using System.Text.RegularExpressions;
namespace GodotTools.IdeMessaging
{
public class ClientHandshake : IHandshake
{
private static readonly string ClientHandshakeBase = $"{Peer.ClientHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
private static readonly string ServerHandshakePattern = $@"{Regex.Escape(Peer.ServerHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}";
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
{
identity = null;
var match = Regex.Match(handshake, ServerHandshakePattern);
if (!match.Success)
return false;
if (!uint.TryParse(match.Groups[1].Value, out uint serverMajor) || Peer.ProtocolVersionMajor != serverMajor)
{
logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
return false;
}
if (!uint.TryParse(match.Groups[2].Value, out uint serverMinor) || Peer.ProtocolVersionMinor < serverMinor)
{
logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
return false;
}
if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
{
logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
return false;
}
identity = match.Groups[4].Value;
return true;
}
}
}

View File

@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using GodotTools.IdeMessaging.Requests;
using Newtonsoft.Json;
namespace GodotTools.IdeMessaging
{
// ReSharper disable once UnusedType.Global
public abstract class ClientMessageHandler : IMessageHandler
{
private readonly Dictionary<string, Peer.RequestHandler> requestHandlers;
protected ClientMessageHandler()
{
requestHandlers = InitializeRequestHandlers();
}
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
{
if (!requestHandlers.TryGetValue(id, out var handler))
{
logger.LogError($"Received unknown request: {id}");
return new MessageContent(MessageStatus.RequestNotSupported, "null");
}
try
{
var response = await handler(peer, content);
return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
}
catch (JsonException)
{
logger.LogError($"Received request with invalid body: {id}");
return new MessageContent(MessageStatus.InvalidRequestBody, "null");
}
}
private Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
{
return new Dictionary<string, Peer.RequestHandler>
{
[OpenFileRequest.Id] = async (peer, content) =>
{
var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body);
return await HandleOpenFile(request);
}
};
}
protected abstract Task<Response> HandleOpenFile(OpenFileRequest request);
}
}

View File

@ -0,0 +1,47 @@
namespace GodotTools.IdeMessaging
{
public readonly struct GodotIdeMetadata
{
public int Port { get; }
public string EditorExecutablePath { get; }
public const string DefaultFileName = "ide_messaging_meta.txt";
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,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.2</LangVersion>
<PackageId>GodotTools.IdeMessaging</PackageId>
<Version>1.1.0</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<Authors>Godot Engine contributors</Authors>
<Company />
<PackageTags>godot</PackageTags>
<RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/GodotTools/GodotTools.IdeMessaging</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>
This library enables communication with the Godot Engine editor (the version with .NET support).
It's intended for use in IDEs/editors plugins for a better experience working with Godot C# projects.
A client using this library is only compatible with servers of the same major version and of a lower or equal minor version.
</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace GodotTools.IdeMessaging
{
public interface IHandshake
{
string GetHandshakeLine(string identity);
bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger);
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace GodotTools.IdeMessaging
{
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,9 @@
using System.Threading.Tasks;
namespace GodotTools.IdeMessaging
{
public interface IMessageHandler
{
Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger);
}
}

View File

@ -0,0 +1,52 @@
namespace GodotTools.IdeMessaging
{
public class Message
{
public MessageKind Kind { get; }
public string Id { get; }
public MessageContent Content { get; }
public Message(MessageKind kind, string id, MessageContent content)
{
Kind = kind;
Id = id;
Content = content;
}
public override string ToString()
{
return $"{Kind} | {Id}";
}
}
public enum MessageKind
{
Request,
Response
}
public enum MessageStatus
{
Ok,
RequestNotSupported,
InvalidRequestBody
}
public readonly struct MessageContent
{
public MessageStatus Status { get; }
public string Body { get; }
public MessageContent(string body)
{
Status = MessageStatus.Ok;
Body = body;
}
public MessageContent(MessageStatus status, string body)
{
Status = status;
Body = body;
}
}
}

View File

@ -0,0 +1,100 @@
using System;
using System.Text;
namespace GodotTools.IdeMessaging
{
public class MessageDecoder
{
private class DecodedMessage
{
public MessageKind? Kind;
public string Id;
public MessageStatus? Status;
public readonly StringBuilder Body = new StringBuilder();
public uint? PendingBodyLines;
public void Clear()
{
Kind = null;
Id = null;
Status = null;
Body.Clear();
PendingBodyLines = null;
}
public Message ToMessage()
{
if (!Kind.HasValue || Id == null || !Status.HasValue ||
!PendingBodyLines.HasValue || PendingBodyLines.Value > 0)
throw new InvalidOperationException();
return new Message(Kind.Value, Id, new MessageContent(Status.Value, Body.ToString()));
}
}
public enum State
{
Decoding,
Decoded,
Errored
}
private readonly DecodedMessage decodingMessage = new DecodedMessage();
public State Decode(string messageLine, out Message decodedMessage)
{
decodedMessage = null;
if (!decodingMessage.Kind.HasValue)
{
if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageKind kind))
{
decodingMessage.Clear();
return State.Errored;
}
decodingMessage.Kind = kind;
}
else if (decodingMessage.Id == null)
{
decodingMessage.Id = messageLine;
}
else if (decodingMessage.Status == null)
{
if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageStatus status))
{
decodingMessage.Clear();
return State.Errored;
}
decodingMessage.Status = status;
}
else if (decodingMessage.PendingBodyLines == null)
{
if (!uint.TryParse(messageLine, out uint pendingBodyLines))
{
decodingMessage.Clear();
return State.Errored;
}
decodingMessage.PendingBodyLines = pendingBodyLines;
}
else
{
if (decodingMessage.PendingBodyLines > 0)
{
decodingMessage.Body.AppendLine(messageLine);
decodingMessage.PendingBodyLines -= 1;
}
else
{
decodedMessage = decodingMessage.ToMessage();
decodingMessage.Clear();
return State.Decoded;
}
}
return State.Decoding;
}
}
}

View File

@ -0,0 +1,302 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GodotTools.IdeMessaging.Requests;
using GodotTools.IdeMessaging.Utils;
namespace GodotTools.IdeMessaging
{
public sealed class Peer : IDisposable
{
/// <summary>
/// Major version.
/// There is no forward nor backward compatibility between different major versions.
/// Connection is refused if client and server have different major versions.
/// </summary>
public static readonly int ProtocolVersionMajor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Major;
/// <summary>
/// Minor version, which clients must be backward compatible with.
/// Connection is refused if the client's minor version is lower than the server's.
/// </summary>
public static readonly int ProtocolVersionMinor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Minor;
/// <summary>
/// Revision, which doesn't affect compatibility.
/// </summary>
public static readonly int ProtocolVersionRevision = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Revision;
public const string ClientHandshakeName = "GodotIdeClient";
public const string ServerHandshakeName = "GodotIdeServer";
private const int ClientWriteTimeout = 8000;
public delegate Task<Response> RequestHandler(Peer peer, MessageContent content);
private readonly TcpClient tcpClient;
private readonly TextReader clientReader;
private readonly TextWriter clientWriter;
private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1);
private string remoteIdentity = string.Empty;
public string RemoteIdentity => remoteIdentity;
public event Action Connected;
public event Action Disconnected;
private ILogger Logger { get; }
public bool IsDisposed { get; private set; }
public bool IsTcpClientConnected => tcpClient.Client != null && tcpClient.Client.Connected;
private bool IsConnected { get; set; }
private readonly IHandshake handshake;
private readonly IMessageHandler messageHandler;
private readonly Dictionary<string, Queue<ResponseAwaiter>> requestAwaiterQueues = new Dictionary<string, Queue<ResponseAwaiter>>();
private readonly SemaphoreSlim requestsSem = new SemaphoreSlim(1);
public Peer(TcpClient tcpClient, IHandshake handshake, IMessageHandler messageHandler, ILogger logger)
{
this.tcpClient = tcpClient;
this.handshake = handshake;
this.messageHandler = messageHandler;
Logger = logger;
NetworkStream clientStream = tcpClient.GetStream();
clientStream.WriteTimeout = ClientWriteTimeout;
clientReader = new StreamReader(clientStream, Encoding.UTF8);
clientWriter = new StreamWriter(clientStream, Encoding.UTF8) {NewLine = "\n"};
}
public async Task Process()
{
try
{
var decoder = new MessageDecoder();
string messageLine;
while ((messageLine = await ReadLine()) != null)
{
var state = decoder.Decode(messageLine, out var msg);
if (state == MessageDecoder.State.Decoding)
continue; // Not finished decoding yet
if (state == MessageDecoder.State.Errored)
{
Logger.LogError($"Received message line with invalid format: {messageLine}");
continue;
}
Logger.LogDebug($"Received message: {msg}");
try
{
try
{
if (msg.Kind == MessageKind.Request)
{
var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger);
await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent));
}
else if (msg.Kind == MessageKind.Response)
{
ResponseAwaiter responseAwaiter;
using (await requestsSem.UseAsync())
{
if (!requestAwaiterQueues.TryGetValue(msg.Id, out var queue) || queue.Count <= 0)
{
Logger.LogError($"Received unexpected response: {msg.Id}");
return;
}
responseAwaiter = queue.Dequeue();
}
responseAwaiter.SetResult(msg.Content);
}
else
{
throw new IndexOutOfRangeException($"Invalid message kind {msg.Kind}");
}
}
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 peer loop", e);
}
}
public async Task<bool> DoHandshake(string identity)
{
if (!await WriteLine(handshake.GetHandshakeLine(identity)))
{
Logger.LogError("Could not write handshake");
return false;
}
var readHandshakeTask = ReadLine();
if (await Task.WhenAny(readHandshakeTask, Task.Delay(8000)) != readHandshakeTask)
{
Logger.LogError("Timeout waiting for the client handshake");
return false;
}
string peerHandshake = await readHandshakeTask;
if (handshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger))
{
Logger.LogError("Received invalid handshake: " + peerHandshake);
return false;
}
IsConnected = true;
Connected?.Invoke();
Logger.LogInfo("Peer connection started");
return true;
}
private async Task<string> ReadLine()
{
try
{
return await clientReader.ReadLineAsync();
}
catch (Exception e)
{
if (IsDisposed)
{
var se = e as SocketException ?? e.InnerException as SocketException;
if (se != null && se.SocketErrorCode == SocketError.Interrupted)
return null;
}
throw;
}
}
private Task<bool> WriteMessage(Message message)
{
Logger.LogDebug($"Sending message: {message}");
int bodyLineCount = message.Content.Body.Count(c => c == '\n');
bodyLineCount += 1; // Extra line break at the end
var builder = new StringBuilder();
builder.AppendLine(message.Kind.ToString());
builder.AppendLine(message.Id);
builder.AppendLine(message.Content.Status.ToString());
builder.AppendLine(bodyLineCount.ToString());
builder.AppendLine(message.Content.Body);
return WriteLine(builder.ToString());
}
public async Task<TResponse> SendRequest<TResponse>(string id, string body)
where TResponse : Response, new()
{
ResponseAwaiter responseAwaiter;
using (await requestsSem.UseAsync())
{
bool written = await WriteMessage(new Message(MessageKind.Request, id, new MessageContent(body)));
if (!written)
return null;
if (!requestAwaiterQueues.TryGetValue(id, out var queue))
{
queue = new Queue<ResponseAwaiter>();
requestAwaiterQueues.Add(id, queue);
}
responseAwaiter = new ResponseAwaiter<TResponse>();
queue.Enqueue(responseAwaiter);
}
return (TResponse)await responseAwaiter;
}
private async Task<bool> WriteLine(string text)
{
if (clientWriter == null || IsDisposed || !IsTcpClientConnected)
return false;
using (await writeSem.UseAsync())
{
try
{
await clientWriter.WriteLineAsync(text);
await clientWriter.FlushAsync();
}
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;
}
// ReSharper disable once UnusedMember.Global
public void ShutdownSocketSend()
{
tcpClient.Client.Shutdown(SocketShutdown.Send);
}
public void Dispose()
{
if (IsDisposed)
return;
IsDisposed = true;
if (IsTcpClientConnected)
{
if (IsConnected)
Disconnected?.Invoke();
}
clientReader?.Dispose();
clientWriter?.Dispose();
((IDisposable)tcpClient)?.Dispose();
}
}
}

View File

@ -0,0 +1,116 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
using Newtonsoft.Json;
namespace GodotTools.IdeMessaging.Requests
{
public abstract class Request
{
[JsonIgnore] public string Id { get; }
protected Request(string id)
{
Id = id;
}
}
public abstract class Response
{
[JsonIgnore] public MessageStatus Status { get; set; } = MessageStatus.Ok;
}
public sealed class CodeCompletionRequest : Request
{
public enum CompletionKind
{
InputActions = 0,
NodePaths,
ResourcePaths,
ScenePaths,
ShaderParams,
Signals,
ThemeColors,
ThemeConstants,
ThemeFonts,
ThemeStyles
}
public CompletionKind Kind { get; set; }
public string ScriptFile { get; set; }
public new const string Id = "CodeCompletion";
public CodeCompletionRequest() : base(Id)
{
}
}
public sealed class CodeCompletionResponse : Response
{
public CodeCompletionRequest.CompletionKind Kind;
public string ScriptFile { get; set; }
public string[] Suggestions { get; set; }
}
public sealed class PlayRequest : Request
{
public new const string Id = "Play";
public PlayRequest() : base(Id)
{
}
}
public sealed class PlayResponse : Response
{
}
public sealed class DebugPlayRequest : Request
{
public string DebuggerHost { get; set; }
public int DebuggerPort { get; set; }
public bool? BuildBeforePlaying { get; set; }
public new const string Id = "DebugPlay";
public DebugPlayRequest() : base(Id)
{
}
}
public sealed class DebugPlayResponse : Response
{
}
public sealed class OpenFileRequest : Request
{
public string File { get; set; }
public int? Line { get; set; }
public int? Column { get; set; }
public new const string Id = "OpenFile";
public OpenFileRequest() : base(Id)
{
}
}
public sealed class OpenFileResponse : Response
{
}
public sealed class ReloadScriptsRequest : Request
{
public new const string Id = "ReloadScripts";
public ReloadScriptsRequest() : base(Id)
{
}
}
public sealed class ReloadScriptsResponse : Response
{
}
}

View File

@ -0,0 +1,23 @@
using GodotTools.IdeMessaging.Requests;
using GodotTools.IdeMessaging.Utils;
using Newtonsoft.Json;
namespace GodotTools.IdeMessaging
{
public abstract class ResponseAwaiter : NotifyAwaiter<Response>
{
public abstract void SetResult(MessageContent content);
}
public class ResponseAwaiter<T> : ResponseAwaiter
where T : Response, new()
{
public override void SetResult(MessageContent content)
{
if (content.Status == MessageStatus.Ok)
SetResult(JsonConvert.DeserializeObject<T>(content.Body));
else
SetResult(new T {Status = content.Status});
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Runtime.CompilerServices;
namespace GodotTools.IdeMessaging.Utils
{
public 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(T);
IsCompleted = false;
return this;
}
public NotifyAwaiter<T> GetAwaiter()
{
return this;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace GodotTools.IdeMessaging.Utils
{
public static class SemaphoreExtensions
{
public static ConfiguredTaskAwaitable<IDisposable> UseAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken = default(CancellationToken))
{
var wrapper = new SemaphoreSlimWaitReleaseWrapper(semaphoreSlim, out Task waitAsyncTask, cancellationToken);
return waitAsyncTask.ContinueWith<IDisposable>(t => wrapper, cancellationToken).ConfigureAwait(false);
}
private struct SemaphoreSlimWaitReleaseWrapper : IDisposable
{
private readonly SemaphoreSlim semaphoreSlim;
public SemaphoreSlimWaitReleaseWrapper(SemaphoreSlim semaphoreSlim, out Task waitAsyncTask, CancellationToken cancellationToken = default(CancellationToken))
{
this.semaphoreSlim = semaphoreSlim;
waitAsyncTask = this.semaphoreSlim.WaitAsync(cancellationToken);
}
public void Dispose()
{
semaphoreSlim.Release();
}
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="EnvDTE" Version="8.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,289 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text.RegularExpressions;
using EnvDTE;
namespace GodotTools.OpenVisualStudio
{
internal static class Program
{
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable pprot);
[DllImport("ole32.dll")]
private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static void ShowHelp()
{
Console.WriteLine("Opens the file(s) in a Visual Studio instance that is editing the specified solution.");
Console.WriteLine("If an existing instance for the solution is not found, a new one is created.");
Console.WriteLine();
Console.WriteLine("Usage:");
Console.WriteLine(@" GodotTools.OpenVisualStudio.exe solution [file[;line[;col]]...]");
Console.WriteLine();
Console.WriteLine("Lines and columns begin at one. Zero or lower will result in an error.");
Console.WriteLine("If a line is specified but a column is not, the line is selected in the text editor.");
}
// STAThread needed, otherwise CoRegisterMessageFilter may return CO_E_NOT_SUPPORTED.
[STAThread]
private static int Main(string[] args)
{
if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
{
ShowHelp();
return 0;
}
string solutionFile = NormalizePath(args[0]);
var dte = FindInstanceEditingSolution(solutionFile);
if (dte == null)
{
// Open a new instance
dte = TryVisualStudioLaunch("VisualStudio.DTE.17.0");
if (dte == null)
{
// Launch of VS 2022 failed, fallback to 2019
dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0");
}
dte.UserControl = true;
try
{
dte.Solution.Open(solutionFile);
}
catch (ArgumentException)
{
Console.Error.WriteLine("Solution.Open: Invalid path or file not found");
return 1;
}
dte.MainWindow.Visible = true;
}
MessageFilter.Register();
try
{
// Open files
for (int i = 1; i < args.Length; i++)
{
// Both the line number and the column begin at one
string[] fileArgumentParts = args[i].Split(';');
string filePath = NormalizePath(fileArgumentParts[0]);
try
{
dte.ItemOperations.OpenFile(filePath);
}
catch (ArgumentException)
{
Console.Error.WriteLine("ItemOperations.OpenFile: Invalid path or file not found");
return 1;
}
if (fileArgumentParts.Length > 1)
{
if (int.TryParse(fileArgumentParts[1], out int line))
{
var textSelection = (TextSelection)dte.ActiveDocument.Selection;
if (fileArgumentParts.Length > 2)
{
if (int.TryParse(fileArgumentParts[2], out int column))
{
textSelection.MoveToLineAndOffset(line, column);
}
else
{
Console.Error.WriteLine("The column part of the argument must be a valid integer");
return 1;
}
}
else
{
textSelection.GotoLine(line, Select: true);
}
}
else
{
Console.Error.WriteLine("The line part of the argument must be a valid integer");
return 1;
}
}
}
}
finally
{
var mainWindow = dte.MainWindow;
mainWindow.Activate();
SetForegroundWindow(new IntPtr(mainWindow.HWnd));
MessageFilter.Revoke();
}
return 0;
}
private static DTE TryVisualStudioLaunch(string version)
{
try
{
var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true);
var dte = (DTE)Activator.CreateInstance(visualStudioDteType);
return dte;
}
catch (COMException)
{
return null;
}
}
private static DTE FindInstanceEditingSolution(string solutionPath)
{
if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
return null;
try
{
pprot.EnumRunning(out IEnumMoniker ppenumMoniker);
ppenumMoniker.Reset();
var moniker = new IMoniker[1];
while (ppenumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
{
string ppszDisplayName;
CreateBindCtx(0, out IBindCtx ppbc);
try
{
moniker[0].GetDisplayName(ppbc, null, out ppszDisplayName);
}
finally
{
Marshal.ReleaseComObject(ppbc);
}
if (ppszDisplayName == null)
continue;
// The digits after the colon are the process ID
if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.1[6-7].0:[0-9]"))
continue;
if (pprot.GetObject(moniker[0], out object ppunkObject) == 0)
{
if (ppunkObject is DTE dte && dte.Solution.FullName.Length > 0)
{
if (NormalizePath(dte.Solution.FullName) == solutionPath)
return dte;
}
}
}
}
finally
{
Marshal.ReleaseComObject(pprot);
}
return null;
}
static string NormalizePath(string path)
{
return new Uri(Path.GetFullPath(path)).LocalPath
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.ToUpperInvariant();
}
#region MessageFilter. See: http: //msdn.microsoft.com/en-us/library/ms228772.aspx
private class MessageFilter : IOleMessageFilter
{
// Class containing the IOleMessageFilter
// thread error-handling functions
private static IOleMessageFilter _oldFilter;
// Start the filter
public static void Register()
{
IOleMessageFilter newFilter = new MessageFilter();
int ret = CoRegisterMessageFilter(newFilter, out _oldFilter);
if (ret != 0)
Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
}
// Done with the filter, close it
public static void Revoke()
{
int ret = CoRegisterMessageFilter(_oldFilter, out _);
if (ret != 0)
Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
}
//
// IOleMessageFilter functions
// Handle incoming thread requests
int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
{
// Return the flag SERVERCALL_ISHANDLED
return 0;
}
// Thread call was rejected, so try again.
int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
{
if (dwRejectType == 2)
// flag = SERVERCALL_RETRYLATER
{
// Retry the thread call immediately if return >= 0 & < 100
return 99;
}
// Too busy; cancel call
return -1;
}
int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
{
// Return the flag PENDINGMSG_WAITDEFPROCESS
return 2;
}
// Implement the IOleMessageFilter interface
[DllImport("ole32.dll")]
private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
}
[ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IOleMessageFilter
{
[PreserveSig]
int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
[PreserveSig]
int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
[PreserveSig]
int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
}
#endregion
}
}

View File

@ -0,0 +1,15 @@
namespace GodotTools
{
public static class ApiAssemblyNames
{
public const string SolutionName = "GodotSharp";
public const string Core = "GodotSharp";
public const string Editor = "GodotSharpEditor";
}
public enum ApiAssemblyType
{
Core,
Editor
}
}

View File

@ -0,0 +1,165 @@
using GodotTools.Core;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace GodotTools.ProjectEditor
{
public class DotNetSolution
{
private const string _solutionTemplate =
@"Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
{0}
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
{1}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2}
EndGlobalSection
EndGlobal
";
private const string _projectDeclaration =
@"Project(""{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}"") = ""{0}"", ""{1}"", ""{{{2}}}""
EndProject";
private const string _solutionPlatformsConfig =
@" {0}|Any CPU = {0}|Any CPU";
private const string _projectPlatformsConfig =
@" {{{0}}}.{1}|Any CPU.ActiveCfg = {1}|Any CPU
{{{0}}}.{1}|Any CPU.Build.0 = {1}|Any CPU";
private string _directoryPath;
private readonly Dictionary<string, ProjectInfo> _projects = new Dictionary<string, ProjectInfo>();
public string Name { get; }
public string DirectoryPath
{
get => _directoryPath;
set => _directoryPath = value.IsAbsolutePath() ? value : Path.GetFullPath(value);
}
public class ProjectInfo
{
public string Guid;
public string PathRelativeToSolution;
public List<string> Configs = new List<string>();
}
public void AddNewProject(string name, ProjectInfo projectInfo)
{
_projects[name] = projectInfo;
}
public bool HasProject(string name)
{
return _projects.ContainsKey(name);
}
public ProjectInfo GetProjectInfo(string name)
{
return _projects[name];
}
public bool RemoveProject(string name)
{
return _projects.Remove(name);
}
public void Save()
{
if (!Directory.Exists(DirectoryPath))
throw new FileNotFoundException("The solution directory does not exist.");
string projectsDecl = string.Empty;
string slnPlatformsCfg = string.Empty;
string projPlatformsCfg = string.Empty;
bool isFirstProject = true;
foreach (var pair in _projects)
{
string name = pair.Key;
ProjectInfo projectInfo = pair.Value;
if (!isFirstProject)
projectsDecl += "\n";
projectsDecl += string.Format(_projectDeclaration,
name, projectInfo.PathRelativeToSolution.Replace("/", "\\"), projectInfo.Guid);
for (int i = 0; i < projectInfo.Configs.Count; i++)
{
string config = projectInfo.Configs[i];
if (i != 0 || !isFirstProject)
{
slnPlatformsCfg += "\n";
projPlatformsCfg += "\n";
}
slnPlatformsCfg += string.Format(_solutionPlatformsConfig, config);
projPlatformsCfg += string.Format(_projectPlatformsConfig, projectInfo.Guid, config);
}
isFirstProject = false;
}
string solutionPath = Path.Combine(DirectoryPath, Name + ".sln");
string content = string.Format(_solutionTemplate, projectsDecl, slnPlatformsCfg, projPlatformsCfg);
File.WriteAllText(solutionPath, content, Encoding.UTF8); // UTF-8 with BOM
}
public DotNetSolution(string name)
{
Name = name;
}
public static void MigrateFromOldConfigNames(string slnPath)
{
if (!File.Exists(slnPath))
return;
string input = File.ReadAllText(slnPath);
if (!Regex.IsMatch(input, Regex.Escape("Tools|Any CPU")))
return;
// This method renames old configurations in solutions to the new ones.
//
// This is the order configs appear in the solution and what we want to rename them to:
// Debug|Any CPU = Debug|Any CPU -> ExportDebug|Any CPU = ExportDebug|Any CPU
// Tools|Any CPU = Tools|Any CPU -> Debug|Any CPU = Debug|Any CPU
//
// But we want to move Tools (now Debug) to the top, so it's easier to rename like this:
// Debug|Any CPU = Debug|Any CPU -> Debug|Any CPU = Debug|Any CPU
// Release|Any CPU = Release|Any CPU -> ExportDebug|Any CPU = ExportDebug|Any CPU
// Tools|Any CPU = Tools|Any CPU -> ExportRelease|Any CPU = ExportRelease|Any CPU
var dict = new Dictionary<string, string>
{
{"Debug|Any CPU", "Debug|Any CPU"},
{"Release|Any CPU", "ExportDebug|Any CPU"},
{"Tools|Any CPU", "ExportRelease|Any CPU"}
};
var regex = new Regex(string.Join("|", dict.Keys.Select(Regex.Escape)));
string result = regex.Replace(input, m => dict[m.Value]);
if (result != input)
{
// Save a copy of the solution before replacing it
FileUtils.SaveBackupCopy(slnPath);
File.WriteAllText(slnPath, result);
}
}
}
}

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Build" />
<PackageReference Include="Microsoft.Build" Version="16.5.0" />
<PackageReference Include="semver" Version="2.0.6" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
</ItemGroup>
<!--
The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described
here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486
We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when
searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed.
-->
<ItemGroup>
<None Include="MSBuild.exe" CopyToOutputDirectory="Always" />
</ItemGroup>
<Target Name="CopyMSBuildStubWindows" AfterTargets="Build" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) ">
<PropertyGroup>
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
<GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
</PropertyGroup>
<!-- Need to copy it here as well on Windows -->
<Copy SourceFiles="MSBuild.exe" DestinationFiles="$(GodotOutputDataDir)\Mono\lib\mono\v4.0\MSBuild.exe" />
</Target>
</Project>

View File

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace GodotTools.ProjectEditor
{
public static class IdentifierUtils
{
public static string SanitizeQualifiedIdentifier(string qualifiedIdentifier, bool allowEmptyIdentifiers)
{
if (string.IsNullOrEmpty(qualifiedIdentifier))
throw new ArgumentException($"{nameof(qualifiedIdentifier)} cannot be empty", nameof(qualifiedIdentifier));
string[] identifiers = qualifiedIdentifier.Split('.');
for (int i = 0; i < identifiers.Length; i++)
{
identifiers[i] = SanitizeIdentifier(identifiers[i], allowEmpty: allowEmptyIdentifiers);
}
return string.Join(".", identifiers);
}
/// <summary>
/// Skips invalid identifier characters including decimal digit numbers at the start of the identifier.
/// </summary>
private static void SkipInvalidCharacters(string source, int startIndex, StringBuilder outputBuilder)
{
for (int i = startIndex; i < source.Length; i++)
{
char @char = source[i];
switch (char.GetUnicodeCategory(@char))
{
case UnicodeCategory.UppercaseLetter:
case UnicodeCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter:
case UnicodeCategory.ModifierLetter:
case UnicodeCategory.LetterNumber:
case UnicodeCategory.OtherLetter:
outputBuilder.Append(@char);
break;
case UnicodeCategory.NonSpacingMark:
case UnicodeCategory.SpacingCombiningMark:
case UnicodeCategory.ConnectorPunctuation:
case UnicodeCategory.DecimalDigitNumber:
// Identifiers may start with underscore
if (outputBuilder.Length > startIndex || @char == '_')
outputBuilder.Append(@char);
break;
}
}
}
public static string SanitizeIdentifier(string identifier, bool allowEmpty)
{
if (string.IsNullOrEmpty(identifier))
{
if (allowEmpty)
return "Empty"; // Default value for empty identifiers
throw new ArgumentException($"{nameof(identifier)} cannot be empty if {nameof(allowEmpty)} is false", nameof(identifier));
}
if (identifier.Length > 511)
identifier = identifier.Substring(0, 511);
var identifierBuilder = new StringBuilder();
int startIndex = 0;
if (identifier[0] == '@')
{
identifierBuilder.Append('@');
startIndex += 1;
}
SkipInvalidCharacters(identifier, startIndex, identifierBuilder);
if (identifierBuilder.Length == startIndex)
{
// All characters were invalid so now it's empty. Fill it with something.
identifierBuilder.Append("Empty");
}
identifier = identifierBuilder.ToString();
if (identifier[0] != '@' && IsKeyword(identifier, anyDoubleUnderscore: true))
identifier = '@' + identifier;
return identifier;
}
private static bool IsKeyword(string value, bool anyDoubleUnderscore)
{
// Identifiers that start with double underscore are meant to be used for reserved keywords.
// Only existing keywords are enforced, but it may be useful to forbid any identifier
// that begins with double underscore to prevent issues with future C# versions.
if (anyDoubleUnderscore)
{
if (value.Length > 2 && value[0] == '_' && value[1] == '_' && value[2] != '_')
return true;
}
else
{
if (_doubleUnderscoreKeywords.Contains(value))
return true;
}
return _keywords.Contains(value);
}
private static readonly HashSet<string> _doubleUnderscoreKeywords = new HashSet<string>
{
"__arglist",
"__makeref",
"__reftype",
"__refvalue",
};
private static readonly HashSet<string> _keywords = new HashSet<string>
{
"as",
"do",
"if",
"in",
"is",
"for",
"int",
"new",
"out",
"ref",
"try",
"base",
"bool",
"byte",
"case",
"char",
"else",
"enum",
"goto",
"lock",
"long",
"null",
"this",
"true",
"uint",
"void",
"break",
"catch",
"class",
"const",
"event",
"false",
"fixed",
"float",
"sbyte",
"short",
"throw",
"ulong",
"using",
"where",
"while",
"yield",
"double",
"extern",
"object",
"params",
"public",
"return",
"sealed",
"sizeof",
"static",
"string",
"struct",
"switch",
"typeof",
"unsafe",
"ushort",
"checked",
"decimal",
"default",
"finally",
"foreach",
"partial",
"private",
"virtual",
"abstract",
"continue",
"delegate",
"explicit",
"implicit",
"internal",
"operator",
"override",
"readonly",
"volatile",
"interface",
"namespace",
"protected",
"unchecked",
"stackalloc",
};
}
}

View File

@ -0,0 +1,133 @@
using GodotTools.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Globbing;
namespace GodotTools.ProjectEditor
{
public static class ProjectExtensions
{
public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
string normalizedInclude = include.NormalizePath();
foreach (var itemGroup in root.ItemGroups)
{
if (noCondition && itemGroup.Condition.Length != 0)
continue;
foreach (var item in itemGroup.Items)
{
if (item.ItemType != itemType)
continue;
var glob = MSBuildGlob.Parse(item.Include.NormalizePath());
if (glob.IsMatch(normalizedInclude))
return item;
}
}
return null;
}
public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
string normalizedInclude = Path.GetFullPath(include).NormalizePath();
foreach (var itemGroup in root.ItemGroups)
{
if (noCondition && itemGroup.Condition.Length != 0)
continue;
foreach (var item in itemGroup.Items)
{
if (item.ItemType != itemType)
continue;
var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath());
if (glob.IsMatch(normalizedInclude))
return item;
}
}
return null;
}
public static IEnumerable<ProjectItemElement> FindAllItemsInFolder(this ProjectRootElement root, string itemType, string folder)
{
string absFolderNormalizedWithSep = Path.GetFullPath(folder).NormalizePath() + Path.DirectorySeparatorChar;
foreach (var itemGroup in root.ItemGroups)
{
foreach (var item in itemGroup.Items)
{
if (item.ItemType != itemType)
continue;
string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath();
if (absPathNormalized.StartsWith(absFolderNormalizedWithSep))
yield return item;
}
}
}
public static bool HasItem(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
return root.FindItemOrNull(itemType, include, noCondition) != null;
}
public static bool AddItemChecked(this ProjectRootElement root, string itemType, string include)
{
if (!root.HasItem(itemType, include, noCondition: true))
{
root.AddItem(itemType, include);
return true;
}
return false;
}
public static bool RemoveItemChecked(this ProjectRootElement root, string itemType, string include)
{
var item = root.FindItemOrNullAbs(itemType, include);
if (item != null)
{
item.Parent.RemoveChild(item);
return true;
}
return false;
}
public static Guid GetGuid(this ProjectRootElement root)
{
foreach (var property in root.Properties)
{
if (property.Name == "ProjectGuid")
return Guid.Parse(property.Value);
}
return Guid.Empty;
}
public static bool AreDefaultCompileItemsEnabled(this ProjectRootElement root)
{
var enableDefaultCompileItemsProps = root.PropertyGroups
.Where(g => string.IsNullOrEmpty(g.Condition))
.SelectMany(g => g.Properties
.Where(p => p.Name == "EnableDefaultCompileItems" && string.IsNullOrEmpty(p.Condition)));
bool enableDefaultCompileItems = true;
foreach (var prop in enableDefaultCompileItemsProps)
enableDefaultCompileItems = prop.Value.Equals("true", StringComparison.OrdinalIgnoreCase);
return enableDefaultCompileItems;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.IO;
using System.Text;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
namespace GodotTools.ProjectEditor
{
public static class ProjectGenerator
{
public const string GodotSdkVersionToUse = "3.3.0";
public const string GodotSdkNameToUse = "Godot.NET.Sdk";
public static ProjectRootElement GenGameProject(string name)
{
if (name.Length == 0)
throw new ArgumentException("Project name is empty", nameof(name));
var root = ProjectRootElement.Create(NewProjectFileOptions.None);
root.Sdk = $"{GodotSdkNameToUse}/{GodotSdkVersionToUse}";
var mainGroup = root.AddPropertyGroup();
mainGroup.AddProperty("TargetFramework", "net472");
string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true);
// If the name is not a valid namespace, manually set RootNamespace to a sanitized one.
if (sanitizedName != name)
mainGroup.AddProperty("RootNamespace", sanitizedName);
return root;
}
public static string GenAndSaveGameProject(string dir, string name)
{
if (name.Length == 0)
throw new ArgumentException("Project name is empty", nameof(name));
string path = Path.Combine(dir, name + ".csproj");
var root = GenGameProject(name);
// Save (without BOM)
root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
return Guid.NewGuid().ToString().ToUpper();
}
}
}

View File

@ -0,0 +1,471 @@
using System;
using GodotTools.Core;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using JetBrains.Annotations;
using Microsoft.Build.Construction;
using Microsoft.Build.Globbing;
using Semver;
namespace GodotTools.ProjectEditor
{
public sealed class MSBuildProject
{
internal ProjectRootElement Root { get; set; }
public bool HasUnsavedChanges { get; set; }
public void Save() => Root.Save();
public MSBuildProject(ProjectRootElement root)
{
Root = root;
}
}
public static class ProjectUtils
{
public static MSBuildProject Open(string path)
{
var root = ProjectRootElement.Open(path);
return root != null ? new MSBuildProject(root) : null;
}
[PublicAPI]
public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
{
var dir = Directory.GetParent(projectPath).FullName;
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
if (root.AreDefaultCompileItemsEnabled())
{
// No need to add. It's already included automatically by the MSBuild Sdk.
// This assumes the source file is inside the project directory and not manually excluded in the csproj
return;
}
var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\");
if (root.AddItemChecked(itemType, normalizedInclude))
root.Save();
}
public static void RenameItemInProjectChecked(string projectPath, string itemType, string oldInclude, string newInclude)
{
var dir = Directory.GetParent(projectPath).FullName;
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
if (root.AreDefaultCompileItemsEnabled())
{
// No need to add. It's already included automatically by the MSBuild Sdk.
// This assumes the source file is inside the project directory and not manually excluded in the csproj
return;
}
var normalizedOldInclude = oldInclude.NormalizePath();
var normalizedNewInclude = newInclude.NormalizePath();
var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude);
if (item == null)
return;
// Check if the found item include already matches the new path
var glob = MSBuildGlob.Parse(item.Include);
if (glob.IsMatch(normalizedNewInclude))
return;
// Otherwise, if the item include uses globbing it's better to add a new item instead of modifying
if (!string.IsNullOrEmpty(glob.WildcardDirectoryPart) || glob.FilenamePart.Contains("*"))
{
root.AddItem(itemType, normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\"));
root.Save();
return;
}
item.Include = normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\");
root.Save();
}
public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include)
{
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
if (root.AreDefaultCompileItemsEnabled())
{
// No need to add. It's already included automatically by the MSBuild Sdk.
// This assumes the source file is inside the project directory and not manually excluded in the csproj
return;
}
var normalizedInclude = include.NormalizePath();
var item = root.FindItemOrNullAbs(itemType, normalizedInclude);
// Couldn't find an existing item that matches to remove
if (item == null)
return;
var glob = MSBuildGlob.Parse(item.Include);
// If the item include uses globbing don't remove it
if (!string.IsNullOrEmpty(glob.WildcardDirectoryPart) || glob.FilenamePart.Contains("*"))
{
return;
}
item.Parent.RemoveChild(item);
root.Save();
}
public static void RenameItemsToNewFolderInProjectChecked(string projectPath, string itemType, string oldFolder, string newFolder)
{
var dir = Directory.GetParent(projectPath).FullName;
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
if (root.AreDefaultCompileItemsEnabled())
{
// No need to add. It's already included automatically by the MSBuild Sdk.
// This assumes the source file is inside the project directory and not manually excluded in the csproj
return;
}
bool dirty = false;
var oldFolderNormalized = oldFolder.NormalizePath();
var newFolderNormalized = newFolder.NormalizePath();
string absOldFolderNormalized = Path.GetFullPath(oldFolderNormalized).NormalizePath();
string absNewFolderNormalized = Path.GetFullPath(newFolderNormalized).NormalizePath();
foreach (var item in root.FindAllItemsInFolder(itemType, oldFolderNormalized))
{
string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath();
string absNewIncludeNormalized = absNewFolderNormalized + absPathNormalized.Substring(absOldFolderNormalized.Length);
item.Include = absNewIncludeNormalized.RelativeToPath(dir).Replace("/", "\\");
dirty = true;
}
if (dirty)
root.Save();
}
public static void RemoveItemsInFolderFromProjectChecked(string projectPath, string itemType, string folder)
{
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
if (root.AreDefaultCompileItemsEnabled())
{
// No need to add. It's already included automatically by the MSBuild Sdk.
// This assumes the source file is inside the project directory and not manually excluded in the csproj
return;
}
var folderNormalized = folder.NormalizePath();
var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList();
if (itemsToRemove.Count > 0)
{
foreach (var item in itemsToRemove)
item.Parent.RemoveChild(item);
root.Save();
}
}
private static string[] GetAllFilesRecursive(string rootDirectory, string mask)
{
string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories);
// We want relative paths
for (int i = 0; i < files.Length; i++)
{
files[i] = files[i].RelativeToPath(rootDirectory);
}
return files;
}
public static string[] GetIncludeFiles(string projectPath, string itemType)
{
var result = new List<string>();
var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
if (root.AreDefaultCompileItemsEnabled())
{
var excluded = new List<string>();
result.AddRange(existingFiles);
foreach (var item in root.Items)
{
if (string.IsNullOrEmpty(item.Condition))
continue;
if (item.ItemType != itemType)
continue;
string normalizedRemove = item.Remove.NormalizePath();
var glob = MSBuildGlob.Parse(normalizedRemove);
excluded.AddRange(result.Where(includedFile => glob.IsMatch(includedFile)));
}
result.RemoveAll(f => excluded.Contains(f));
}
foreach (var itemGroup in root.ItemGroups)
{
if (itemGroup.Condition.Length != 0)
continue;
foreach (var item in itemGroup.Items)
{
if (item.ItemType != itemType)
continue;
string normalizedInclude = item.Include.NormalizePath();
var glob = MSBuildGlob.Parse(normalizedInclude);
foreach (var existingFile in existingFiles)
{
if (glob.IsMatch(existingFile))
{
result.Add(existingFile);
}
}
}
}
return result.ToArray();
}
public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName)
{
var root = project.Root;
if (!string.IsNullOrEmpty(root.Sdk))
return;
root.Sdk = $"{ProjectGenerator.GodotSdkNameToUse}/{ProjectGenerator.GodotSdkVersionToUse}";
root.ToolsVersion = null;
root.DefaultTargets = null;
root.AddProperty("TargetFramework", "net472");
// Remove obsolete properties, items and elements. We're going to be conservative
// here to minimize the chances of introducing breaking changes. As such we will
// only remove elements that could potentially cause issues with the Godot.NET.Sdk.
void RemoveElements(IEnumerable<ProjectElement> elements)
{
foreach (var element in elements)
element.Parent.RemoveChild(element);
}
// Default Configuration
RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
.Where(p => p.Name == "Configuration" && p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Debug"));
// Default Platform
RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
.Where(p => p.Name == "Platform" && p.Condition.Trim() == "'$(Platform)' == ''" && p.Value == "AnyCPU"));
// Simple properties
var yabaiProperties = new[]
{
"OutputPath",
"BaseIntermediateOutputPath",
"IntermediateOutputPath",
"TargetFrameworkVersion",
"ProjectTypeGuids",
"ApiConfiguration"
};
RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
.Where(p => yabaiProperties.Contains(p.Name)));
// Configuration dependent properties
var yabaiPropertiesForConfigs = new[]
{
"DebugSymbols",
"DebugType",
"Optimize",
"DefineConstants",
"ErrorReport",
"WarningLevel",
"ConsolePause"
};
var configNames = new[]
{
"ExportDebug", "ExportRelease", "Debug",
"Tools", "Release" // Include old config names as well in case it's upgrading from 3.2.1 or older
};
foreach (var config in configNames)
{
var group = root.PropertyGroups
.FirstOrDefault(g => g.Condition.Trim() == $"'$(Configuration)|$(Platform)' == '{config}|AnyCPU'");
if (group == null)
continue;
RemoveElements(group.Properties.Where(p => yabaiPropertiesForConfigs.Contains(p.Name)));
if (group.Count == 0)
{
// No more children, safe to delete the group
group.Parent.RemoveChild(group);
}
}
// Godot API References
var apiAssemblies = new[] { ApiAssemblyNames.Core, ApiAssemblyNames.Editor };
RemoveElements(root.ItemGroups.SelectMany(g => g.Items)
.Where(i => i.ItemType == "Reference" && apiAssemblies.Contains(i.Include)));
// Microsoft.NETFramework.ReferenceAssemblies PackageReference
RemoveElements(root.ItemGroups.SelectMany(g => g.Items).Where(i =>
i.ItemType == "PackageReference" &&
i.Include.Equals("Microsoft.NETFramework.ReferenceAssemblies", StringComparison.OrdinalIgnoreCase)));
// Imports
var yabaiImports = new[]
{
"$(MSBuildBinPath)/Microsoft.CSharp.targets",
"$(MSBuildBinPath)Microsoft.CSharp.targets"
};
RemoveElements(root.Imports.Where(import => yabaiImports.Contains(
import.Project.Replace("\\", "/").Replace("//", "/"))));
// 'EnableDefaultCompileItems' and 'GenerateAssemblyInfo' are kept enabled by default
// on new projects, but when migrating old projects we disable them to avoid errors.
root.AddProperty("EnableDefaultCompileItems", "false");
root.AddProperty("GenerateAssemblyInfo", "false");
// Older AssemblyInfo.cs cause the following error:
// 'Properties/AssemblyInfo.cs(19,28): error CS8357:
// The specified version string contains wildcards, which are not compatible with determinism.
// Either remove wildcards from the version string, or disable determinism for this compilation.'
// We disable deterministic builds to prevent this. The user can then fix this manually when desired
// by fixing 'AssemblyVersion("1.0.*")' to not use wildcards.
root.AddProperty("Deterministic", "false");
project.HasUnsavedChanges = true;
var xDoc = XDocument.Parse(root.RawXml);
if (xDoc.Root == null)
return; // Too bad, we will have to keep the xmlns/namespace and xml declaration
XElement GetElement(XDocument doc, string name, string value, string parentName)
{
foreach (var node in doc.DescendantNodes())
{
if (!(node is XElement element))
continue;
if (element.Name.LocalName.Equals(name) && element.Value == value &&
element.Parent != null && element.Parent.Name.LocalName.Equals(parentName))
{
return element;
}
}
return null;
}
// Add comment about Microsoft.NET.Sdk properties disabled during migration
GetElement(xDoc, name: "EnableDefaultCompileItems", value: "false", parentName: "PropertyGroup")
.AddBeforeSelf(new XComment("The following properties were overridden during migration to prevent errors.\n" +
" Enabling them may require other manual changes to the project and its files."));
void RemoveNamespace(XElement element)
{
element.Attributes().Where(x => x.IsNamespaceDeclaration).Remove();
element.Name = element.Name.LocalName;
foreach (var node in element.DescendantNodes())
{
if (node is XElement xElement)
{
// Need to do the same for all children recursively as it adds it to them for some reason...
RemoveNamespace(xElement);
}
}
}
// Remove xmlns/namespace
RemoveNamespace(xDoc.Root);
// Remove xml declaration
xDoc.Nodes().FirstOrDefault(node => node.NodeType == XmlNodeType.XmlDeclaration)?.Remove();
string projectFullPath = root.FullPath;
root = ProjectRootElement.Create(xDoc.CreateReader());
root.FullPath = projectFullPath;
project.Root = root;
}
public static void EnsureGodotSdkIsUpToDate(MSBuildProject project)
{
string godotSdkAttrValue = $"{ProjectGenerator.GodotSdkNameToUse}/{ProjectGenerator.GodotSdkVersionToUse}";
var root = project.Root;
string rootSdk = root.Sdk?.Trim();
if (!string.IsNullOrEmpty(rootSdk))
{
// Check if the version is already the same.
if (rootSdk.Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase))
return;
// We also allow higher versions as long as the major and minor are the same.
var semVerToUse = SemVersion.Parse(ProjectGenerator.GodotSdkVersionToUse);
var godotSdkAttrLaxValueRegex = new Regex($@"^{ProjectGenerator.GodotSdkNameToUse}/(?<ver>.*)$");
var match = godotSdkAttrLaxValueRegex.Match(rootSdk);
if (match.Success &&
SemVersion.TryParse(match.Groups["ver"].Value, out var semVerDetected) &&
semVerDetected.Major == semVerToUse.Major &&
semVerDetected.Minor == semVerToUse.Minor &&
semVerDetected > semVerToUse)
{
return;
}
}
root.Sdk = godotSdkAttrValue;
project.HasUnsavedChanges = true;
}
}
}

View File

@ -0,0 +1,41 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.ProjectEditor", "GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj", "{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools", "GodotTools\GodotTools.csproj", "{27B00618-A6F2-4828-B922-05CAEB08C286}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotTools.Core\GodotTools.Core.csproj", "{639E48BD-44E5-4091-8EDD-22D36DC0768D}"
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.OpenVisualStudio", "GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj", "{EAFFF236-FA96-4A4D-BD23-0E51EF988277}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Release|Any CPU.Build.0 = Release|Any CPU
{27B00618-A6F2-4828-B922-05CAEB08C286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27B00618-A6F2-4828-B922-05CAEB08C286}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27B00618-A6F2-4828-B922-05CAEB08C286}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27B00618-A6F2-4828-B922-05CAEB08C286}.Release|Any CPU.Build.0 = Release|Any CPU
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Release|Any CPU.Build.0 = Release|Any CPU
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,58 @@
using System;
using Godot;
using Godot.Collections;
using GodotTools.Internals;
using Path = System.IO.Path;
namespace GodotTools.Build
{
[Serializable]
public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
{
public string Solution { get; }
public string[] Targets { get; }
public string Configuration { get; }
public bool Restore { get; }
// TODO Use List once we have proper serialization
public Array<string> CustomProperties { get; } = new Array<string>();
public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}");
public override bool Equals(object obj)
{
if (obj is BuildInfo other)
return other.Solution == Solution && other.Targets == Targets &&
other.Configuration == Configuration && other.Restore == Restore &&
other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath;
return false;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = (hash * 29) + Solution.GetHashCode();
hash = (hash * 29) + Targets.GetHashCode();
hash = (hash * 29) + Configuration.GetHashCode();
hash = (hash * 29) + Restore.GetHashCode();
hash = (hash * 29) + CustomProperties.GetHashCode();
hash = (hash * 29) + LogsDirPath.GetHashCode();
return hash;
}
}
private BuildInfo()
{
}
public BuildInfo(string solution, string[] targets, string configuration, bool restore)
{
Solution = solution;
Targets = targets;
Configuration = configuration;
Restore = restore;
}
}
}

View File

@ -0,0 +1,294 @@
using System;
using System.IO;
using System.Threading.Tasks;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
using OS = GodotTools.Utils.OS;
using Path = System.IO.Path;
namespace GodotTools.Build
{
public static class BuildManager
{
private static BuildInfo _buildInProgress;
public const string PropNameMSBuildMono = "MSBuild (Mono)";
public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)";
public const string PropNameDotnetCli = "dotnet CLI";
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
public const string MsBuildLogFileName = "msbuild_log.txt";
public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason);
public static event BuildLaunchFailedEventHandler BuildLaunchFailed;
public static event Action<BuildInfo> BuildStarted;
public static event Action<BuildResult> BuildFinished;
public static event Action<string> StdOutputReceived;
public static event Action<string> StdErrorReceived;
private static void RemoveOldIssuesFile(BuildInfo buildInfo)
{
string issuesFile = GetIssuesFilePath(buildInfo);
if (!File.Exists(issuesFile))
return;
File.Delete(issuesFile);
}
private static void ShowBuildErrorDialog(string message)
{
var plugin = GodotSharpEditor.Instance;
plugin.ShowErrorDialog(message, "Build error");
plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
}
public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
private static string GetLogFilePath(BuildInfo buildInfo)
{
return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName);
}
private static string GetIssuesFilePath(BuildInfo buildInfo)
{
return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName);
}
private static void PrintVerbose(string text)
{
if (Godot.OS.IsStdoutVerbose())
Godot.GD.Print(text);
}
public static bool Build(BuildInfo buildInfo)
{
if (_buildInProgress != null)
throw new InvalidOperationException("A build is already in progress");
_buildInProgress = buildInfo;
try
{
BuildStarted?.Invoke(buildInfo);
// Required in order to update the build tasks list
Internal.GodotMainIteration();
try
{
RemoveOldIssuesFile(buildInfo);
}
catch (IOException e)
{
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e);
}
try
{
int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);
if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
return exitCode == 0;
}
catch (Exception e)
{
BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e);
return false;
}
}
finally
{
_buildInProgress = null;
}
}
public static async Task<bool> BuildAsync(BuildInfo buildInfo)
{
if (_buildInProgress != null)
throw new InvalidOperationException("A build is already in progress");
_buildInProgress = buildInfo;
try
{
BuildStarted?.Invoke(buildInfo);
try
{
RemoveOldIssuesFile(buildInfo);
}
catch (IOException e)
{
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e);
}
try
{
int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);
if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
return exitCode == 0;
}
catch (Exception e)
{
BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e);
return false;
}
}
finally
{
_buildInProgress = null;
}
}
public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null)
{
var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true);
// If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
if (Internal.GodotIsRealTDouble())
buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
return BuildProjectBlocking(buildInfo);
}
private static bool BuildProjectBlocking(BuildInfo buildInfo)
{
if (!File.Exists(buildInfo.Solution))
return true; // No solution to build
// Make sure the API assemblies are up to date before building the project.
// We may not have had the chance to update the release API assemblies, and the debug ones
// may have been deleted by the user at some point after they were loaded by the Godot editor.
string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug");
if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
{
ShowBuildErrorDialog("Failed to update the Godot API assemblies");
return false;
}
using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1))
{
pr.Step("Building project solution", 0);
if (!Build(buildInfo))
{
ShowBuildErrorDialog("Failed to build project solution");
return false;
}
}
return true;
}
public static bool EditorBuildCallback()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return true; // No solution to build
GenerateEditorScriptMetadata();
if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
return true; // Requested play from an external editor/IDE which already built the project
return BuildProjectBlocking("Debug");
}
// NOTE: This will be replaced with C# source generators in 4.0
public static void GenerateEditorScriptMetadata()
{
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (!File.Exists(editorScriptsMetadataPath))
return;
try
{
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
}
catch (IOException e)
{
throw new IOException("Failed to copy scripts metadata file.", innerException: e);
}
}
// NOTE: This will be replaced with C# source generators in 4.0
public static string GenerateExportedGameScriptMetadata(bool isDebug)
{
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
return scriptsMetadataPath;
}
public static void Initialize()
{
// Build tool settings
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
BuildTool msbuildDefault;
if (OS.IsWindows)
{
if (RiderPathManager.IsExternalEditorSetToRider(editorSettings))
msbuildDefault = BuildTool.JetBrainsMsBuild;
else
msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs;
}
else
{
msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono;
}
EditorDef("mono/builds/build_tool", msbuildDefault);
string hintString;
if (OS.IsWindows)
{
hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
$"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," +
$"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," +
$"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
}
else
{
hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
$"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
}
editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = Godot.Variant.Type.Int,
["name"] = "mono/builds/build_tool",
["hint"] = Godot.PropertyHint.Enum,
["hint_string"] = hintString
});
}
}
}

View File

@ -0,0 +1,418 @@
using Godot;
using System;
using Godot.Collections;
using GodotTools.Internals;
using JetBrains.Annotations;
using File = GodotTools.Utils.File;
using Path = System.IO.Path;
namespace GodotTools.Build
{
public class BuildOutputView : VBoxContainer, ISerializationListener
{
[Serializable]
private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization
{
public bool Warning { get; set; }
public string File { get; set; }
public int Line { get; set; }
public int Column { get; set; }
public string Code { get; set; }
public string Message { get; set; }
public string ProjectFile { get; set; }
}
[Signal]
public delegate void BuildStateChanged();
public bool HasBuildExited { get; private set; } = false;
public BuildResult? BuildResult { get; private set; } = null;
public int ErrorCount { get; private set; } = 0;
public int WarningCount { get; private set; } = 0;
public bool ErrorsVisible { get; set; } = true;
public bool WarningsVisible { get; set; } = true;
public Texture BuildStateIcon
{
get
{
if (!HasBuildExited)
return GetIcon("Stop", "EditorIcons");
if (BuildResult == Build.BuildResult.Error)
return GetIcon("Error", "EditorIcons");
if (WarningCount > 1)
return GetIcon("Warning", "EditorIcons");
return null;
}
}
public bool LogVisible
{
set => _buildLog.Visible = value;
}
// TODO Use List once we have proper serialization.
private readonly Array<BuildIssue> _issues = new Array<BuildIssue>();
private ItemList _issuesList;
private PopupMenu _issuesListContextMenu;
private TextEdit _buildLog;
private BuildInfo _buildInfo;
private readonly object _pendingBuildLogTextLock = new object();
[NotNull] private string _pendingBuildLogText = string.Empty;
private void LoadIssuesFromFile(string csvFile)
{
using (var file = new Godot.File())
{
try
{
Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read);
if (openError != Error.Ok)
return;
while (!file.EofReached())
{
string[] csvColumns = file.GetCsvLine();
if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
return;
if (csvColumns.Length != 7)
{
GD.PushError($"Expected 7 columns, got {csvColumns.Length}");
continue;
}
var issue = new BuildIssue
{
Warning = csvColumns[0] == "warning",
File = csvColumns[1],
Line = int.Parse(csvColumns[2]),
Column = int.Parse(csvColumns[3]),
Code = csvColumns[4],
Message = csvColumns[5],
ProjectFile = csvColumns[6]
};
if (issue.Warning)
WarningCount += 1;
else
ErrorCount += 1;
_issues.Add(issue);
}
}
finally
{
file.Close(); // Disposing it is not enough. We need to call Close()
}
}
}
private void IssueActivated(int idx)
{
if (idx < 0 || idx >= _issuesList.GetItemCount())
throw new IndexOutOfRangeException("Item list index out of range");
// Get correct issue idx from issue list
int issueIndex = (int)_issuesList.GetItemMetadata(idx);
if (issueIndex < 0 || issueIndex >= _issues.Count)
throw new IndexOutOfRangeException("Issue index out of range");
BuildIssue issue = _issues[issueIndex];
if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File))
return;
string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : _buildInfo.Solution.GetBaseDir();
string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath());
if (!File.Exists(file))
return;
file = ProjectSettings.LocalizePath(file);
if (file.StartsWith("res://"))
{
var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType);
if (script != null && Internal.ScriptEditorEdit(script, issue.Line, issue.Column))
Internal.EditorNodeShowScriptScreen();
}
}
public void UpdateIssuesList()
{
_issuesList.Clear();
using (var warningIcon = GetIcon("Warning", "EditorIcons"))
using (var errorIcon = GetIcon("Error", "EditorIcons"))
{
for (int i = 0; i < _issues.Count; i++)
{
BuildIssue issue = _issues[i];
if (!(issue.Warning ? WarningsVisible : ErrorsVisible))
continue;
string tooltip = string.Empty;
tooltip += $"Message: {issue.Message}";
if (!string.IsNullOrEmpty(issue.Code))
tooltip += $"\nCode: {issue.Code}";
tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
string text = string.Empty;
if (!string.IsNullOrEmpty(issue.File))
{
text += $"{issue.File}({issue.Line},{issue.Column}): ";
tooltip += $"\nFile: {issue.File}";
tooltip += $"\nLine: {issue.Line}";
tooltip += $"\nColumn: {issue.Column}";
}
if (!string.IsNullOrEmpty(issue.ProjectFile))
tooltip += $"\nProject: {issue.ProjectFile}";
text += issue.Message;
int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal);
string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx);
_issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon);
int index = _issuesList.GetItemCount() - 1;
_issuesList.SetItemTooltip(index, tooltip);
_issuesList.SetItemMetadata(index, i);
}
}
}
private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
{
HasBuildExited = true;
BuildResult = Build.BuildResult.Error;
_issuesList.Clear();
var issue = new BuildIssue {Message = cause, Warning = false};
ErrorCount += 1;
_issues.Add(issue);
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void BuildStarted(BuildInfo buildInfo)
{
_buildInfo = buildInfo;
HasBuildExited = false;
_issues.Clear();
WarningCount = 0;
ErrorCount = 0;
_buildLog.Text = string.Empty;
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void BuildFinished(BuildResult result)
{
HasBuildExited = true;
BuildResult = result;
LoadIssuesFromFile(Path.Combine(_buildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void UpdateBuildLogText()
{
lock (_pendingBuildLogTextLock)
{
_buildLog.Text += _pendingBuildLogText;
_pendingBuildLogText = string.Empty;
ScrollToLastNonEmptyLogLine();
}
}
private void StdOutputReceived(string text)
{
lock (_pendingBuildLogTextLock)
{
if (_pendingBuildLogText.Length == 0)
CallDeferred(nameof(UpdateBuildLogText));
_pendingBuildLogText += text + "\n";
}
}
private void StdErrorReceived(string text)
{
lock (_pendingBuildLogTextLock)
{
if (_pendingBuildLogText.Length == 0)
CallDeferred(nameof(UpdateBuildLogText));
_pendingBuildLogText += text + "\n";
}
}
private void ScrollToLastNonEmptyLogLine()
{
int line;
for (line = _buildLog.GetLineCount(); line > 0; line--)
{
string lineText = _buildLog.GetLine(line);
if (!string.IsNullOrEmpty(lineText) || !string.IsNullOrEmpty(lineText?.Trim()))
break;
}
_buildLog.CursorSetLine(line);
}
public void RestartBuild()
{
if (!HasBuildExited)
throw new InvalidOperationException("Build already started");
BuildManager.RestartBuild(this);
}
public void StopBuild()
{
if (!HasBuildExited)
throw new InvalidOperationException("Build is not in progress");
BuildManager.StopBuild(this);
}
private enum IssuesContextMenuOption
{
Copy
}
private void IssuesListContextOptionPressed(IssuesContextMenuOption id)
{
switch (id)
{
case IssuesContextMenuOption.Copy:
{
// We don't allow multi-selection but just in case that changes later...
string text = null;
foreach (int issueIndex in _issuesList.GetSelectedItems())
{
if (text != null)
text += "\n";
text += _issuesList.GetItemText(issueIndex);
}
if (text != null)
OS.Clipboard = text;
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid issue context menu option");
}
}
private void IssuesListRmbSelected(int index, Vector2 atPosition)
{
_ = index; // Unused
_issuesListContextMenu.Clear();
_issuesListContextMenu.SetSize(new Vector2(1, 1));
if (_issuesList.IsAnythingSelected())
{
// Add menu entries for the selected item
_issuesListContextMenu.AddIconItem(GetIcon("ActionCopy", "EditorIcons"),
label: "Copy Error".TTR(), (int)IssuesContextMenuOption.Copy);
}
if (_issuesListContextMenu.GetItemCount() > 0)
{
_issuesListContextMenu.SetPosition(_issuesList.RectGlobalPosition + atPosition);
_issuesListContextMenu.Popup_();
}
}
public override void _Ready()
{
base._Ready();
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
var hsc = new HSplitContainer
{
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
AddChild(hsc);
_issuesList = new ItemList
{
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the build log
};
_issuesList.Connect("item_activated", this, nameof(IssueActivated));
_issuesList.AllowRmbSelect = true;
_issuesList.Connect("item_rmb_selected", this, nameof(IssuesListRmbSelected));
hsc.AddChild(_issuesList);
_issuesListContextMenu = new PopupMenu();
_issuesListContextMenu.Connect("id_pressed", this, nameof(IssuesListContextOptionPressed));
_issuesList.AddChild(_issuesListContextMenu);
_buildLog = new TextEdit
{
Readonly = true,
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the issues list
};
hsc.AddChild(_buildLog);
AddBuildEventListeners();
}
private void AddBuildEventListeners()
{
BuildManager.BuildLaunchFailed += BuildLaunchFailed;
BuildManager.BuildStarted += BuildStarted;
BuildManager.BuildFinished += BuildFinished;
// StdOutput/Error can be received from different threads, so we need to use CallDeferred
BuildManager.StdOutputReceived += StdOutputReceived;
BuildManager.StdErrorReceived += StdErrorReceived;
}
public void OnBeforeSerialize()
{
// In case it didn't update yet. We don't want to have to serialize any pending output.
UpdateBuildLogText();
}
public void OnAfterDeserialize()
{
AddBuildEventListeners(); // Re-add them
}
}
}

View File

@ -0,0 +1,8 @@
namespace GodotTools.Build
{
public enum BuildResult
{
Error,
Success
}
}

View File

@ -0,0 +1,156 @@
using GodotTools.Core;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using GodotTools.BuildLogger;
using GodotTools.Internals;
using GodotTools.Utils;
using Directory = System.IO.Directory;
namespace GodotTools.Build
{
public static class BuildSystem
{
private static string MonoWindowsBinDir
{
get
{
string monoWinBinDir = Path.Combine(Internal.MonoWindowsInstallRoot, "bin");
if (!Directory.Exists(monoWinBinDir))
throw new FileNotFoundException("Cannot find the Windows Mono install bin directory.");
return monoWinBinDir;
}
}
private static Godot.EditorSettings EditorSettings =>
GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
private static bool UsingMonoMsBuildOnWindows
{
get
{
if (OS.IsWindows)
{
return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool")
== BuildTool.MsBuildMono;
}
return false;
}
}
private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{
(string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
if (msbuildPath == null)
throw new FileNotFoundException("Cannot find the MSBuild executable.");
string compilerArgs = BuildArguments(buildTool, buildInfo);
var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs);
string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}";
stdOutHandler?.Invoke(launchMessage);
if (Godot.OS.IsStdoutVerbose())
Console.WriteLine(launchMessage);
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
if (UsingMonoMsBuildOnWindows)
{
// These environment variables are required for Mono's MSBuild to find the compilers.
// We use the batch files in Mono's bin directory to make sure the compilers are executed with mono.
string monoWinBinDir = MonoWindowsBinDir;
startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat"));
startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat"));
startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat"));
}
// Needed when running from Developer Command Prompt for VS
RemovePlatformVariable(startInfo.EnvironmentVariables);
var process = new Process {StartInfo = startInfo};
if (stdOutHandler != null)
process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data);
if (stdErrHandler != null)
process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return process;
}
public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{
process.WaitForExit();
return process.ExitCode;
}
}
public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{
await process.WaitForExitAsync();
return process.ExitCode;
}
}
private static string BuildArguments(BuildTool buildTool, BuildInfo buildInfo)
{
string arguments = string.Empty;
if (buildTool == BuildTool.DotnetCli)
arguments += "msbuild"; // `dotnet msbuild` command
arguments += $@" ""{buildInfo.Solution}""";
if (buildInfo.Restore)
arguments += " /restore";
arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " +
$@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " +
$@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{buildInfo.LogsDirPath}""";
foreach (string customProperty in buildInfo.CustomProperties)
{
arguments += " /p:" + customProperty;
}
return arguments;
}
private static void RemovePlatformVariable(StringDictionary environmentVariables)
{
// EnvironmentVariables is case sensitive? Seriously?
var platformEnvironmentVariables = new List<string>();
foreach (string env in environmentVariables.Keys)
{
if (env.ToUpper() == "PLATFORM")
platformEnvironmentVariables.Add(env);
}
foreach (string env in platformEnvironmentVariables)
environmentVariables.Remove(env);
}
}
}

View File

@ -0,0 +1,10 @@
namespace GodotTools.Build
{
public enum BuildTool
{
MsBuildMono,
MsBuildVs,
JetBrainsMsBuild,
DotnetCli
}
}

View File

@ -0,0 +1,181 @@
using System;
using Godot;
using GodotTools.Internals;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
namespace GodotTools.Build
{
public class MSBuildPanel : VBoxContainer
{
public BuildOutputView BuildOutputView { get; private set; }
private MenuButton _buildMenuBtn;
private Button _errorsBtn;
private Button _warningsBtn;
private Button _viewLogBtn;
private void WarningsToggled(bool pressed)
{
BuildOutputView.WarningsVisible = pressed;
BuildOutputView.UpdateIssuesList();
}
private void ErrorsToggled(bool pressed)
{
BuildOutputView.ErrorsVisible = pressed;
BuildOutputView.UpdateIssuesList();
}
[UsedImplicitly]
public void BuildSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug"))
return; // Build failed
// Notify running game for hot-reload
Internal.ScriptEditorDebuggerReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
[UsedImplicitly]
private void RebuildSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Rebuild" }))
return; // Build failed
// Notify running game for hot-reload
Internal.ScriptEditorDebuggerReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
[UsedImplicitly]
private void CleanSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Clean" });
}
private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed;
private void BuildMenuOptionPressed(BuildMenuOptions id)
{
switch (id)
{
case BuildMenuOptions.BuildSolution:
BuildSolution();
break;
case BuildMenuOptions.RebuildSolution:
RebuildSolution();
break;
case BuildMenuOptions.CleanSolution:
CleanSolution();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option");
}
}
private enum BuildMenuOptions
{
BuildSolution,
RebuildSolution,
CleanSolution
}
public override void _Ready()
{
base._Ready();
RectMinSize = new Vector2(0, 228) * EditorScale;
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill };
AddChild(toolBarHBox);
_buildMenuBtn = new MenuButton { Text = "Build", Icon = GetIcon("Play", "EditorIcons") };
toolBarHBox.AddChild(_buildMenuBtn);
var buildMenu = _buildMenuBtn.GetPopup();
buildMenu.AddItem("Build Solution".TTR(), (int)BuildMenuOptions.BuildSolution);
buildMenu.AddItem("Rebuild Solution".TTR(), (int)BuildMenuOptions.RebuildSolution);
buildMenu.AddItem("Clean Solution".TTR(), (int)BuildMenuOptions.CleanSolution);
buildMenu.Connect("id_pressed", this, nameof(BuildMenuOptionPressed));
_errorsBtn = new Button
{
HintTooltip = "Show Errors".TTR(),
Icon = GetIcon("StatusError", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
_errorsBtn.Connect("toggled", this, nameof(ErrorsToggled));
toolBarHBox.AddChild(_errorsBtn);
_warningsBtn = new Button
{
HintTooltip = "Show Warnings".TTR(),
Icon = GetIcon("NodeWarning", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
_warningsBtn.Connect("toggled", this, nameof(WarningsToggled));
toolBarHBox.AddChild(_warningsBtn);
_viewLogBtn = new Button
{
Text = "Show Output".TTR(),
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
_viewLogBtn.Connect("toggled", this, nameof(ViewLogToggled));
toolBarHBox.AddChild(_viewLogBtn);
BuildOutputView = new BuildOutputView();
AddChild(BuildOutputView);
}
public override void _Notification(int what)
{
base._Notification(what);
if (what == NotificationThemeChanged)
{
if (_buildMenuBtn != null)
_buildMenuBtn.Icon = GetIcon("Play", "EditorIcons");
if (_errorsBtn != null)
_errorsBtn.Icon = GetIcon("StatusError", "EditorIcons");
if (_warningsBtn != null)
_warningsBtn.Icon = GetIcon("NodeWarning", "EditorIcons");
}
}
}
}

View File

@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.IO;
using Godot;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
using Directory = System.IO.Directory;
using Environment = System.Environment;
using File = System.IO.File;
using Path = System.IO.Path;
using OS = GodotTools.Utils.OS;
namespace GodotTools.Build
{
public static class MsBuildFinder
{
private static string _msbuildToolsPath = string.Empty;
private static string _msbuildUnixPath = string.Empty;
public static (string, BuildTool) FindMsBuild()
{
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool");
if (OS.IsWindows)
{
switch (buildTool)
{
case BuildTool.DotnetCli:
{
string dotnetCliPath = OS.PathWhich("dotnet");
if (!string.IsNullOrEmpty(dotnetCliPath))
return (dotnetCliPath, BuildTool.DotnetCli);
GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio.");
goto case BuildTool.MsBuildVs;
}
case BuildTool.MsBuildVs:
{
if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_msbuildToolsPath = FindMsBuildToolsPathOnWindows();
if (string.IsNullOrEmpty(_msbuildToolsPath))
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'.");
}
if (!_msbuildToolsPath.EndsWith("\\"))
_msbuildToolsPath += "\\";
return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs);
}
case BuildTool.MsBuildMono:
{
string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
if (!File.Exists(msbuildPath))
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}");
return (msbuildPath, BuildTool.MsBuildMono);
}
case BuildTool.JetBrainsMsBuild:
{
string editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName);
if (!File.Exists(editorPath))
throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}");
var riderDir = new FileInfo(editorPath).Directory?.Parent;
string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe");
if (!File.Exists(msbuildPath))
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}");
return (msbuildPath, BuildTool.JetBrainsMsBuild);
}
default:
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
}
if (OS.IsUnixLike)
{
switch (buildTool)
{
case BuildTool.DotnetCli:
{
string dotnetCliPath = FindBuildEngineOnUnix("dotnet");
if (!string.IsNullOrEmpty(dotnetCliPath))
return (dotnetCliPath, BuildTool.DotnetCli);
GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono.");
goto case BuildTool.MsBuildMono;
}
case BuildTool.MsBuildMono:
{
if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_msbuildUnixPath = FindBuildEngineOnUnix("msbuild");
}
if (string.IsNullOrEmpty(_msbuildUnixPath))
throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'");
return (_msbuildUnixPath, BuildTool.MsBuildMono);
}
default:
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
}
throw new PlatformNotSupportedException();
}
private static IEnumerable<string> MsBuildHintDirs
{
get
{
var result = new List<string>();
if (OS.IsOSX)
{
result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/");
result.Add("/opt/local/bin/");
result.Add("/usr/local/var/homebrew/linked/mono/bin/");
result.Add("/usr/local/bin/");
result.Add("/usr/local/bin/dotnet/");
result.Add("/usr/local/share/dotnet/");
}
result.Add("/opt/novell/mono/bin/");
return result;
}
}
private static string FindBuildEngineOnUnix(string name)
{
string ret = OS.PathWhich(name);
if (!string.IsNullOrEmpty(ret))
return ret;
string retFallback = OS.PathWhich($"{name}.exe");
if (!string.IsNullOrEmpty(retFallback))
return retFallback;
foreach (string hintDir in MsBuildHintDirs)
{
string hintPath = Path.Combine(hintDir, name);
if (File.Exists(hintPath))
return hintPath;
}
return string.Empty;
}
private static string FindMsBuildToolsPathOnWindows()
{
if (!OS.IsWindows)
throw new PlatformNotSupportedException();
// Try to find 15.0 with vswhere
string[] envNames = Internal.GodotIs32Bits() ?
envNames = new[] { "ProgramFiles", "ProgramW6432" } :
envNames = new[] { "ProgramFiles(x86)", "ProgramFiles" };
string vsWherePath = null;
foreach (var envName in envNames)
{
vsWherePath = Environment.GetEnvironmentVariable(envName);
if (!string.IsNullOrEmpty(vsWherePath))
{
vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
if (File.Exists(vsWherePath))
break;
}
vsWherePath = null;
}
var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"};
var outputArray = new Godot.Collections.Array<string>();
int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs,
blocking: true, output: (Godot.Collections.Array)outputArray);
if (exitCode != 0)
return string.Empty;
if (outputArray.Count == 0)
return string.Empty;
var lines = outputArray[0].Split('\n');
foreach (string line in lines)
{
int sepIdx = line.IndexOf(':');
if (sepIdx <= 0)
continue;
string key = line.Substring(0, sepIdx); // No need to trim
if (key != "installationPath")
continue;
string value = line.Substring(sepIdx + 1).StripEdges();
if (string.IsNullOrEmpty(value))
throw new FormatException("installationPath value is empty");
if (!value.EndsWith("\\"))
value += "\\";
// Since VS2019, the directory is simply named "Current"
string msbuildDir = Path.Combine(value, "MSBuild\\Current\\Bin");
if (Directory.Exists(msbuildDir))
return msbuildDir;
// Directory name "15.0" is used in VS 2017
return Path.Combine(value, "MSBuild\\15.0\\Bin");
}
return string.Empty;
}
}
}

View File

@ -0,0 +1,108 @@
using Godot;
using System;
using System.Linq;
using Godot.Collections;
using GodotTools.Internals;
using GodotTools.ProjectEditor;
using File = GodotTools.Utils.File;
using Directory = GodotTools.Utils.Directory;
namespace GodotTools
{
public static class CsProjOperations
{
public static string GenerateGameProject(string dir, string name)
{
try
{
return ProjectGenerator.GenAndSaveGameProject(dir, name);
}
catch (Exception e)
{
GD.PushError(e.ToString());
return string.Empty;
}
}
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static ulong ConvertToTimestamp(this DateTime value)
{
TimeSpan elapsedTime = value - Epoch;
return (ulong)elapsedTime.TotalSeconds;
}
private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata)
{
fileMetadata = null;
var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr);
if (parseError != Error.Ok)
{
GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}");
return false;
}
string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile);
var firstMatch = classes.FirstOrDefault(classDecl =>
classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object.
classDecl.SearchName == searchName // Filter by the name we're looking for
);
if (firstMatch == null)
return false; // Not found
fileMetadata = new Dictionary
{
["modified_time"] = $"{modifiedTime}",
["class"] = new Dictionary
{
["namespace"] = firstMatch.Namespace,
["class_name"] = firstMatch.Name,
["nested"] = firstMatch.Nested
}
};
return true;
}
public static void GenerateScriptsMetadata(string projectPath, string outputPath)
{
var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate();
bool IsUpToDate(string includeFile, ulong modifiedTime)
{
return metadataDict.TryGetValue(includeFile, out var oldFileVar) &&
ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string,
out ulong storedModifiedTime) && storedModifiedTime == modifiedTime;
}
var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile")
.Select(path => ("res://" + path).SimplifyGodotPath())
.ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp())
.Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value))
.ToArray();
foreach (var pair in outdatedFiles)
{
metadataDict.Remove(pair.Key);
string includeFile = pair.Key;
if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata))
metadataDict[includeFile] = fileMetadata;
}
string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict);
string baseDir = outputPath.GetBaseDir();
if (!Directory.Exists(baseDir))
Directory.CreateDirectory(baseDir);
File.WriteAllText(outputPath, json);
}
}
}

View File

@ -0,0 +1,834 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Godot;
using GodotTools.Core;
using GodotTools.Internals;
using Mono.Cecil;
using Directory = GodotTools.Utils.Directory;
using File = GodotTools.Utils.File;
using OS = GodotTools.Utils.OS;
using Path = System.IO.Path;
namespace GodotTools.Export
{
public struct AotOptions
{
public bool EnableLLVM;
public bool LLVMOnly;
public string LLVMPath;
public string LLVMOutputPath;
public bool FullAot;
private bool _useInterpreter;
public bool UseInterpreter
{
get => _useInterpreter && !LLVMOnly;
set => _useInterpreter = value;
}
public string[] ExtraAotOptions;
public string[] ExtraOptimizerOptions;
public string ToolchainPath;
}
public static class AotBuilder
{
public static void CompileAssemblies(ExportPlugin exporter, AotOptions aotOpts, string[] features, string platform, bool isDebug, string bclDir, string outputDir, string outputDataDir, IDictionary<string, string> assemblies)
{
// TODO: WASM
string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
if (!Directory.Exists(aotTempDir))
Directory.CreateDirectory(aotTempDir);
var assembliesPrepared = new Dictionary<string, string>();
foreach (var dependency in assemblies)
{
string assemblyName = dependency.Key;
string assemblyPath = dependency.Value;
string assemblyPathInBcl = Path.Combine(bclDir, assemblyName + ".dll");
if (File.Exists(assemblyPathInBcl))
{
// Don't create teporaries for assemblies from the BCL
assembliesPrepared.Add(assemblyName, assemblyPathInBcl);
}
else
{
string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll");
File.Copy(assemblyPath, tempAssemblyPath);
assembliesPrepared.Add(assemblyName, tempAssemblyPath);
}
}
if (platform == OS.Platforms.iOS)
{
string[] architectures = GetEnablediOSArchs(features).ToArray();
CompileAssembliesForiOS(exporter, isDebug, architectures, aotOpts, aotTempDir, assembliesPrepared, bclDir);
}
else if (platform == OS.Platforms.Android)
{
string[] abis = GetEnabledAndroidAbis(features).ToArray();
CompileAssembliesForAndroid(exporter, isDebug, abis, aotOpts, aotTempDir, assembliesPrepared, bclDir);
}
else
{
string bits = features.Contains("64") ? "64" : features.Contains("32") ? "32" : null;
CompileAssembliesForDesktop(exporter, platform, isDebug, bits, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir);
}
}
public static void CompileAssembliesForAndroid(ExportPlugin exporter, bool isDebug, string[] abis, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir)
{
foreach (var assembly in assemblies)
{
string assemblyName = assembly.Key;
string assemblyPath = assembly.Value;
// Not sure if the 'lib' prefix is an Android thing or just Godot being picky,
// but we use '-aot-' as well just in case to avoid conflicts with other libs.
string outputFileName = "lib-aot-" + assemblyName + ".dll.so";
foreach (string abi in abis)
{
string aotAbiTempDir = Path.Combine(aotTempDir, abi);
string soFilePath = Path.Combine(aotAbiTempDir, outputFileName);
var compilerArgs = GetAotCompilerArgs(OS.Platforms.Android, isDebug, abi, aotOpts, assemblyPath, soFilePath);
// Make sure the output directory exists
Directory.CreateDirectory(aotAbiTempDir);
string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.Android}-{abi}");
ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
// The Godot exporter expects us to pass the abi in the tags parameter
exporter.AddSharedObject(soFilePath, tags: new[] { abi });
}
}
}
public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string bits, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir)
{
foreach (var assembly in assemblies)
{
string assemblyName = assembly.Key;
string assemblyPath = assembly.Value;
string outputFileExtension = platform == OS.Platforms.Windows ? ".dll" :
platform == OS.Platforms.OSX ? ".dylib" :
".so";
string outputFileName = assemblyName + ".dll" + outputFileExtension;
string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName);
var compilerArgs = GetAotCompilerArgs(platform, isDebug, bits, aotOpts, assemblyPath, tempOutputFilePath);
string compilerDirPath = GetMonoCrossDesktopDirName(platform, bits);
ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
if (platform == OS.Platforms.OSX)
{
exporter.AddSharedObject(tempOutputFilePath, tags: null);
}
else
{
string libDir = platform == OS.Platforms.Windows ? "bin" : "lib";
string outputDataLibDir = Path.Combine(outputDataDir, "Mono", libDir);
File.Copy(tempOutputFilePath, Path.Combine(outputDataLibDir, outputFileName));
}
}
}
public static void CompileAssembliesForiOS(ExportPlugin exporter, bool isDebug, string[] architectures, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir)
{
void RunAr(IEnumerable<string> objFilePaths, string outputFilePath)
{
var arArgs = new List<string>()
{
"cr",
outputFilePath
};
foreach (string objFilePath in objFilePaths)
arArgs.Add(objFilePath);
int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs);
if (arExitCode != 0)
throw new Exception($"Command 'ar' exited with code: {arExitCode}");
}
void RunLipo(IEnumerable<string> libFilePaths, string outputFilePath)
{
var lipoArgs = new List<string>();
lipoArgs.Add("-create");
lipoArgs.AddRange(libFilePaths);
lipoArgs.Add("-output");
lipoArgs.Add(outputFilePath);
int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs);
if (lipoExitCode != 0)
throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}");
}
void CreateDummyLibForSimulator(string name, string xcFrameworkPath = null)
{
xcFrameworkPath = xcFrameworkPath ?? MonoFrameworkFromTemplate(name);
string simulatorSubDir = Path.Combine(xcFrameworkPath, "ios-arm64_x86_64-simulator");
string libFilePath = Path.Combine(simulatorSubDir, name + ".a");
if (File.Exists(libFilePath))
return;
string CompileForArch(string arch)
{
string baseFilePath = Path.Combine(aotTempDir, $"{name}.{arch}");
string sourceFilePath = baseFilePath + ".c";
string source = $"int _{AssemblyNameToAotSymbol(name)}() {{ return 0; }}\n";
File.WriteAllText(sourceFilePath, source);
const string iOSPlatformName = "iPhoneSimulator";
const string versionMin = "10.0";
string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath,
$"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk");
string objFilePath = baseFilePath + ".o";
var clangArgs = new[]
{
"-isysroot", iOSSdkPath,
$"-miphonesimulator-version-min={versionMin}",
"-arch", arch,
"-c",
"-o", objFilePath,
sourceFilePath
};
int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs);
if (clangExitCode != 0)
throw new Exception($"Command 'clang' exited with code: {clangExitCode}");
string arOutputFilePath = Path.Combine(aotTempDir, baseFilePath + ".a");
RunAr(new[] {objFilePath}, arOutputFilePath);
return arOutputFilePath;
}
RunLipo(new[] {CompileForArch("arm64"), CompileForArch("x86_64")}, libFilePath);
}
string projectAssemblyName = GodotSharpDirs.ProjectAssemblyName;
string libAotName = $"lib-aot-{projectAssemblyName}";
string libAotXcFrameworkPath = Path.Combine(aotTempDir, $"{libAotName}.xcframework");
string libAotXcFrameworkDevicePath = Path.Combine(libAotXcFrameworkPath, "ios-arm64");
string libAotXcFrameworkSimPath = Path.Combine(libAotXcFrameworkPath, "ios-arm64_x86_64-simulator");
Directory.CreateDirectory(libAotXcFrameworkPath);
Directory.CreateDirectory(libAotXcFrameworkDevicePath);
Directory.CreateDirectory(libAotXcFrameworkSimPath);
string libAotFileName = $"{libAotName}.a";
string libAotFilePath = Path.Combine(libAotXcFrameworkDevicePath, libAotFileName);
var cppCode = new StringBuilder();
var aotModuleInfoSymbols = new List<string>(assemblies.Count);
var aotObjFilePaths = new List<string>(assemblies.Count);
string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers",
$"{OS.Platforms.iOS}-arm64");
string crossCompiler = FindCrossCompiler(compilerDirPath);
string aotCacheDir = Path.Combine(ProjectSettings.GlobalizePath(GodotSharpDirs.ResTempDir),
"obj", isDebug ? "ExportDebug" : "ExportRelease", "godot-aot-cache");
if (!Directory.Exists(aotCacheDir))
Directory.CreateDirectory(aotCacheDir);
var aotCache = new AotCache(Path.Combine(aotCacheDir, "cache.json"));
try
{
foreach (var assembly in assemblies)
{
string assemblyName = assembly.Key;
string assemblyPath = assembly.Value;
string asmFilePath = Path.Combine(aotCacheDir, assemblyName + ".dll.S");
string objFilePath = Path.Combine(aotCacheDir, assemblyName + ".dll.o");
aotCache.RunCached(name: assemblyName, input: assemblyPath, output: objFilePath, () =>
{
Console.WriteLine($"AOT compiler: Compiling '{assemblyName}'...");
var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug,
"arm64", aotOpts, assemblyPath, asmFilePath);
ExecuteCompiler(crossCompiler, compilerArgs, bclDir);
// Assembling
const string iOSPlatformName = "iPhoneOS";
const string versionMin = "10.0"; // TODO: Turn this hard-coded version into an exporter setting
string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath,
$"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk");
var clangArgs = new List<string>()
{
"-isysroot", iOSSdkPath,
"-Qunused-arguments",
$"-miphoneos-version-min={versionMin}",
"-arch", "arm64",
"-c",
"-o", objFilePath,
"-x", "assembler"
};
if (isDebug)
clangArgs.Add("-DDEBUG");
clangArgs.Add(asmFilePath);
int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs);
if (clangExitCode != 0)
throw new Exception($"Command 'clang' exited with code: {clangExitCode}");
});
aotObjFilePaths.Add(objFilePath);
aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info");
}
}
finally
{
aotCache.SaveCache();
}
RunAr(aotObjFilePaths, libAotFilePath);
// Archive the AOT object files into a static library
File.WriteAllText(Path.Combine(libAotXcFrameworkPath, "Info.plist"),
$@"<?xml version=""1.0"" encoding=""UTF-8""?>
<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">
<plist version=""1.0"">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>{libAotFileName}</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>{libAotFileName}</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>
");
// Add the fat AOT static library to the Xcode project
CreateDummyLibForSimulator(libAotName, libAotXcFrameworkPath);
exporter.AddIosProjectStaticLib(libAotXcFrameworkPath);
// Generate driver code
cppCode.AppendLine("#include <TargetConditionals.h>");
cppCode.AppendLine("#if !TARGET_OS_SIMULATOR");
cppCode.AppendLine("extern \"C\" {");
cppCode.AppendLine("// Mono API");
cppCode.AppendLine(@"
typedef enum {
MONO_AOT_MODE_NONE,
MONO_AOT_MODE_NORMAL,
MONO_AOT_MODE_HYBRID,
MONO_AOT_MODE_FULL,
MONO_AOT_MODE_LLVMONLY,
MONO_AOT_MODE_INTERP,
MONO_AOT_MODE_INTERP_LLVMONLY,
MONO_AOT_MODE_LLVMONLY_INTERP,
MONO_AOT_MODE_LAST = 1000,
} MonoAotMode;");
cppCode.AppendLine("void mono_jit_set_aot_mode(MonoAotMode);");
cppCode.AppendLine("void mono_aot_register_module(void *);");
if (aotOpts.UseInterpreter)
{
cppCode.AppendLine("void mono_ee_interp_init(const char *);");
cppCode.AppendLine("void mono_icall_table_init();");
cppCode.AppendLine("void mono_marshal_ilgen_init();");
cppCode.AppendLine("void mono_method_builder_ilgen_init();");
cppCode.AppendLine("void mono_sgen_mono_ilgen_init();");
}
foreach (string symbol in aotModuleInfoSymbols)
cppCode.AppendLine($"extern void *{symbol};");
cppCode.AppendLine("void gd_mono_setup_aot() {");
foreach (string symbol in aotModuleInfoSymbols)
cppCode.AppendLine($"\tmono_aot_register_module({symbol});");
if (aotOpts.UseInterpreter)
{
cppCode.AppendLine("\tmono_icall_table_init();");
cppCode.AppendLine("\tmono_marshal_ilgen_init();");
cppCode.AppendLine("\tmono_method_builder_ilgen_init();");
cppCode.AppendLine("\tmono_sgen_mono_ilgen_init();");
cppCode.AppendLine("\tmono_ee_interp_init(0);");
}
string aotModeStr = null;
if (aotOpts.LLVMOnly)
{
aotModeStr = "MONO_AOT_MODE_LLVMONLY"; // --aot=llvmonly
}
else
{
if (aotOpts.UseInterpreter)
aotModeStr = "MONO_AOT_MODE_INTERP"; // --aot=interp or --aot=interp,full
else if (aotOpts.FullAot)
aotModeStr = "MONO_AOT_MODE_FULL"; // --aot=full
}
// One of the options above is always set for iOS
Debug.Assert(aotModeStr != null);
cppCode.AppendLine($"\tmono_jit_set_aot_mode({aotModeStr});");
cppCode.AppendLine("} // gd_mono_setup_aot");
// Prevent symbols from being stripped
var symbols = CollectSymbols(assemblies);
foreach (string symbol in symbols)
{
cppCode.Append("extern void *");
cppCode.Append(symbol);
cppCode.AppendLine(";");
}
cppCode.AppendLine("__attribute__((used)) __attribute__((optnone)) static void __godot_symbol_referencer() {");
cppCode.AppendLine("\tvoid *aux;");
foreach (string symbol in symbols)
{
cppCode.Append("\taux = ");
cppCode.Append(symbol);
cppCode.AppendLine(";");
}
cppCode.AppendLine("} // __godot_symbol_referencer");
cppCode.AppendLine("} // extern \"C\"");
cppCode.AppendLine("#endif // !TARGET_OS_SIMULATOR");
// Add the driver code to the Xcode project
exporter.AddIosCppCode(cppCode.ToString());
// Add the required Mono libraries to the Xcode project
string MonoLibFile(string libFileName) => libFileName + ".iphone.fat.a";
string MonoLibFromTemplate(string libFileName) =>
Path.Combine(Internal.FullTemplatesDir, "iphone-mono-libs", MonoLibFile(libFileName));
string MonoFrameworkFile(string frameworkFileName) => frameworkFileName + ".xcframework";
string MonoFrameworkFromTemplate(string frameworkFileName) =>
Path.Combine(Internal.FullTemplatesDir, "iphone-mono-libs", MonoFrameworkFile(frameworkFileName));
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmonosgen-2.0"));
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-native"));
if (aotOpts.UseInterpreter)
{
CreateDummyLibForSimulator("libmono-ee-interp");
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-ee-interp"));
CreateDummyLibForSimulator("libmono-icall-table");
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-icall-table"));
CreateDummyLibForSimulator("libmono-ilgen");
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-ilgen"));
}
// TODO: Turn into an exporter option
bool enableProfiling = false;
if (enableProfiling)
exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-profiler-log"));
// Add frameworks required by Mono to the Xcode project
exporter.AddIosFramework("libiconv.tbd");
exporter.AddIosFramework("GSS.framework");
exporter.AddIosFramework("CFNetwork.framework");
if (!aotOpts.UseInterpreter)
exporter.AddIosFramework("SystemConfiguration.framework");
}
private static List<string> CollectSymbols(IDictionary<string, string> assemblies)
{
var symbols = new List<string>();
var resolver = new DefaultAssemblyResolver();
foreach (var searchDir in resolver.GetSearchDirectories())
resolver.RemoveSearchDirectory(searchDir);
foreach (var searchDir in assemblies
.Select(a => a.Value.GetBaseDir().NormalizePath()).Distinct())
{
resolver.AddSearchDirectory(searchDir);
}
AssemblyDefinition ReadAssembly(string fileName)
=> AssemblyDefinition.ReadAssembly(fileName,
new ReaderParameters {AssemblyResolver = resolver});
foreach (var assembly in assemblies)
{
using (var assemblyDef = ReadAssembly(assembly.Value))
CollectSymbolsFromAssembly(assemblyDef, symbols);
}
return symbols;
}
private static void CollectSymbolsFromAssembly(AssemblyDefinition assembly, ICollection<string> symbols)
{
if (!assembly.MainModule.HasTypes)
return;
foreach (var type in assembly.MainModule.Types)
{
CollectSymbolsFromType(type, symbols);
}
}
private static void CollectSymbolsFromType(TypeDefinition type, ICollection<string> symbols)
{
if (type.HasNestedTypes)
{
foreach (var nestedType in type.NestedTypes)
CollectSymbolsFromType(nestedType, symbols);
}
if (type.Module.HasModuleReferences)
CollectPInvokeSymbols(type, symbols);
}
private static void CollectPInvokeSymbols(TypeDefinition type, ICollection<string> symbols)
{
if (!type.HasMethods)
return;
foreach (var method in type.Methods)
{
if (!method.IsPInvokeImpl || !method.HasPInvokeInfo)
continue;
var pInvokeInfo = method.PInvokeInfo;
if (pInvokeInfo == null)
continue;
switch (pInvokeInfo.Module.Name)
{
case "__Internal":
case "libSystem.Net.Security.Native":
case "System.Net.Security.Native":
case "libSystem.Security.Cryptography.Native.Apple":
case "System.Security.Cryptography.Native.Apple":
case "libSystem.Native":
case "System.Native":
case "libSystem.Globalization.Native":
case "System.Globalization.Native":
{
symbols.Add(pInvokeInfo.EntryPoint);
break;
}
}
}
}
/// Converts an assembly name to a valid symbol name in the same way the AOT compiler does
private static string AssemblyNameToAotSymbol(string assemblyName)
{
var builder = new StringBuilder();
foreach (char @char in assemblyName)
builder.Append(char.IsLetterOrDigit(@char) || @char == '_' ? @char : '_');
return builder.ToString();
}
private static IEnumerable<string> GetAotCompilerArgs(string platform, bool isDebug, string target, AotOptions aotOpts, string assemblyPath, string outputFilePath)
{
// TODO: LLVM
bool aotSoftDebug = isDebug && !aotOpts.EnableLLVM;
bool aotDwarfDebug = platform == OS.Platforms.iOS;
var aotOptions = new List<string>();
var optimizerOptions = new List<string>();
if (aotOpts.LLVMOnly)
{
aotOptions.Add("llvmonly");
}
else
{
// Can be both 'interp' and 'full'
if (aotOpts.UseInterpreter)
aotOptions.Add("interp");
if (aotOpts.FullAot)
aotOptions.Add("full");
}
aotOptions.Add(aotSoftDebug ? "soft-debug" : "nodebug");
if (aotDwarfDebug)
aotOptions.Add("dwarfdebug");
if (platform == OS.Platforms.Android)
{
string abi = target;
string androidToolchain = aotOpts.ToolchainPath;
if (string.IsNullOrEmpty(androidToolchain))
{
androidToolchain = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "android-toolchains", $"{abi}"); // TODO: $"{abi}-{apiLevel}{(clang?"clang":"")}"
if (!Directory.Exists(androidToolchain))
throw new FileNotFoundException("Missing android toolchain. Specify one in the AOT export settings.");
}
else if (!Directory.Exists(androidToolchain))
{
throw new FileNotFoundException("Android toolchain not found: " + androidToolchain);
}
var androidToolPrefixes = new Dictionary<string, string>
{
["armeabi-v7a"] = "arm-linux-androideabi-",
["arm64-v8a"] = "aarch64-linux-android-",
["x86"] = "i686-linux-android-",
["x86_64"] = "x86_64-linux-android-"
};
aotOptions.Add("tool-prefix=" + Path.Combine(androidToolchain, "bin", androidToolPrefixes[abi]));
string triple = GetAndroidTriple(abi);
aotOptions.Add($"mtriple={triple}");
}
else if (platform == OS.Platforms.iOS)
{
if (!aotOpts.LLVMOnly && !aotOpts.UseInterpreter)
optimizerOptions.Add("gsharedvt");
aotOptions.Add("static");
// I couldn't get the Mono cross-compiler to do assembling, so we'll have to do it ourselves
aotOptions.Add("asmonly");
aotOptions.Add("direct-icalls");
if (aotSoftDebug)
aotOptions.Add("no-direct-calls");
if (aotOpts.LLVMOnly || !aotOpts.UseInterpreter)
aotOptions.Add("direct-pinvoke");
string arch = target;
aotOptions.Add($"mtriple={arch}-ios");
}
aotOptions.Add($"outfile={outputFilePath}");
if (aotOpts.EnableLLVM)
{
aotOptions.Add($"llvm-path={aotOpts.LLVMPath}");
aotOptions.Add($"llvm-outfile={aotOpts.LLVMOutputPath}");
}
if (aotOpts.ExtraAotOptions.Length > 0)
aotOptions.AddRange(aotOpts.ExtraAotOptions);
if (aotOpts.ExtraOptimizerOptions.Length > 0)
optimizerOptions.AddRange(aotOpts.ExtraOptimizerOptions);
string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option;
string OptionsToString(IEnumerable<string> options) => string.Join(",", options.Select(EscapeOption));
var runtimeArgs = new List<string>();
// The '--debug' runtime option is required when using the 'soft-debug' and 'dwarfdebug' AOT options
if (aotSoftDebug || aotDwarfDebug)
runtimeArgs.Add("--debug");
if (aotOpts.EnableLLVM)
runtimeArgs.Add("--llvm");
runtimeArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot");
if (optimizerOptions.Count > 0)
runtimeArgs.Add($"-O={OptionsToString(optimizerOptions)}");
runtimeArgs.Add(assemblyPath);
return runtimeArgs;
}
private static void ExecuteCompiler(string compiler, IEnumerable<string> compilerArgs, string bclDir)
{
// TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
string CmdLineArgsToString(IEnumerable<string> args)
{
// Not perfect, but as long as we are careful...
return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
}
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo(compiler, CmdLineArgsToString(compilerArgs))
{
UseShellExecute = false
};
process.StartInfo.EnvironmentVariables.Remove("MONO_ENV_OPTIONS");
process.StartInfo.EnvironmentVariables.Remove("MONO_THREADS_SUSPEND");
process.StartInfo.EnvironmentVariables.Add("MONO_PATH", bclDir);
Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}");
if (!process.Start())
throw new Exception("Failed to start process for Mono AOT compiler");
process.WaitForExit();
if (process.ExitCode != 0)
throw new Exception($"Mono AOT compiler exited with code: {process.ExitCode}");
}
}
private static IEnumerable<string> GetEnablediOSArchs(string[] features)
{
var iosArchs = new[]
{
"armv7",
"arm64"
};
return iosArchs.Where(features.Contains);
}
private static IEnumerable<string> GetEnabledAndroidAbis(string[] features)
{
var androidAbis = new[]
{
"armeabi-v7a",
"arm64-v8a",
"x86",
"x86_64"
};
return androidAbis.Where(features.Contains);
}
private static string GetAndroidTriple(string abi)
{
var abiArchs = new Dictionary<string, string>
{
["armeabi-v7a"] = "armv7",
["arm64-v8a"] = "aarch64-v8a",
["x86"] = "i686",
["x86_64"] = "x86_64"
};
string arch = abiArchs[abi];
return $"{arch}-linux-android";
}
private static string GetMonoCrossDesktopDirName(string platform, string bits)
{
switch (platform)
{
case OS.Platforms.Windows:
case OS.Platforms.UWP:
{
string arch = bits == "64" ? "x86_64" : "i686";
return $"windows-{arch}";
}
case OS.Platforms.OSX:
{
Debug.Assert(bits == null || bits == "64");
string arch = "x86_64";
return $"{platform}-{arch}";
}
case OS.Platforms.X11:
case OS.Platforms.Server:
{
string arch = bits == "64" ? "x86_64" : "i686";
return $"linux-{arch}";
}
case OS.Platforms.Haiku:
{
string arch = bits == "64" ? "x86_64" : "i686";
return $"{platform}-{arch}";
}
default:
throw new NotSupportedException($"Platform not supported: {platform}");
}
}
// TODO: Replace this for a specific path for each platform
private static string FindCrossCompiler(string monoCrossBin)
{
string exeExt = OS.IsWindows ? ".exe" : string.Empty;
var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}", SearchOption.TopDirectoryOnly);
if (files.Length > 0)
return Path.Combine(monoCrossBin, files[0].Name);
throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}");
}
}
}

View File

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
namespace GodotTools.Export
{
public class AotCache
{
private readonly string _cacheFilePath;
private readonly Cache _cache = new Cache();
private bool _hasUnsavedChanges = false;
public AotCache(string cacheFilePath)
{
_cacheFilePath = cacheFilePath;
if (File.Exists(_cacheFilePath))
LoadCache(_cacheFilePath, out _cache);
}
private static byte[] ComputeSha256Checksum(string filePath)
{
using (var sha256 = SHA256.Create())
{
using (var streamReader = File.OpenRead(filePath))
return sha256.ComputeHash(streamReader);
}
}
private static bool CompareHashes(byte[] a, byte[] b)
{
if (a.Length != b.Length)
return false;
for (int i = 0; i < a.Length; i++)
{
if (a[i] != b[i])
return false;
}
return true;
}
private class Cache
{
[JsonProperty("assemblies")]
public Dictionary<string, CachedChecksums> Assemblies { get; set; } =
new Dictionary<string, CachedChecksums>();
}
private struct CachedChecksums
{
[JsonProperty("input_checksum")] public string InputChecksumBase64 { get; set; }
[JsonProperty("output_checksum")] public string OutputChecksumBase64 { get; set; }
}
private static void LoadCache(string cacheFilePath, out Cache cache)
{
using (var streamReader = new StreamReader(cacheFilePath, Encoding.UTF8))
using (var jsonReader = new JsonTextReader(streamReader))
{
cache = new JsonSerializer().Deserialize<Cache>(jsonReader);
}
}
private static void SaveCache(string cacheFilePath, Cache cache)
{
using (var streamWriter = new StreamWriter(cacheFilePath, append: false, Encoding.UTF8))
using (var jsonWriter = new JsonTextWriter(streamWriter))
{
new JsonSerializer().Serialize(jsonWriter, cache);
}
}
private bool TryGetCachedChecksums(string name, out CachedChecksums cachedChecksums)
=> _cache.Assemblies.TryGetValue(name, out cachedChecksums);
private void ChangeCache(string name, byte[] inputChecksum, byte[] outputChecksum)
{
_cache.Assemblies[name] = new CachedChecksums()
{
InputChecksumBase64 = Convert.ToBase64String(inputChecksum),
OutputChecksumBase64 = Convert.ToBase64String(outputChecksum)
};
_hasUnsavedChanges = true;
}
public void SaveCache()
{
if (!_hasUnsavedChanges)
return;
SaveCache(_cacheFilePath, _cache);
_hasUnsavedChanges = false;
}
private bool IsCached(string name, byte[] inputChecksum, string output)
{
if (!File.Exists(output))
{
return false;
}
if (!TryGetCachedChecksums(name, out var cachedChecksums))
return false;
if (string.IsNullOrEmpty(cachedChecksums.InputChecksumBase64) ||
string.IsNullOrEmpty(cachedChecksums.OutputChecksumBase64))
return false;
var cachedInputChecksum = Convert.FromBase64String(cachedChecksums.InputChecksumBase64);
if (!CompareHashes(inputChecksum, cachedInputChecksum))
return false;
var outputChecksum = ComputeSha256Checksum(output);
var cachedOutputChecksum = Convert.FromBase64String(cachedChecksums.OutputChecksumBase64);
if (!CompareHashes(outputChecksum, cachedOutputChecksum))
return false;
return true;
}
public void RunCached(string name, string input, string output, Action action)
{
var inputChecksum = ComputeSha256Checksum(input);
if (IsCached(name, inputChecksum, output))
{
Console.WriteLine($"AOT compiler cache: '{name}' already compiled.");
return;
}
action();
var outputChecksum = ComputeSha256Checksum(output);
ChangeCache(name, inputChecksum, outputChecksum);
}
}
}

View File

@ -0,0 +1,485 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using GodotTools.Build;
using GodotTools.Core;
using GodotTools.Internals;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using Directory = GodotTools.Utils.Directory;
using File = GodotTools.Utils.File;
using OS = GodotTools.Utils.OS;
using Path = System.IO.Path;
namespace GodotTools.Export
{
public class ExportPlugin : EditorExportPlugin
{
[Flags]
private enum I18NCodesets
{
None = 0,
CJK = 1,
MidEast = 2,
Other = 4,
Rare = 8,
West = 16,
All = CJK | MidEast | Other | Rare | West
}
private string _maybeLastExportError;
private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir)
{
var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets");
if (codesets == I18NCodesets.None)
return;
void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll"));
AddI18NAssembly("I18N");
if ((codesets & I18NCodesets.CJK) != 0)
AddI18NAssembly("I18N.CJK");
if ((codesets & I18NCodesets.MidEast) != 0)
AddI18NAssembly("I18N.MidEast");
if ((codesets & I18NCodesets.Other) != 0)
AddI18NAssembly("I18N.Other");
if ((codesets & I18NCodesets.Rare) != 0)
AddI18NAssembly("I18N.Rare");
if ((codesets & I18NCodesets.West) != 0)
AddI18NAssembly("I18N.West");
}
public void RegisterExportSettings()
{
// TODO: These would be better as export preset options, but that doesn't seem to be supported yet
GlobalDef("mono/export/include_scripts_content", false);
GlobalDef("mono/export/export_assemblies_inside_pck", true);
GlobalDef("mono/export/i18n_codesets", I18NCodesets.All);
ProjectSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = Variant.Type.Int,
["name"] = "mono/export/i18n_codesets",
["hint"] = PropertyHint.Flags,
["hint_string"] = "CJK,MidEast,Other,Rare,West"
});
GlobalDef("mono/export/aot/enabled", false);
GlobalDef("mono/export/aot/full_aot", false);
GlobalDef("mono/export/aot/use_interpreter", true);
// --aot or --aot=opt1,opt2 (use 'mono --aot=help AuxAssembly.dll' to list AOT options)
GlobalDef("mono/export/aot/extra_aot_options", Array.Empty<string>());
// --optimize/-O=opt1,opt2 (use 'mono --list-opt'' to list optimize options)
GlobalDef("mono/export/aot/extra_optimizer_options", Array.Empty<string>());
GlobalDef("mono/export/aot/android_toolchain_path", "");
}
private void AddFile(string srcPath, string dstPath, bool remap = false)
{
// Add file to the PCK
AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap);
}
// With this method we can override how a file is exported in the PCK
public override void _ExportFile(string path, string type, string[] features)
{
base._ExportFile(path, type, features);
if (type != Internal.CSharpLanguageType)
return;
if (Path.GetExtension(path) != Internal.CSharpLanguageExtension)
throw new ArgumentException($"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path));
// TODO What if the source file is not part of the game's C# project
bool includeScriptsContent = (bool)ProjectSettings.GetSetting("mono/export/include_scripts_content");
if (!includeScriptsContent)
{
// We don't want to include the source code on exported games.
// Sadly, Godot prints errors when adding an empty file (nothing goes wrong, it's just noise).
// Because of this, we add a file which contains a line break.
AddFile(path, System.Text.Encoding.UTF8.GetBytes("\n"), remap: false);
// Tell the Godot exporter that we already took care of the file
Skip();
}
}
public override void _ExportBegin(string[] features, bool isDebug, string path, int flags)
{
base._ExportBegin(features, isDebug, path, flags);
try
{
_ExportBeginImpl(features, isDebug, path, flags);
}
catch (Exception e)
{
_maybeLastExportError = e.Message;
// 'maybeLastExportError' cannot be null or empty if there was an error, so we
// must consider the possibility of exceptions being thrown without a message.
if (string.IsNullOrEmpty(_maybeLastExportError))
_maybeLastExportError = $"Exception thrown: {e.GetType().Name}";
GD.PushError($"Failed to export project: {_maybeLastExportError}");
Console.Error.WriteLine(e);
// TODO: Do something on error once _ExportBegin supports failing.
}
}
private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
{
_ = flags; // Unused
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return;
if (!DeterminePlatformFromFeatures(features, out string platform))
throw new NotSupportedException("Target platform not supported");
string outputDir = new FileInfo(path).Directory?.FullName ??
throw new FileNotFoundException("Base directory not found");
string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
AddFile(scriptsMetadataPath, scriptsMetadataPath);
if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
throw new Exception("Failed to build project");
// Add dependency assemblies
var assemblies = new Godot.Collections.Dictionary<string, string>();
string projectDllName = GodotSharpDirs.ProjectAssemblyName;
string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig);
string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll");
assemblies[projectDllName] = projectDllSrcPath;
string bclDir = DeterminePlatformBclDir(platform);
if (platform == OS.Platforms.Android)
{
string godotAndroidExtProfileDir = GetBclProfileDir("godot_android_ext");
string monoAndroidAssemblyPath = Path.Combine(godotAndroidExtProfileDir, "Mono.Android.dll");
if (!File.Exists(monoAndroidAssemblyPath))
throw new FileNotFoundException("Assembly not found: 'Mono.Android'", monoAndroidAssemblyPath);
assemblies["Mono.Android"] = monoAndroidAssemblyPath;
}
else if (platform == OS.Platforms.HTML5)
{
// Ideally these would be added automatically since they're referenced by the wasm BCL assemblies.
// However, at least in the case of 'WebAssembly.Net.Http' for some reason the BCL assemblies
// reference a different version even though the assembly is the same, for some weird reason.
var wasmFrameworkAssemblies = new[] { "WebAssembly.Bindings", "WebAssembly.Net.WebSockets" };
foreach (string thisWasmFrameworkAssemblyName in wasmFrameworkAssemblies)
{
string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName + ".dll");
if (!File.Exists(thisWasmFrameworkAssemblyPath))
throw new FileNotFoundException($"Assembly not found: '{thisWasmFrameworkAssemblyName}'", thisWasmFrameworkAssemblyPath);
assemblies[thisWasmFrameworkAssemblyName] = thisWasmFrameworkAssemblyPath;
}
// Assemblies that can have a different name in a newer version. Newer version must come first and it has priority.
(string newName, string oldName)[] wasmFrameworkAssembliesOneOf = new[]
{
("System.Net.Http.WebAssemblyHttpHandler", "WebAssembly.Net.Http")
};
foreach (var thisWasmFrameworkAssemblyName in wasmFrameworkAssembliesOneOf)
{
string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName.newName + ".dll");
if (File.Exists(thisWasmFrameworkAssemblyPath))
{
assemblies[thisWasmFrameworkAssemblyName.newName] = thisWasmFrameworkAssemblyPath;
}
else
{
thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName.oldName + ".dll");
if (!File.Exists(thisWasmFrameworkAssemblyPath))
{
throw new FileNotFoundException("Expected one of the following assemblies but none were found: " +
$"'{thisWasmFrameworkAssemblyName.newName}' / '{thisWasmFrameworkAssemblyName.oldName}'",
thisWasmFrameworkAssemblyPath);
}
assemblies[thisWasmFrameworkAssemblyName.oldName] = thisWasmFrameworkAssemblyPath;
}
}
}
var initialAssemblies = assemblies.Duplicate();
internal_GetExportedAssemblyDependencies(initialAssemblies, buildConfig, bclDir, assemblies);
AddI18NAssemblies(assemblies, bclDir);
string outputDataDir = null;
if (PlatformHasTemplateDir(platform))
outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir);
string apiConfig = isDebug ? "Debug" : "Release";
string resAssembliesDir = Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig);
bool assembliesInsidePck = (bool)ProjectSettings.GetSetting("mono/export/export_assemblies_inside_pck") || outputDataDir == null;
if (!assembliesInsidePck)
{
string outputDataGameAssembliesDir = Path.Combine(outputDataDir, "Assemblies");
if (!Directory.Exists(outputDataGameAssembliesDir))
Directory.CreateDirectory(outputDataGameAssembliesDir);
}
foreach (var assembly in assemblies)
{
void AddToAssembliesDir(string fileSrcPath)
{
if (assembliesInsidePck)
{
string fileDstPath = Path.Combine(resAssembliesDir, fileSrcPath.GetFile());
AddFile(fileSrcPath, fileDstPath);
}
else
{
Debug.Assert(outputDataDir != null);
string fileDstPath = Path.Combine(outputDataDir, "Assemblies", fileSrcPath.GetFile());
File.Copy(fileSrcPath, fileDstPath);
}
}
string assemblySrcPath = assembly.Value;
string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null);
string pdbSrcPath = assemblyPathWithoutExtension + ".pdb";
AddToAssembliesDir(assemblySrcPath);
if (File.Exists(pdbSrcPath))
AddToAssembliesDir(pdbSrcPath);
}
// AOT compilation
bool aotEnabled = platform == OS.Platforms.iOS || (bool)ProjectSettings.GetSetting("mono/export/aot/enabled");
if (aotEnabled)
{
string aotToolchainPath = null;
if (platform == OS.Platforms.Android)
aotToolchainPath = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path");
if (aotToolchainPath == string.Empty)
aotToolchainPath = null; // Don't risk it being used as current working dir
// TODO: LLVM settings are hard-coded and disabled for now
var aotOpts = new AotOptions
{
EnableLLVM = false,
LLVMOnly = false,
LLVMPath = "",
LLVMOutputPath = "",
FullAot = platform == OS.Platforms.iOS || (bool)(ProjectSettings.GetSetting("mono/export/aot/full_aot") ?? false),
UseInterpreter = (bool)ProjectSettings.GetSetting("mono/export/aot/use_interpreter"),
ExtraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options") ?? Array.Empty<string>(),
ExtraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? Array.Empty<string>(),
ToolchainPath = aotToolchainPath
};
AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, assemblies);
}
}
public override void _ExportEnd()
{
base._ExportEnd();
string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
if (Directory.Exists(aotTempDir))
Directory.Delete(aotTempDir, recursive: true);
// TODO: Just a workaround until the export plugins can be made to abort with errors
if (!string.IsNullOrEmpty(_maybeLastExportError)) // Check empty as well, because it's set to empty after hot-reloading
{
string lastExportError = _maybeLastExportError;
_maybeLastExportError = null;
GodotSharpEditor.Instance.ShowErrorDialog(lastExportError, "Failed to export C# project");
}
}
[NotNull]
private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir)
{
string target = isDebug ? "release_debug" : "release";
// NOTE: Bits is ok for now as all platforms with a data directory only have one or two architectures.
// However, this may change in the future if we add arm linux or windows desktop templates.
string bits = features.Contains("64") ? "64" : "32";
string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}";
string templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName());
bool validTemplatePathFound = true;
if (!Directory.Exists(templateDirPath))
{
validTemplatePathFound = false;
if (isDebug)
{
target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name
templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName());
validTemplatePathFound = true;
if (!Directory.Exists(templateDirPath))
validTemplatePathFound = false;
}
}
if (!validTemplatePathFound)
throw new FileNotFoundException("Data template directory not found", templateDirPath);
string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject());
if (Directory.Exists(outputDataDir))
Directory.Delete(outputDataDir, recursive: true); // Clean first
Directory.CreateDirectory(outputDataDir);
foreach (string dir in Directory.GetDirectories(templateDirPath, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(templateDirPath.Length + 1)));
}
foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories))
{
File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1)));
}
return outputDataDir;
}
private static bool PlatformHasTemplateDir(string platform)
{
// OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest.
return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform);
}
private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform)
{
foreach (var feature in features)
{
if (OS.PlatformNameMap.TryGetValue(feature, out platform))
return true;
}
platform = null;
return false;
}
private static string GetBclProfileDir(string profile)
{
string templatesDir = Internal.FullTemplatesDir;
return Path.Combine(templatesDir, "bcl", profile);
}
private static string DeterminePlatformBclDir(string platform)
{
string templatesDir = Internal.FullTemplatesDir;
string platformBclDir = Path.Combine(templatesDir, "bcl", platform);
if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll")))
{
string profile = DeterminePlatformBclProfile(platform);
platformBclDir = Path.Combine(templatesDir, "bcl", profile);
if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll")))
{
if (PlatformRequiresCustomBcl(platform))
throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}");
platformBclDir = typeof(object).Assembly.Location.GetBaseDir(); // Use the one we're running on
}
}
return platformBclDir;
}
/// <summary>
/// Determines whether the BCL bundled with the Godot editor can be used for the target platform,
/// or if it requires a custom BCL that must be distributed with the export templates.
/// </summary>
private static bool PlatformRequiresCustomBcl(string platform)
{
if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform))
return true;
// The 'net_4_x' BCL is not compatible between Windows and the other platforms.
// We use the names 'net_4_x_win' and 'net_4_x' to differentiate between the two.
bool isWinOrUwp = new[]
{
OS.Platforms.Windows,
OS.Platforms.UWP
}.Contains(platform);
return OS.IsWindows ? !isWinOrUwp : isWinOrUwp;
}
private static string DeterminePlatformBclProfile(string platform)
{
switch (platform)
{
case OS.Platforms.Windows:
case OS.Platforms.UWP:
return "net_4_x_win";
case OS.Platforms.OSX:
case OS.Platforms.X11:
case OS.Platforms.Server:
case OS.Platforms.Haiku:
return "net_4_x";
case OS.Platforms.Android:
return "monodroid";
case OS.Platforms.iOS:
return "monotouch";
case OS.Platforms.HTML5:
return "wasm";
default:
throw new NotSupportedException($"Platform not supported: {platform}");
}
}
private static string DetermineDataDirNameForProject()
{
string appName = (string)ProjectSettings.GetSetting("application/config/name");
string appNameSafe = appName.ToSafeDirName();
return $"data_{appNameSafe}";
}
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_GetExportedAssemblyDependencies(Godot.Collections.Dictionary<string, string> initialAssemblies,
string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencyAssemblies);
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.IO;
namespace GodotTools.Export
{
public static class XcodeHelper
{
private static string _XcodePath = null;
public static string XcodePath
{
get
{
if (_XcodePath == null)
{
_XcodePath = FindXcode();
if (_XcodePath == null)
throw new Exception("Could not find Xcode");
}
return _XcodePath;
}
}
private static string FindSelectedXcode()
{
var outputWrapper = new Godot.Collections.Array();
int exitCode = Godot.OS.Execute("xcode-select", new string[] { "--print-path" }, blocking: true, output: outputWrapper);
if (exitCode == 0)
{
string output = (string)outputWrapper[0];
return output.Trim();
}
Console.Error.WriteLine($"'xcode-select --print-path' exited with code: {exitCode}");
return null;
}
public static string FindXcode()
{
string selectedXcode = FindSelectedXcode();
if (selectedXcode != null)
{
if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer")))
return selectedXcode;
// The path already pointed to Contents/Developer
var dirInfo = new DirectoryInfo(selectedXcode);
if (dirInfo.Name != "Developer" || dirInfo.Parent.Name != "Contents")
{
Console.WriteLine(Path.GetDirectoryName(selectedXcode));
Console.WriteLine(System.IO.Directory.GetParent(selectedXcode).Name);
Console.Error.WriteLine("Unrecognized path for selected Xcode");
}
else
{
return System.IO.Path.GetFullPath($"{selectedXcode}/../..");
}
}
else
{
Console.Error.WriteLine("Could not find the selected Xcode; trying with a hint path");
}
const string XcodeHintPath = "/Applications/Xcode.app";
if (Directory.Exists(XcodeHintPath))
{
if (Directory.Exists(Path.Combine(XcodeHintPath, "Contents", "Developer")))
return XcodeHintPath;
Console.Error.WriteLine($"Found Xcode at '{XcodeHintPath}' but it's missing the 'Contents/Developer' sub-directory");
}
return null;
}
public static string FindXcodeTool(string toolName)
{
string XcodeDefaultToolchain = Path.Combine(XcodePath, "Contents", "Developer", "Toolchains", "XcodeDefault.xctoolchain");
string path = Path.Combine(XcodeDefaultToolchain, "usr", "bin", toolName);
if (File.Exists(path))
return path;
throw new FileNotFoundException($"Cannot find Xcode tool: {toolName}");
}
}
}

View File

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

View File

@ -0,0 +1,548 @@
using Godot;
using GodotTools.Core;
using GodotTools.Export;
using GodotTools.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GodotTools.Build;
using GodotTools.Ides;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
using GodotTools.ProjectEditor;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
using OS = GodotTools.Utils.OS;
using Path = System.IO.Path;
namespace GodotTools
{
public class GodotSharpEditor : EditorPlugin, ISerializationListener
{
private EditorSettings _editorSettings;
private PopupMenu _menuPopup;
private AcceptDialog _errorDialog;
private ToolButton _bottomPanelBtn;
private ToolButton _toolBarButton;
// TODO Use WeakReference once we have proper serialization.
private WeakRef _exportPluginWeak;
public GodotIdeManager GodotIdeManager { get; private set; }
public MSBuildPanel MSBuildPanel { get; private set; }
public bool SkipBuildBeforePlaying { get; set; } = false;
private bool CreateProjectSolution()
{
using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 3))
{
pr.Step("Generating C# project...".TTR());
string resourceDir = ProjectSettings.GlobalizePath("res://");
string path = resourceDir;
string name = GodotSharpDirs.ProjectAssemblyName;
string guid = CsProjOperations.GenerateGameProject(path, name);
if (guid.Length > 0)
{
var solution = new DotNetSolution(name)
{
DirectoryPath = path
};
var projectInfo = new DotNetSolution.ProjectInfo
{
Guid = guid,
PathRelativeToSolution = name + ".csproj",
Configs = new List<string> { "Debug", "ExportDebug", "ExportRelease" }
};
solution.AddNewProject(name, projectInfo);
try
{
solution.Save();
}
catch (IOException e)
{
ShowErrorDialog("Failed to save solution. Exception message: ".TTR() + e.Message);
return false;
}
pr.Step("Updating Godot API assemblies...".TTR());
string debugApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Debug");
if (!string.IsNullOrEmpty(debugApiAssembliesError))
{
ShowErrorDialog("Failed to update the Godot API assemblies: " + debugApiAssembliesError);
return false;
}
string releaseApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Release");
if (!string.IsNullOrEmpty(releaseApiAssembliesError))
{
ShowErrorDialog("Failed to update the Godot API assemblies: " + releaseApiAssembliesError);
return false;
}
pr.Step("Done".TTR());
// Here, after all calls to progress_task_step
CallDeferred(nameof(_RemoveCreateSlnMenuOption));
}
else
{
ShowErrorDialog("Failed to create C# project.".TTR());
}
return true;
}
}
private void _RemoveCreateSlnMenuOption()
{
_menuPopup.RemoveItem(_menuPopup.GetItemIndex((int)MenuOptions.CreateSln));
_bottomPanelBtn.Show();
_toolBarButton.Show();
}
private void _MenuOptionPressed(MenuOptions id)
{
switch (id)
{
case MenuOptions.CreateSln:
CreateProjectSolution();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid menu option");
}
}
private void BuildSolutionPressed()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
{
if (!CreateProjectSolution())
return; // Failed to create solution
}
Instance.MSBuildPanel.BuildSolution();
}
private void _FileSystemDockFileMoved(string file, string newFile)
{
if (Path.GetExtension(file) == Internal.CSharpLanguageExtension)
{
ProjectUtils.RenameItemInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
ProjectSettings.GlobalizePath(file), ProjectSettings.GlobalizePath(newFile));
}
}
private void _FileSystemDockFileRemoved(string file)
{
if (Path.GetExtension(file) == Internal.CSharpLanguageExtension)
{
ProjectUtils.RemoveItemFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
ProjectSettings.GlobalizePath(file));
}
}
private void _FileSystemDockFolderMoved(string oldFolder, string newFolder)
{
if (File.Exists(GodotSharpDirs.ProjectCsProjPath))
{
ProjectUtils.RenameItemsToNewFolderInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
ProjectSettings.GlobalizePath(oldFolder), ProjectSettings.GlobalizePath(newFolder));
}
}
private void _FileSystemDockFolderRemoved(string oldFolder)
{
if (File.Exists(GodotSharpDirs.ProjectCsProjPath))
{
ProjectUtils.RemoveItemsInFolderFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
ProjectSettings.GlobalizePath(oldFolder));
}
}
public override void _Ready()
{
base._Ready();
MSBuildPanel.BuildOutputView.Connect(
nameof(BuildOutputView.BuildStateChanged), this, nameof(BuildStateChanged));
var fileSystemDock = GetEditorInterface().GetFileSystemDock();
fileSystemDock.Connect("files_moved", this, nameof(_FileSystemDockFileMoved));
fileSystemDock.Connect("file_removed", this, nameof(_FileSystemDockFileRemoved));
fileSystemDock.Connect("folder_moved", this, nameof(_FileSystemDockFolderMoved));
fileSystemDock.Connect("folder_removed", this, nameof(_FileSystemDockFolderRemoved));
}
private enum MenuOptions
{
CreateSln,
}
public void ShowErrorDialog(string message, string title = "Error")
{
_errorDialog.WindowTitle = title;
_errorDialog.DialogText = message;
_errorDialog.PopupCenteredMinsize();
}
private static string _vsCodePath = string.Empty;
private static readonly string[] VsCodeNames =
{
"code", "code-oss", "vscode", "vscode-oss", "visual-studio-code", "visual-studio-code-oss"
};
[UsedImplicitly]
public Error OpenInExternalEditor(Script script, int line, int col)
{
var editorId = (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor");
switch (editorId)
{
case ExternalEditorId.None:
// Not an error. Tells the caller to fallback to the global external editor settings or the built-in editor.
return Error.Unavailable;
case ExternalEditorId.VisualStudio:
{
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
var args = new List<string>
{
GodotSharpDirs.ProjectSlnPath,
line >= 0 ? $"{scriptPath};{line + 1};{col + 1}" : scriptPath
};
string command = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "GodotTools.OpenVisualStudio.exe");
try
{
if (Godot.OS.IsStdoutVerbose())
Console.WriteLine($"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}");
OS.RunProcess(command, args);
}
catch (Exception e)
{
GD.PushError($"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'");
}
break;
}
case ExternalEditorId.VisualStudioForMac:
goto case ExternalEditorId.MonoDevelop;
case ExternalEditorId.Rider:
{
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
RiderPathManager.OpenFile(GodotSharpDirs.ProjectSlnPath, scriptPath, line);
return Error.Ok;
}
case ExternalEditorId.MonoDevelop:
{
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
GodotIdeManager.LaunchIdeAsync().ContinueWith(launchTask =>
{
var editorPick = launchTask.Result;
if (line >= 0)
editorPick?.SendOpenFile(scriptPath, line + 1, col);
else
editorPick?.SendOpenFile(scriptPath);
});
break;
}
case ExternalEditorId.VsCode:
{
if (string.IsNullOrEmpty(_vsCodePath) || !File.Exists(_vsCodePath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
}
var args = new List<string>();
bool osxAppBundleInstalled = false;
if (OS.IsOSX)
{
// The package path is '/Applications/Visual Studio Code.app'
const string vscodeBundleId = "com.microsoft.VSCode";
osxAppBundleInstalled = Internal.IsOsxAppBundleInstalled(vscodeBundleId);
if (osxAppBundleInstalled)
{
args.Add("-b");
args.Add(vscodeBundleId);
// The reusing of existing windows made by the 'open' command might not choose a wubdiw that is
// editing our folder. It's better to ask for a new window and let VSCode do the window management.
args.Add("-n");
// The open process must wait until the application finishes (which is instant in VSCode's case)
args.Add("--wait-apps");
args.Add("--args");
}
}
string resourcePath = ProjectSettings.GlobalizePath("res://");
args.Add(resourcePath);
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
if (line >= 0)
{
args.Add("-g");
args.Add($"{scriptPath}:{line}:{col}");
}
else
{
args.Add(scriptPath);
}
string command;
if (OS.IsOSX)
{
if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath))
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
}
command = osxAppBundleInstalled ? "/usr/bin/open" : _vsCodePath;
}
else
{
if (string.IsNullOrEmpty(_vsCodePath))
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
}
command = _vsCodePath;
}
try
{
OS.RunProcess(command, args);
}
catch (Exception e)
{
GD.PushError($"Error when trying to run code editor: VSCode. Exception message: '{e.Message}'");
}
break;
}
default:
throw new ArgumentOutOfRangeException();
}
return Error.Ok;
}
[UsedImplicitly]
public bool OverridesExternalEditor()
{
return (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor") !=
ExternalEditorId.None;
}
public override bool Build()
{
return BuildManager.EditorBuildCallback();
}
private void ApplyNecessaryChangesToSolution()
{
try
{
// Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease
DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath);
var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath)
?? throw new Exception("Cannot open C# project");
// NOTE: The order in which changes are made to the project is important
// Migrate to MSBuild project Sdks style if using the old style
ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, GodotSharpDirs.ProjectAssemblyName);
ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject);
if (msbuildProject.HasUnsavedChanges)
{
// Save a copy of the project before replacing it
FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath);
msbuildProject.Save();
}
}
catch (Exception e)
{
GD.PushError(e.ToString());
}
}
private void BuildStateChanged()
{
if (_bottomPanelBtn != null)
_bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
}
public override void EnablePlugin()
{
base.EnablePlugin();
if (Instance != null)
throw new InvalidOperationException();
Instance = this;
var editorInterface = GetEditorInterface();
var editorBaseControl = editorInterface.GetBaseControl();
_editorSettings = editorInterface.GetEditorSettings();
_errorDialog = new AcceptDialog();
editorBaseControl.AddChild(_errorDialog);
MSBuildPanel = new MSBuildPanel();
_bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" });
_menuPopup = new PopupMenu();
_menuPopup.Hide();
_menuPopup.SetAsToplevel(true);
AddToolSubmenuItem("C#", _menuPopup);
var buildSolutionShortcut = (ShortCut)EditorShortcut("mono/build_solution");
_toolBarButton = new ToolButton
{
Text = "Build",
HintTooltip = "Build Solution".TTR(),
FocusMode = Control.FocusModeEnum.None,
Shortcut = buildSolutionShortcut,
ShortcutInTooltip = true
};
_toolBarButton.Connect("pressed", this, nameof(BuildSolutionPressed));
AddControlToContainer(CustomControlContainer.Toolbar, _toolBarButton);
if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
{
ApplyNecessaryChangesToSolution();
}
else
{
_bottomPanelBtn.Hide();
_toolBarButton.Hide();
_menuPopup.AddItem("Create C# solution".TTR(), (int)MenuOptions.CreateSln);
}
_menuPopup.Connect("id_pressed", this, nameof(_MenuOptionPressed));
// External editor settings
EditorDef("mono/editor/external_editor", ExternalEditorId.None);
string settingsHintStr = "Disabled";
if (OS.IsWindows)
{
settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudio}" +
$",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
}
else if (OS.IsOSX)
{
settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudioForMac}" +
$",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
}
else if (OS.IsUnixLike)
{
settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
}
_editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = Variant.Type.Int,
["name"] = "mono/editor/external_editor",
["hint"] = PropertyHint.Enum,
["hint_string"] = settingsHintStr
});
// Export plugin
var exportPlugin = new ExportPlugin();
AddExportPlugin(exportPlugin);
exportPlugin.RegisterExportSettings();
_exportPluginWeak = WeakRef(exportPlugin);
BuildManager.Initialize();
RiderPathManager.Initialize();
GodotIdeManager = new GodotIdeManager();
AddChild(GodotIdeManager);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_exportPluginWeak != null)
{
// We need to dispose our export plugin before the editor destroys EditorSettings.
// Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid
// will be freed after EditorSettings already was, and its device polling thread
// will try to access the EditorSettings singleton, resulting in null dereferencing.
(_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose();
_exportPluginWeak.Dispose();
}
GodotIdeManager?.Dispose();
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
Instance = this;
}
// Singleton
public static GodotSharpEditor Instance { get; private set; }
[UsedImplicitly]
private GodotSharpEditor()
{
}
}
}

View File

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.2</LangVersion>
<!-- The Godot editor uses the Debug Godot API assemblies -->
<GodotApiConfiguration>Debug</GodotApiConfiguration>
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
<GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
<GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir>
</PropertyGroup>
<PropertyGroup Condition=" Exists('$(GodotApiAssembliesDir)/GodotSharp.dll') ">
<!-- The project is part of the Godot source tree -->
<!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' -->
<OutputPath>$(GodotOutputDataDir)/Tools</OutputPath>
<!-- Must not append '$(TargetFramework)' to the output path in this case -->
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GodotTools.IdeMessaging" Version="1.1.1" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<Reference Include="GodotSharp">
<HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="GodotSharpEditor">
<HintPath>$(GodotApiAssembliesDir)/GodotSharpEditor.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" />
<ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" />
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
<!-- Include it if this is an SCons build targeting Windows, or if it's not an SCons build but we're on Windows -->
<ProjectReference Include="..\GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) " />
</ItemGroup>
</Project>

View File

@ -0,0 +1,48 @@
using Godot;
using GodotTools.Internals;
using static GodotTools.Internals.Globals;
namespace GodotTools
{
public class HotReloadAssemblyWatcher : Node
{
private Timer _watchTimer;
public override void _Notification(int what)
{
if (what == MainLoop.NotificationWmFocusIn)
{
RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
}
private void TimerTimeout()
{
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
public void RestartTimer()
{
_watchTimer.Stop();
_watchTimer.Start();
}
public override void _Ready()
{
base._Ready();
_watchTimer = new Timer
{
OneShot = false,
WaitTime = (float)EditorDef("mono/assembly_watch_interval_sec", 0.5)
};
_watchTimer.Connect("timeout", this, nameof(TimerTimeout));
AddChild(_watchTimer);
_watchTimer.Start();
}
}
}

View File

@ -0,0 +1,229 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Godot;
using GodotTools.IdeMessaging;
using GodotTools.IdeMessaging.Requests;
using GodotTools.Internals;
namespace GodotTools.Ides
{
public sealed class GodotIdeManager : Node, ISerializationListener
{
private MessagingServer _messagingServer;
private MonoDevelop.Instance _monoDevelInstance;
private MonoDevelop.Instance _vsForMacInstance;
private MessagingServer GetRunningOrNewServer()
{
if (_messagingServer != null && !_messagingServer.IsDisposed)
return _messagingServer;
_messagingServer?.Dispose();
_messagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger());
_ = _messagingServer.Listen();
return _messagingServer;
}
public override void _Ready()
{
_ = GetRunningOrNewServer();
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
_ = GetRunningOrNewServer();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_messagingServer?.Dispose();
}
}
private string GetExternalEditorIdentity(ExternalEditorId editorId)
{
// Manually convert to string to avoid breaking compatibility in case we rename the enum fields.
switch (editorId)
{
case ExternalEditorId.None:
return null;
case ExternalEditorId.VisualStudio:
return "VisualStudio";
case ExternalEditorId.VsCode:
return "VisualStudioCode";
case ExternalEditorId.Rider:
return "Rider";
case ExternalEditorId.VisualStudioForMac:
return "VisualStudioForMac";
case ExternalEditorId.MonoDevelop:
return "MonoDevelop";
default:
throw new NotImplementedException();
}
}
public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000)
{
var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
.GetEditorSettings().GetSetting("mono/editor/external_editor");
string editorIdentity = GetExternalEditorIdentity(editorId);
var runningServer = GetRunningOrNewServer();
if (runningServer.IsAnyConnected(editorIdentity))
return new EditorPick(editorIdentity);
LaunchIde(editorId, editorIdentity);
var timeoutTask = Task.Delay(millisecondsTimeout);
var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity));
if (completedTask != timeoutTask)
return new EditorPick(editorIdentity);
return null;
}
private void LaunchIde(ExternalEditorId editorId, string editorIdentity)
{
switch (editorId)
{
case ExternalEditorId.None:
case ExternalEditorId.VisualStudio:
case ExternalEditorId.VsCode:
case ExternalEditorId.Rider:
throw new NotSupportedException();
case ExternalEditorId.VisualStudioForMac:
goto case ExternalEditorId.MonoDevelop;
case ExternalEditorId.MonoDevelop:
{
MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
{
if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac)
{
_vsForMacInstance = (_vsForMacInstance?.IsDisposed ?? true ? null : _vsForMacInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
return _vsForMacInstance;
}
_monoDevelInstance = (_monoDevelInstance?.IsDisposed ?? true ? null : _monoDevelInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
return _monoDevelInstance;
}
try
{
var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
if (instance.IsRunning && !GetRunningOrNewServer().IsAnyConnected(editorIdentity))
{
// After launch we wait up to 30 seconds for the IDE to connect to our messaging server.
var waitAfterLaunch = TimeSpan.FromSeconds(30);
var timeSinceLaunch = DateTime.Now - instance.LaunchTime;
if (timeSinceLaunch > waitAfterLaunch)
{
instance.Dispose();
instance.Execute();
}
}
else if (!instance.IsRunning)
{
instance.Execute();
}
}
catch (FileNotFoundException)
{
string editorName = editorId == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
GD.PushError($"Cannot find code editor: {editorName}");
}
break;
}
default:
throw new ArgumentOutOfRangeException();
}
}
public readonly struct EditorPick
{
private readonly string _identity;
public EditorPick(string identity)
{
_identity = identity;
}
public bool IsAnyConnected() =>
GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(_identity);
private void SendRequest<TResponse>(Request request)
where TResponse : Response, new()
{
// Logs an error if no client is connected with the specified identity
GodotSharpEditor.Instance.GodotIdeManager
.GetRunningOrNewServer()
.BroadcastRequest<TResponse>(_identity, request);
}
public void SendOpenFile(string file)
{
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file});
}
public void SendOpenFile(string file, int line)
{
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line});
}
public void SendOpenFile(string file, int line, int column)
{
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column});
}
}
public EditorPick PickEditor(ExternalEditorId editorId) => new EditorPick(GetExternalEditorIdentity(editorId));
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,395 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using GodotTools.IdeMessaging;
using GodotTools.IdeMessaging.Requests;
using GodotTools.IdeMessaging.Utils;
using GodotTools.Internals;
using GodotTools.Utils;
using Newtonsoft.Json;
using Directory = System.IO.Directory;
using File = System.IO.File;
namespace GodotTools.Ides
{
public sealed class MessagingServer : IDisposable
{
private readonly ILogger _logger;
private readonly FileStream _metaFile;
private string _metaFilePath;
private readonly SemaphoreSlim _peersSem = new SemaphoreSlim(1);
private readonly TcpListener _listener;
private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> _clientConnectedAwaiters =
new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> _clientDisconnectedAwaiters =
new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
public async Task<bool> AwaitClientConnected(string identity)
{
if (!_clientConnectedAwaiters.TryGetValue(identity, out var queue))
{
queue = new Queue<NotifyAwaiter<bool>>();
_clientConnectedAwaiters.Add(identity, queue);
}
var awaiter = new NotifyAwaiter<bool>();
queue.Enqueue(awaiter);
return await awaiter;
}
public async Task<bool> AwaitClientDisconnected(string identity)
{
if (!_clientDisconnectedAwaiters.TryGetValue(identity, out var queue))
{
queue = new Queue<NotifyAwaiter<bool>>();
_clientDisconnectedAwaiters.Add(identity, queue);
}
var awaiter = new NotifyAwaiter<bool>();
queue.Enqueue(awaiter);
return await awaiter;
}
public bool IsDisposed { get; private set; }
public bool IsAnyConnected(string identity) => string.IsNullOrEmpty(identity) ?
Peers.Count > 0 :
Peers.Any(c => c.RemoteIdentity == identity);
private List<Peer> Peers { get; } = new List<Peer>();
~MessagingServer()
{
Dispose(disposing: false);
}
public async void Dispose()
{
if (IsDisposed)
return;
using (await _peersSem.UseAsync())
{
if (IsDisposed) // lock may not be fair
return;
IsDisposed = true;
}
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
foreach (var connection in Peers)
connection.Dispose();
Peers.Clear();
_listener?.Stop();
_metaFile?.Dispose();
File.Delete(_metaFilePath);
}
}
public MessagingServer(string editorExecutablePath, string projectMetadataDir, ILogger logger)
{
this._logger = logger;
_metaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
// Make sure the directory exists
Directory.CreateDirectory(projectMetadataDir);
// 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);
}
}
private async Task AcceptClient(TcpClient tcpClient)
{
_logger.LogDebug("Accept client...");
using (var peer = new Peer(tcpClient, new ServerHandshake(), new ServerMessageHandler(), _logger))
{
// ReSharper disable AccessToDisposedClosure
peer.Connected += () =>
{
_logger.LogInfo("Connection open with Ide Client");
if (_clientConnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
{
while (queue.Count > 0)
queue.Dequeue().SetResult(true);
_clientConnectedAwaiters.Remove(peer.RemoteIdentity);
}
};
peer.Disconnected += () =>
{
if (_clientDisconnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
{
while (queue.Count > 0)
queue.Dequeue().SetResult(true);
_clientDisconnectedAwaiters.Remove(peer.RemoteIdentity);
}
};
// ReSharper restore AccessToDisposedClosure
try
{
if (!await peer.DoHandshake("server"))
{
_logger.LogError("Handshake failed");
return;
}
}
catch (Exception e)
{
_logger.LogError("Handshake failed with unhandled exception: ", e);
return;
}
using (await _peersSem.UseAsync())
Peers.Add(peer);
try
{
await peer.Process();
}
finally
{
using (await _peersSem.UseAsync())
Peers.Remove(peer);
}
}
}
public async Task Listen()
{
try
{
while (!IsDisposed)
_ = AcceptClient(await _listener.AcceptTcpClientAsync());
}
catch (Exception e)
{
if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
throw;
}
}
public async void BroadcastRequest<TResponse>(string identity, Request request)
where TResponse : Response, new()
{
using (await _peersSem.UseAsync())
{
if (!IsAnyConnected(identity))
{
_logger.LogError("Cannot write request. No client connected to the Godot Ide Server.");
return;
}
var selectedConnections = string.IsNullOrEmpty(identity) ?
Peers :
Peers.Where(c => c.RemoteIdentity == identity);
string body = JsonConvert.SerializeObject(request);
foreach (var connection in selectedConnections)
_ = connection.SendRequest<TResponse>(request.Id, body);
}
}
private class ServerHandshake : IHandshake
{
private static readonly string _serverHandshakeBase =
$"{Peer.ServerHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
private static readonly string _clientHandshakePattern =
$@"{Regex.Escape(Peer.ClientHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
public string GetHandshakeLine(string identity) => $"{_serverHandshakeBase},{identity}";
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
{
identity = null;
var match = Regex.Match(handshake, _clientHandshakePattern);
if (!match.Success)
return false;
if (!uint.TryParse(match.Groups[1].Value, out uint clientMajor) || Peer.ProtocolVersionMajor != clientMajor)
{
logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
return false;
}
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (!uint.TryParse(match.Groups[2].Value, out uint clientMinor) || Peer.ProtocolVersionMinor > clientMinor)
{
logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
return false;
}
if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
{
logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
return false;
}
identity = match.Groups[4].Value;
return true;
}
}
private class ServerMessageHandler : IMessageHandler
{
private static void DispatchToMainThread(Action action)
{
var d = new SendOrPostCallback(state => action());
Godot.Dispatcher.SynchronizationContext.Post(d, null);
}
private readonly Dictionary<string, Peer.RequestHandler> requestHandlers = InitializeRequestHandlers();
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
{
if (!requestHandlers.TryGetValue(id, out var handler))
{
logger.LogError($"Received unknown request: {id}");
return new MessageContent(MessageStatus.RequestNotSupported, "null");
}
try
{
var response = await handler(peer, content);
return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
}
catch (JsonException)
{
logger.LogError($"Received request with invalid body: {id}");
return new MessageContent(MessageStatus.InvalidRequestBody, "null");
}
}
private static Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
{
return new Dictionary<string, Peer.RequestHandler>
{
[PlayRequest.Id] = async (peer, content) =>
{
_ = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
return await HandlePlay();
},
[DebugPlayRequest.Id] = async (peer, content) =>
{
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
return await HandleDebugPlay(request);
},
[StopPlayRequest.Id] = async (peer, content) =>
{
var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body);
return await HandleStopPlay(request);
},
[ReloadScriptsRequest.Id] = async (peer, content) =>
{
_ = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
return await HandleReloadScripts();
},
[CodeCompletionRequest.Id] = async (peer, content) =>
{
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
return await HandleCodeCompletionRequest(request);
}
};
}
private static Task<Response> HandlePlay()
{
DispatchToMainThread(() =>
{
// TODO: Add BuildBeforePlaying flag to PlayRequest
// Run the game
Internal.EditorRunPlay();
});
return Task.FromResult<Response>(new PlayResponse());
}
private static Task<Response> HandleDebugPlay(DebugPlayRequest request)
{
DispatchToMainThread(() =>
{
// Tell the build callback whether the editor already built the solution or not
GodotSharpEditor.Instance.SkipBuildBeforePlaying = !(request.BuildBeforePlaying ?? true);
// Pass the debugger agent settings to the player via an environment variables
// TODO: It would be better if this was an argument in EditorRunPlay instead
Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT",
"--debugger-agent=transport=dt_socket" +
$",address={request.DebuggerHost}:{request.DebuggerPort}" +
",server=n");
// Run the game
Internal.EditorRunPlay();
// Restore normal settings
Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", "");
GodotSharpEditor.Instance.SkipBuildBeforePlaying = false;
});
return Task.FromResult<Response>(new DebugPlayResponse());
}
private static Task<Response> HandleStopPlay(StopPlayRequest request)
{
DispatchToMainThread(Internal.EditorRunStop);
return Task.FromResult<Response>(new StopPlayResponse());
}
private static Task<Response> HandleReloadScripts()
{
DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
return Task.FromResult<Response>(new ReloadScriptsResponse());
}
private static async Task<Response> HandleCodeCompletionRequest(CodeCompletionRequest request)
{
// This is needed if the "resource path" part of the path is case insensitive.
// However, it doesn't fix resource loading if the rest of the path is also case insensitive.
string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile);
var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile};
response.Suggestions = await Task.Run(() =>
Internal.CodeCompletionRequest(response.Kind, scriptFileLocalized ?? request.ScriptFile));
return response;
}
}
}
}

View File

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

View File

@ -0,0 +1,141 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using GodotTools.Internals;
using GodotTools.Utils;
namespace GodotTools.Ides.MonoDevelop
{
public class Instance : IDisposable
{
public DateTime LaunchTime { get; private set; }
private readonly string _solutionFile;
private readonly EditorId _editorId;
private Process _process;
public bool IsRunning => _process != null && !_process.HasExited;
public bool IsDisposed { get; private set; }
public void Execute()
{
bool newWindow = _process == null || _process.HasExited;
var args = new List<string>();
string command;
if (OS.IsOSX)
{
string bundleId = BundleIds[_editorId];
if (Internal.IsOsxAppBundleInstalled(bundleId))
{
command = "open";
args.Add("-b");
args.Add(bundleId);
// The 'open' process must wait until the application finishes
if (newWindow)
args.Add("--wait-apps");
args.Add("--args");
}
else
{
command = OS.PathWhich(ExecutableNames[_editorId]);
}
}
else
{
command = OS.PathWhich(ExecutableNames[_editorId]);
}
args.Add("--ipc-tcp");
if (newWindow)
args.Add("\"" + Path.GetFullPath(_solutionFile) + "\"");
if (command == null)
throw new FileNotFoundException();
LaunchTime = DateTime.Now;
if (newWindow)
{
_process = Process.Start(new ProcessStartInfo
{
FileName = command,
Arguments = string.Join(" ", args),
UseShellExecute = true
});
}
else
{
Process.Start(new ProcessStartInfo
{
FileName = command,
Arguments = string.Join(" ", args),
UseShellExecute = true
})?.Dispose();
}
}
public Instance(string solutionFile, EditorId editorId)
{
if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX)
throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform");
_solutionFile = solutionFile;
_editorId = editorId;
}
public void Dispose()
{
IsDisposed = true;
_process?.Dispose();
}
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
static Instance()
{
if (OS.IsOSX)
{
ExecutableNames = new Dictionary<EditorId, string>
{
// Rely on PATH
{EditorId.MonoDevelop, "monodevelop"},
{EditorId.VisualStudioForMac, "VisualStudio"}
};
BundleIds = new Dictionary<EditorId, string>
{
// TODO EditorId.MonoDevelop
{EditorId.VisualStudioForMac, "com.microsoft.visual-studio"}
};
}
else if (OS.IsWindows)
{
ExecutableNames = new Dictionary<EditorId, string>
{
// XamarinStudio is no longer a thing, and the latest version is quite old
// MonoDevelop is available from source only on Windows. The recommendation
// is to use Visual Studio instead. Since there are no official builds, we
// will rely on custom MonoDevelop builds being added to PATH.
{EditorId.MonoDevelop, "MonoDevelop.exe"}
};
}
else if (OS.IsUnixLike)
{
ExecutableNames = new Dictionary<EditorId, string>
{
// Rely on PATH
{EditorId.MonoDevelop, "monodevelop"}
};
}
}
}
}

View File

@ -0,0 +1,470 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
using JetBrains.Annotations;
using Microsoft.Win32;
using Newtonsoft.Json;
using Directory = System.IO.Directory;
using Environment = System.Environment;
using File = System.IO.File;
using Path = System.IO.Path;
using OS = GodotTools.Utils.OS;
// ReSharper disable UnassignedField.Local
// ReSharper disable InconsistentNaming
// ReSharper disable UnassignedField.Global
// ReSharper disable MemberHidesStaticFromOuterClass
namespace GodotTools.Ides.Rider
{
/// <summary>
/// This code is a modified version of the JetBrains resharper-unity plugin listed under Apache License 2.0 license:
/// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
/// </summary>
public static class RiderPathLocator
{
public static RiderInfo[] GetAllRiderPaths()
{
try
{
if (OS.IsWindows)
{
return CollectRiderInfosWindows();
}
if (OS.IsOSX)
{
return CollectRiderInfosMac();
}
if (OS.IsUnixLike)
{
return CollectAllRiderPathsLinux();
}
throw new Exception("Unexpected OS.");
}
catch (Exception e)
{
GD.PushWarning(e.Message);
}
return Array.Empty<RiderInfo>();
}
private static RiderInfo[] CollectAllRiderPathsLinux()
{
var installInfos = new List<RiderInfo>();
string home = Environment.GetEnvironmentVariable("HOME");
if (!string.IsNullOrEmpty(home))
{
string toolboxRiderRootPath = GetToolboxBaseDir();
installInfos.AddRange(CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider.sh", false)
.Select(a => new RiderInfo(a, true)).ToList());
//$Home/.local/share/applications/jetbrains-rider.desktop
var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop"));
if (shortcut.Exists)
{
string[] lines = File.ReadAllLines(shortcut.FullName);
foreach (string line in lines)
{
if (!line.StartsWith("Exec=\""))
continue;
string path = line.Split('"').Where((item, index) => index == 1).SingleOrDefault();
if (string.IsNullOrEmpty(path))
continue;
if (installInfos.Any(a => a.Path == path)) // avoid adding similar build as from toolbox
continue;
installInfos.Add(new RiderInfo(path, false));
}
}
}
// snap install
string snapInstallPath = "/snap/rider/current/bin/rider.sh";
if (new FileInfo(snapInstallPath).Exists)
installInfos.Add(new RiderInfo(snapInstallPath, false));
return installInfos.ToArray();
}
private static RiderInfo[] CollectRiderInfosMac()
{
var installInfos = new List<RiderInfo>();
// "/Applications/*Rider*.app"
// should be combined with "Contents/MacOS/rider"
var folder = new DirectoryInfo("/Applications");
if (folder.Exists)
{
installInfos.AddRange(folder.GetDirectories("*Rider*.app")
.Select(a => new RiderInfo(Path.Combine(a.FullName, "Contents/MacOS/rider"), false))
.ToList());
}
// /Users/user/Library/Application Support/JetBrains/Toolbox/apps/Rider/ch-1/181.3870.267/Rider EAP.app
// should be combined with "Contents/MacOS/rider"
string toolboxRiderRootPath = GetToolboxBaseDir();
var paths = CollectPathsFromToolbox(toolboxRiderRootPath, "", "Rider*.app", true)
.Select(a => new RiderInfo(Path.Combine(a, "Contents/MacOS/rider"), true));
installInfos.AddRange(paths);
return installInfos.ToArray();
}
private static RiderInfo[] CollectRiderInfosWindows()
{
var installInfos = new List<RiderInfo>();
var toolboxRiderRootPath = GetToolboxBaseDir();
var installPathsToolbox = CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider64.exe", false).ToList();
installInfos.AddRange(installPathsToolbox.Select(a => new RiderInfo(a, true)).ToList());
var installPaths = new List<string>();
const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
CollectPathsFromRegistry(registryKey, installPaths);
const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
CollectPathsFromRegistry(wowRegistryKey, installPaths);
installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList());
return installInfos.ToArray();
}
private static string GetToolboxBaseDir()
{
if (OS.IsWindows)
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return GetToolboxRiderRootPath(localAppData);
}
if (OS.IsOSX)
{
var home = Environment.GetEnvironmentVariable("HOME");
if (string.IsNullOrEmpty(home))
return string.Empty;
var localAppData = Path.Combine(home, @"Library/Application Support");
return GetToolboxRiderRootPath(localAppData);
}
if (OS.IsUnixLike)
{
var home = Environment.GetEnvironmentVariable("HOME");
if (string.IsNullOrEmpty(home))
return string.Empty;
var localAppData = Path.Combine(home, @".local/share");
return GetToolboxRiderRootPath(localAppData);
}
return string.Empty;
}
private static string GetToolboxRiderRootPath(string localAppData)
{
var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox");
var settingsJson = Path.Combine(toolboxPath, ".settings.json");
if (File.Exists(settingsJson))
{
var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson));
if (!string.IsNullOrEmpty(path))
toolboxPath = path;
}
var toolboxRiderRootPath = Path.Combine(toolboxPath, @"apps/Rider");
return toolboxRiderRootPath;
}
internal static ProductInfo GetBuildVersion(string path)
{
var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
var dir = buildTxtFileInfo.DirectoryName;
if (!Directory.Exists(dir))
return null;
var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json"));
if (!buildVersionFile.Exists)
return null;
var json = File.ReadAllText(buildVersionFile.FullName);
return ProductInfo.GetProductInfo(json);
}
internal static Version GetBuildNumber(string path)
{
var file = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
if (!file.Exists)
return null;
var text = File.ReadAllText(file.FullName);
if (text.Length <= 3)
return null;
var versionText = text.Substring(3);
return Version.TryParse(versionText, out var v) ? v : null;
}
internal static bool IsToolbox(string path)
{
return path.StartsWith(GetToolboxBaseDir());
}
private static string GetRelativePathToBuildTxt()
{
if (OS.IsWindows || OS.IsUnixLike)
return "../../build.txt";
if (OS.IsOSX)
return "Contents/Resources/build.txt";
throw new Exception("Unknown OS.");
}
private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths)
{
using (var key = Registry.CurrentUser.OpenSubKey(registryKey))
{
CollectPathsFromRegistry(installPaths, key);
}
using (var key = Registry.LocalMachine.OpenSubKey(registryKey))
{
CollectPathsFromRegistry(installPaths, key);
}
}
private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key)
{
if (key == null) return;
foreach (var subkeyName in key.GetSubKeyNames().Where(a => a.Contains("Rider")))
{
using (var subkey = key.OpenSubKey(subkeyName))
{
var folderObject = subkey?.GetValue("InstallLocation");
if (folderObject == null) continue;
var folder = folderObject.ToString();
var possiblePath = Path.Combine(folder, @"bin\rider64.exe");
if (File.Exists(possiblePath))
installPaths.Add(possiblePath);
}
}
}
private static string[] CollectPathsFromToolbox(string toolboxRiderRootPath, string dirName, string searchPattern,
bool isMac)
{
if (!Directory.Exists(toolboxRiderRootPath))
return Array.Empty<string>();
var channelDirs = Directory.GetDirectories(toolboxRiderRootPath);
var paths = channelDirs.SelectMany(channelDir =>
{
try
{
// use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
var historyFile = Path.Combine(channelDir, ".history.json");
if (File.Exists(historyFile))
{
var json = File.ReadAllText(historyFile);
var build = ToolboxHistory.GetLatestBuildFromJson(json);
if (build != null)
{
var buildDir = Path.Combine(channelDir, build);
var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
if (executablePaths.Any())
return executablePaths;
}
}
var channelFile = Path.Combine(channelDir, ".channel.settings.json");
if (File.Exists(channelFile))
{
var json = File.ReadAllText(channelFile).Replace("active-application", "active_application");
var build = ToolboxInstallData.GetLatestBuildFromJson(json);
if (build != null)
{
var buildDir = Path.Combine(channelDir, build);
var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
if (executablePaths.Any())
return executablePaths;
}
}
// changes in toolbox json files format may brake the logic above, so return all found Rider installations
return Directory.GetDirectories(channelDir)
.SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
}
catch (Exception e)
{
// do not write to Debug.Log, just log it.
Logger.Warn($"Failed to get RiderPath from {channelDir}", e);
}
return Array.Empty<string>();
})
.Where(c => !string.IsNullOrEmpty(c))
.ToArray();
return paths;
}
private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir)
{
var folder = new DirectoryInfo(Path.Combine(buildDir, dirName));
if (!folder.Exists)
return Array.Empty<string>();
if (!isMac)
return new[] { Path.Combine(folder.FullName, searchPattern) }.Where(File.Exists).ToArray();
return folder.GetDirectories(searchPattern).Select(f => f.FullName)
.Where(Directory.Exists).ToArray();
}
// Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
// Note that Unity disable this warning in the generated C# projects
#pragma warning disable 0649
[Serializable]
class SettingsJson
{
public string install_location;
[CanBeNull]
public static string GetInstallLocationFromJson(string json)
{
try
{
return JsonConvert.DeserializeObject<SettingsJson>(json).install_location;
}
catch (Exception)
{
Logger.Warn($"Failed to get install_location from json {json}");
}
return null;
}
}
[Serializable]
class ToolboxHistory
{
public List<ItemNode> history;
public static string GetLatestBuildFromJson(string json)
{
try
{
return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
}
catch (Exception)
{
Logger.Warn($"Failed to get latest build from json {json}");
}
return null;
}
}
[Serializable]
class ItemNode
{
public BuildNode item;
}
[Serializable]
class BuildNode
{
public string build;
}
[Serializable]
public class ProductInfo
{
public string version;
public string versionSuffix;
[CanBeNull]
internal static ProductInfo GetProductInfo(string json)
{
try
{
var productInfo = JsonConvert.DeserializeObject<ProductInfo>(json);
return productInfo;
}
catch (Exception)
{
Logger.Warn($"Failed to get version from json {json}");
}
return null;
}
}
// ReSharper disable once ClassNeverInstantiated.Global
[Serializable]
class ToolboxInstallData
{
// ReSharper disable once InconsistentNaming
public ActiveApplication active_application;
[CanBeNull]
public static string GetLatestBuildFromJson(string json)
{
try
{
var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json);
var builds = toolbox.active_application.builds;
if (builds != null && builds.Any())
return builds.First();
}
catch (Exception)
{
Logger.Warn($"Failed to get latest build from json {json}");
}
return null;
}
}
[Serializable]
class ActiveApplication
{
public List<string> builds;
}
#pragma warning restore 0649
public struct RiderInfo
{
// ReSharper disable once NotAccessedField.Global
public bool IsToolbox;
public string Presentation;
public Version BuildNumber;
public ProductInfo ProductInfo;
public string Path;
public RiderInfo(string path, bool isToolbox)
{
BuildNumber = GetBuildNumber(path);
ProductInfo = GetBuildVersion(path);
Path = new FileInfo(path).FullName; // normalize separators
var presentation = $"Rider {BuildNumber}";
if (ProductInfo != null && !string.IsNullOrEmpty(ProductInfo.version))
{
var suffix = string.IsNullOrEmpty(ProductInfo.versionSuffix) ? "" : $" {ProductInfo.versionSuffix}";
presentation = $"Rider {ProductInfo.version}{suffix}";
}
if (isToolbox)
presentation += " (JetBrains Toolbox)";
Presentation = presentation;
IsToolbox = isToolbox;
}
}
private static class Logger
{
internal static void Warn(string message, Exception e = null)
{
throw new Exception(message, e);
}
}
}
}

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
using GodotTools.Internals;
namespace GodotTools.Ides.Rider
{
public static class RiderPathManager
{
public static readonly string EditorPathSettingName = "mono/editor/editor_path_optional";
private static string GetRiderPathFromSettings()
{
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
if (editorSettings.HasSetting(EditorPathSettingName))
return (string)editorSettings.GetSetting(EditorPathSettingName);
return null;
}
public static void Initialize()
{
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor");
if (editor == ExternalEditorId.Rider)
{
if (!editorSettings.HasSetting(EditorPathSettingName))
{
Globals.EditorDef(EditorPathSettingName, "Optional");
editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = Variant.Type.String,
["name"] = EditorPathSettingName,
["hint"] = PropertyHint.File,
["hint_string"] = ""
});
}
var riderPath = (string)editorSettings.GetSetting(EditorPathSettingName);
if (IsRiderAndExists(riderPath))
{
Globals.EditorDef(EditorPathSettingName, riderPath);
return;
}
var paths = RiderPathLocator.GetAllRiderPaths();
if (!paths.Any())
return;
string newPath = paths.Last().Path;
Globals.EditorDef(EditorPathSettingName, newPath);
editorSettings.SetSetting(EditorPathSettingName, newPath);
}
}
public static bool IsExternalEditorSetToRider(EditorSettings editorSettings)
{
return editorSettings.HasSetting(EditorPathSettingName) &&
IsRider((string)editorSettings.GetSetting(EditorPathSettingName));
}
public static bool IsRider(string path)
{
if (string.IsNullOrEmpty(path))
return false;
if (path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) != -1)
return false;
var fileInfo = new FileInfo(path);
string filename = fileInfo.Name.ToLowerInvariant();
return filename.StartsWith("rider", StringComparison.Ordinal);
}
private static string CheckAndUpdatePath(string riderPath)
{
if (IsRiderAndExists(riderPath))
{
return riderPath;
}
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
var paths = RiderPathLocator.GetAllRiderPaths();
if (!paths.Any())
return null;
string newPath = paths.Last().Path;
editorSettings.SetSetting(EditorPathSettingName, newPath);
Globals.EditorDef(EditorPathSettingName, newPath);
return newPath;
}
private static bool IsRiderAndExists(string riderPath)
{
return !string.IsNullOrEmpty(riderPath) && IsRider(riderPath) && new FileInfo(riderPath).Exists;
}
public static void OpenFile(string slnPath, string scriptPath, int line)
{
string pathFromSettings = GetRiderPathFromSettings();
string path = CheckAndUpdatePath(pathFromSettings);
var args = new List<string>();
args.Add(slnPath);
if (line >= 0)
{
args.Add("--line");
args.Add((line + 1).ToString()); // https://github.com/JetBrains/godot-support/issues/61
}
args.Add(scriptPath);
try
{
Utils.OS.RunProcess(path, args);
}
catch (Exception e)
{
GD.PushError($"Error when trying to run code editor: JetBrains Rider. Exception message: '{e.Message}'");
}
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Runtime.CompilerServices;
using Godot;
namespace GodotTools.Internals
{
public class EditorProgress : IDisposable
{
public string Task { get; }
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_Create(string task, string label, int amount, bool canCancel);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_Dispose(string task);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool internal_Step(string task, string state, int step, bool forceRefresh);
public EditorProgress(string task, string label, int amount, bool canCancel = false)
{
Task = task;
internal_Create(task, label, amount, canCancel);
}
~EditorProgress()
{
// Should never rely on the GC to dispose EditorProgress.
// It should be disposed immediately when the task finishes.
GD.PushError("EditorProgress disposed by the Garbage Collector");
Dispose();
}
public void Dispose()
{
internal_Dispose(Task);
GC.SuppressFinalize(this);
}
public void Step(string state, int step = -1, bool forceRefresh = true)
{
internal_Step(Task, state, step, forceRefresh);
}
public bool TryStep(string state, int step = -1, bool forceRefresh = true)
{
return internal_Step(Task, state, step, forceRefresh);
}
}
}

View File

@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace GodotTools.Internals
{
public static class Globals
{
public static float EditorScale => internal_EditorScale();
public static object GlobalDef(string setting, object defaultValue, bool restartIfChanged = false) =>
internal_GlobalDef(setting, defaultValue, restartIfChanged);
public static object EditorDef(string setting, object defaultValue, bool restartIfChanged = false) =>
internal_EditorDef(setting, defaultValue, restartIfChanged);
public static object EditorShortcut(string setting) =>
internal_EditorShortcut(setting);
[SuppressMessage("ReSharper", "InconsistentNaming")]
public static string TTR(this string text) => internal_TTR(text);
// Internal Calls
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern float internal_EditorScale();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern object internal_GlobalDef(string setting, object defaultValue, bool restartIfChanged);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern object internal_EditorDef(string setting, object defaultValue, bool restartIfChanged);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern object internal_EditorShortcut(string setting);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_TTR(string text);
}
}

View File

@ -0,0 +1,107 @@
using System.Runtime.CompilerServices;
namespace GodotTools.Internals
{
public static class GodotSharpDirs
{
public static string ResDataDir => internal_ResDataDir();
public static string ResMetadataDir => internal_ResMetadataDir();
public static string ResAssembliesBaseDir => internal_ResAssembliesBaseDir();
public static string ResAssembliesDir => internal_ResAssembliesDir();
public static string ResConfigDir => internal_ResConfigDir();
public static string ResTempDir => internal_ResTempDir();
public static string ResTempAssembliesBaseDir => internal_ResTempAssembliesBaseDir();
public static string ResTempAssembliesDir => internal_ResTempAssembliesDir();
public static string MonoUserDir => internal_MonoUserDir();
public static string MonoLogsDir => internal_MonoLogsDir();
#region Tools-only
public static string MonoSolutionsDir => internal_MonoSolutionsDir();
public static string BuildLogsDirs => internal_BuildLogsDirs();
public static string ProjectAssemblyName => internal_ProjectAssemblyName();
public static string ProjectSlnPath => internal_ProjectSlnPath();
public static string ProjectCsProjPath => internal_ProjectCsProjPath();
public static string DataEditorToolsDir => internal_DataEditorToolsDir();
public static string DataEditorPrebuiltApiDir => internal_DataEditorPrebuiltApiDir();
#endregion
public static string DataMonoEtcDir => internal_DataMonoEtcDir();
public static string DataMonoLibDir => internal_DataMonoLibDir();
#region Windows-only
public static string DataMonoBinDir => internal_DataMonoBinDir();
#endregion
#region Internal
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResDataDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResMetadataDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResAssembliesBaseDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResAssembliesDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResConfigDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResTempDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResTempAssembliesBaseDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ResTempAssembliesDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_MonoUserDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_MonoLogsDir();
#region Tools-only
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_MonoSolutionsDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_BuildLogsDirs();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ProjectAssemblyName();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ProjectSlnPath();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_ProjectCsProjPath();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_DataEditorToolsDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_DataEditorPrebuiltApiDir();
#endregion
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_DataMonoEtcDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_DataMonoLibDir();
#region Windows-only
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_DataMonoBinDir();
#endregion
#endregion
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
using GodotTools.IdeMessaging.Requests;
namespace GodotTools.Internals
{
public static class Internal
{
public const string CSharpLanguageType = "CSharpScript";
public const string CSharpLanguageExtension = ".cs";
public static string UpdateApiAssembliesFromPrebuilt(string config) =>
internal_UpdateApiAssembliesFromPrebuilt(config);
public static string FullTemplatesDir =>
internal_FullTemplatesDir();
public static string SimplifyGodotPath(this string path) => internal_SimplifyGodotPath(path);
public static bool IsOsxAppBundleInstalled(string bundleId) => internal_IsOsxAppBundleInstalled(bundleId);
public static bool GodotIs32Bits() => internal_GodotIs32Bits();
public static bool GodotIsRealTDouble() => internal_GodotIsRealTDouble();
public static void GodotMainIteration() => internal_GodotMainIteration();
public static ulong GetCoreApiHash() => internal_GetCoreApiHash();
public static ulong GetEditorApiHash() => internal_GetEditorApiHash();
public static bool IsAssembliesReloadingNeeded() => internal_IsAssembliesReloadingNeeded();
public static void ReloadAssemblies(bool softReload) => internal_ReloadAssemblies(softReload);
public static void ScriptEditorDebuggerReloadScripts() => internal_ScriptEditorDebuggerReloadScripts();
public static bool ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus = true) =>
internal_ScriptEditorEdit(resource, line, col, grabFocus);
public static void EditorNodeShowScriptScreen() => internal_EditorNodeShowScriptScreen();
public static Dictionary<string, object> GetScriptsMetadataOrNothing() =>
internal_GetScriptsMetadataOrNothing(typeof(Dictionary<string, object>));
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();
public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) =>
internal_CodeCompletionRequest((int)kind, scriptFile);
#region Internal
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_UpdateApiAssembliesFromPrebuilt(string config);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_FullTemplatesDir();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_SimplifyGodotPath(this string path);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool internal_IsOsxAppBundleInstalled(string bundleId);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool internal_GodotIs32Bits();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool internal_GodotIsRealTDouble();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_GodotMainIteration();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern ulong internal_GetCoreApiHash();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern ulong internal_GetEditorApiHash();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool internal_IsAssembliesReloadingNeeded();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_ReloadAssemblies(bool softReload);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_ScriptEditorDebuggerReloadScripts();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool internal_ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_EditorNodeShowScriptScreen();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern Dictionary<string, object> internal_GetScriptsMetadataOrNothing(Type dictType);
[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();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile);
#endregion
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
namespace GodotTools.Internals
{
public static class ScriptClassParser
{
public class ClassDecl
{
public string Name { get; }
public string Namespace { get; }
public bool Nested { get; }
public int BaseCount { get; }
public string SearchName => Nested ?
Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) :
Name;
public ClassDecl(string name, string @namespace, bool nested, int baseCount)
{
Name = name;
Namespace = @namespace;
Nested = nested;
BaseCount = baseCount;
}
}
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern Error internal_ParseFile(string filePath, Array<Dictionary> classes, out string errorStr);
public static Error ParseFile(string filePath, out IEnumerable<ClassDecl> classes, out string errorStr)
{
var classesArray = new Array<Dictionary>();
var error = internal_ParseFile(filePath, classesArray, out errorStr);
if (error != Error.Ok)
{
classes = null;
return error;
}
var classesList = new List<ClassDecl>();
foreach (var classDeclDict in classesArray)
{
classesList.Add(new ClassDecl(
(string)classDeclDict["name"],
(string)classDeclDict["namespace"],
(bool)classDeclDict["nested"],
(int)classDeclDict["base_count"]
));
}
classes = classesList;
return Error.Ok;
}
}
}

View File

@ -0,0 +1,19 @@
namespace GodotTools
{
public struct PlaySettings
{
public bool HasDebugger { get; }
public string DebuggerHost { get; }
public int DebuggerPort { get; }
public bool BuildBeforePlaying { get; }
public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying)
{
HasDebugger = true;
DebuggerHost = debuggerHost;
DebuggerPort = debuggerPort;
BuildBeforePlaying = buildBeforePlaying;
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace GodotTools.Utils
{
public static class CollectionExtensions
{
public static T SelectFirstNotNull<T>(this IEnumerable<T> enumerable, Func<T, T> predicate, T orElse = null)
where T : class
{
foreach (T elem in enumerable)
{
T result = predicate(elem);
if (result != null)
return result;
}
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,40 @@
using System.IO;
using Godot;
namespace GodotTools.Utils
{
public static class Directory
{
private static string GlobalizePath(this string path)
{
return ProjectSettings.GlobalizePath(path);
}
public static bool Exists(string path)
{
return System.IO.Directory.Exists(path.GlobalizePath());
}
/// Create directory recursively
public static DirectoryInfo CreateDirectory(string path)
{
return System.IO.Directory.CreateDirectory(path.GlobalizePath());
}
public static void Delete(string path, bool recursive)
{
System.IO.Directory.Delete(path.GlobalizePath(), recursive);
}
public static string[] GetDirectories(string path, string searchPattern, SearchOption searchOption)
{
return System.IO.Directory.GetDirectories(path.GlobalizePath(), searchPattern, searchOption);
}
public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
{
return System.IO.Directory.GetFiles(path.GlobalizePath(), searchPattern, searchOption);
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using Godot;
namespace GodotTools.Utils
{
public static class File
{
private static string GlobalizePath(this string path)
{
return ProjectSettings.GlobalizePath(path);
}
public static void WriteAllText(string path, string contents)
{
System.IO.File.WriteAllText(path.GlobalizePath(), contents);
}
public static bool Exists(string path)
{
return System.IO.File.Exists(path.GlobalizePath());
}
public static DateTime GetLastWriteTime(string path)
{
return System.IO.File.GetLastWriteTime(path.GlobalizePath());
}
public static void Delete(string path)
{
System.IO.File.Delete(path.GlobalizePath());
}
public static void Copy(string sourceFileName, string destFileName)
{
System.IO.File.Copy(sourceFileName.GlobalizePath(), destFileName.GlobalizePath(), overwrite: true);
}
public static byte[] ReadAllBytes(string path)
{
return System.IO.File.ReadAllBytes(path.GlobalizePath());
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using Godot;
using GodotTools.Core;
using JetBrains.Annotations;
using Path = System.IO.Path;
namespace GodotTools.Utils
{
public static class FsPathUtils
{
private static readonly string _resourcePath = ProjectSettings.GlobalizePath("res://");
private static bool PathStartsWithAlreadyNorm(this string childPath, string parentPath)
{
// This won't work for Linux/macOS case insensitive file systems, but it's enough for our current problems
bool caseSensitive = !OS.IsWindows;
string parentPathNorm = parentPath.NormalizePath() + Path.DirectorySeparatorChar;
string childPathNorm = childPath.NormalizePath() + Path.DirectorySeparatorChar;
return childPathNorm.StartsWith(parentPathNorm,
caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
}
public static bool PathStartsWith(this string childPath, string parentPath)
{
string childPathNorm = childPath.NormalizePath() + Path.DirectorySeparatorChar;
string parentPathNorm = parentPath.NormalizePath() + Path.DirectorySeparatorChar;
return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm);
}
[CanBeNull]
public static string LocalizePathWithCaseChecked(string path)
{
string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar;
string resourcePathNorm = _resourcePath.NormalizePath() + Path.DirectorySeparatorChar;
if (!pathNorm.PathStartsWithAlreadyNorm(resourcePathNorm))
return null;
string result = "res://" + pathNorm.Substring(resourcePathNorm.Length);
// Remove the last separator we added
return result.Substring(0, result.Length - 1);
}
}
}

View File

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace GodotTools.Utils
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
public static class OS
{
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string GetPlatformName();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool UnixFileHasExecutableAccess(string filePath);
public static class Names
{
public const string Windows = "Windows";
public const string OSX = "OSX";
public const string X11 = "X11";
public const string Server = "Server";
public const string UWP = "UWP";
public const string Haiku = "Haiku";
public const string Android = "Android";
public const string iOS = "iOS";
public const string HTML5 = "HTML5";
}
public static class Platforms
{
public const string Windows = "windows";
public const string OSX = "osx";
public const string X11 = "x11";
public const string Server = "server";
public const string UWP = "uwp";
public const string Haiku = "haiku";
public const string Android = "android";
public const string iOS = "iphone";
public const string HTML5 = "javascript";
}
public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string>
{
[Names.Windows] = Platforms.Windows,
[Names.OSX] = Platforms.OSX,
[Names.X11] = Platforms.X11,
[Names.Server] = Platforms.Server,
[Names.UWP] = Platforms.UWP,
[Names.Haiku] = Platforms.Haiku,
[Names.Android] = Platforms.Android,
[Names.iOS] = Platforms.iOS,
[Names.HTML5] = Platforms.HTML5
};
private static bool IsOS(string name)
{
return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
private static bool IsAnyOS(IEnumerable<string> names)
{
return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase));
}
private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows));
private static readonly Lazy<bool> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX));
private static readonly Lazy<bool> _isX11 = new Lazy<bool>(() => IsOS(Names.X11));
private static readonly Lazy<bool> _isServer = new Lazy<bool>(() => IsOS(Names.Server));
private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP));
private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku));
private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android));
private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS));
private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5));
private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms));
public static bool IsWindows => _isWindows.Value || IsUWP;
public static bool IsOSX => _isOSX.Value;
public static bool IsX11 => _isX11.Value;
public static bool IsServer => _isServer.Value;
public static bool IsUWP => _isUWP.Value;
public static bool IsHaiku => _isHaiku.Value;
public static bool IsAndroid => _isAndroid.Value;
public static bool IsiOS => _isiOS.Value;
public static bool IsHTML5 => _isHTML5.Value;
private static readonly string[] UnixLikePlatforms = {Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS};
public static bool IsUnixLike => _isUnixLike.Value;
public static char PathSep => IsWindows ? ';' : ':';
public static string PathWhich([NotNull] string name)
{
if (IsWindows)
return PathWhichWindows(name);
return PathWhichUnix(name);
}
private static string PathWhichWindows([NotNull] string name)
{
string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>();
string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
char[] invalidPathChars = Path.GetInvalidPathChars();
var searchDirs = new List<string>();
if (pathDirs != null)
{
foreach (var pathDir in pathDirs)
{
if (pathDir.IndexOfAny(invalidPathChars) != -1)
continue;
searchDirs.Add(pathDir);
}
}
string nameExt = Path.GetExtension(name);
bool hasPathExt = !string.IsNullOrEmpty(nameExt) &&
windowsExts.Contains(nameExt, StringComparer.OrdinalIgnoreCase);
searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list
if (hasPathExt)
return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists);
return (from dir in searchDirs
select Path.Combine(dir, name)
into path
from ext in windowsExts
select path + ext).FirstOrDefault(File.Exists);
}
private static string PathWhichUnix([NotNull] string name)
{
string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
char[] invalidPathChars = Path.GetInvalidPathChars();
var searchDirs = new List<string>();
if (pathDirs != null)
{
foreach (var pathDir in pathDirs)
{
if (pathDir.IndexOfAny(invalidPathChars) != -1)
continue;
searchDirs.Add(pathDir);
}
}
searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list
return searchDirs.Select(dir => Path.Combine(dir, name))
.FirstOrDefault(path => File.Exists(path) && UnixFileHasExecutableAccess(path));
}
public static void RunProcess(string command, IEnumerable<string> arguments)
{
// TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
string CmdLineArgsToString(IEnumerable<string> args)
{
// Not perfect, but as long as we are careful...
return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
}
var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments))
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
if (process == null)
throw new Exception("No process was started");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (IsWindows && process.Id > 0)
User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself
}
}
public static int ExecuteCommand(string command, IEnumerable<string> arguments)
{
// TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
string CmdLineArgsToString(IEnumerable<string> args)
{
// Not perfect, but as long as we are careful...
return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
}
var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments));
Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}");
// Print the output
startInfo.RedirectStandardOutput = false;
startInfo.RedirectStandardError = false;
startInfo.UseShellExecute = false;
using (var process = new Process { StartInfo = startInfo })
{
process.Start();
process.WaitForExit();
return process.ExitCode;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More