diff options
Diffstat (limited to 'modules/mono')
175 files changed, 8360 insertions, 5201 deletions
diff --git a/modules/mono/SCsub b/modules/mono/SCsub index c723b210cb..e8f3174a0a 100644 --- a/modules/mono/SCsub +++ b/modules/mono/SCsub @@ -29,7 +29,7 @@ if env_mono["tools"] or env_mono["target"] != "release": mono_configure.configure(env, env_mono) -if env_mono["tools"] and env_mono["mono_glue"]: +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 diff --git a/modules/mono/build_scripts/godot_tools_build.py b/modules/mono/build_scripts/godot_tools_build.py index cffacf2577..3bbbf29d3b 100644 --- a/modules/mono/build_scripts/godot_tools_build.py +++ b/modules/mono/build_scripts/godot_tools_build.py @@ -13,13 +13,11 @@ def build_godot_tools(source, target, env): solution_path = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln") build_config = "Debug" if env["target"] == "debug" else "Release" - # Custom build target to make sure output is always copied to the data dir. - extra_build_args = ["/Target:Build;GodotTools:BuildAlwaysCopyToDataDir"] + from .solution_builder import build_solution - from .solution_builder import build_solution, nuget_restore + extra_msbuild_args = ["/p:GodotPlatform=" + env["platform"]] - nuget_restore(env, solution_path) - build_solution(env, solution_path, build_config, extra_build_args) + build_solution(env, solution_path, build_config, extra_msbuild_args) # No need to copy targets. The GodotTools csproj takes care of copying them. diff --git a/modules/mono/build_scripts/mono_configure.py b/modules/mono/build_scripts/mono_configure.py index 23f01b3cca..3e771e06f0 100644 --- a/modules/mono/build_scripts/mono_configure.py +++ b/modules/mono/build_scripts/mono_configure.py @@ -125,7 +125,8 @@ def configure(env, env_mono): 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" + "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, @@ -191,17 +192,16 @@ def configure(env, env_mono): 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 - ) + mono_lib_file = find_file_in_dir(mono_lib_path, mono_lib_names, extensions=lib_suffixes) - if not mono_lib_name: + if not mono_lib_file: raise RuntimeError("Could not find mono library in: " + mono_lib_path) if env.msvc: - env.Append(LINKFLAGS=mono_lib_name + ".lib") + env.Append(LINKFLAGS=mono_lib_file) else: - env.Append(LIBS=[mono_lib_name]) + mono_lib_file_path = os.path.join(mono_lib_path, mono_lib_file) + env.Append(LINKFLAGS=mono_lib_file_path) mono_bin_path = os.path.join(mono_root, "bin") diff --git a/modules/mono/build_scripts/solution_builder.py b/modules/mono/build_scripts/solution_builder.py index db6b4ff7aa..03f4e57f02 100644 --- a/modules/mono/build_scripts/solution_builder.py +++ b/modules/mono/build_scripts/solution_builder.py @@ -4,91 +4,41 @@ import os verbose = False -def find_nuget_unix(): - import os - - if "NUGET_PATH" in os.environ: - hint_path = os.environ["NUGET_PATH"] - if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): - return hint_path - hint_path = os.path.join(hint_path, "nuget") - if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): - return hint_path - +def find_dotnet_cli(): import os.path - import sys - - hint_dirs = ["/opt/novell/mono/bin"] - if sys.platform == "darwin": - hint_dirs = [ - "/Library/Frameworks/Mono.framework/Versions/Current/bin", - "/usr/local/var/homebrew/linked/mono/bin", - ] + hint_dirs - for hint_dir in hint_dirs: - hint_path = os.path.join(hint_dir, "nuget") - 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, "nuget") - 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_nuget_windows(env): - import os - - if "NUGET_PATH" in os.environ: - hint_path = os.environ["NUGET_PATH"] - if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): - return hint_path - hint_path = os.path.join(hint_path, "nuget.exe") - if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): - return hint_path - - from .mono_reg_utils import find_mono_root_dir - - mono_root = env["mono_prefix"] or find_mono_root_dir(env["bits"]) - - if mono_root: - mono_bin_dir = os.path.join(mono_root, "bin") - nuget_mono = os.path.join(mono_bin_dir, "nuget.bat") - - if os.path.isfile(nuget_mono): - return nuget_mono - - # Standalone NuGet - - for hint_dir in os.environ["PATH"].split(os.pathsep): - hint_dir = hint_dir.strip('"') - hint_path = os.path.join(hint_dir, "nuget.exe") - if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): - return hint_path - - return None + 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(filename): +def find_msbuild_unix(): import os.path import sys - hint_dirs = ["/opt/novell/mono/bin"] + hint_dirs = [] if sys.platform == "darwin": - hint_dirs = [ + hint_dirs[:0] = [ "/Library/Frameworks/Mono.framework/Versions/Current/bin", "/usr/local/var/homebrew/linked/mono/bin", - ] + hint_dirs + ] for hint_dir in hint_dirs: - hint_path = os.path.join(hint_dir, filename) + hint_path = os.path.join(hint_dir, "msbuild") if os.path.isfile(hint_path): return hint_path elif os.path.isfile(hint_path + ".exe"): @@ -96,7 +46,7 @@ def find_msbuild_unix(filename): for hint_dir in os.environ["PATH"].split(os.pathsep): hint_dir = hint_dir.strip('"') - hint_path = os.path.join(hint_dir, filename) + 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): @@ -158,21 +108,6 @@ def run_command(command, args, env_override=None, name=None): raise RuntimeError("'%s' exited with error code: %s" % (name, e.returncode)) -def nuget_restore(env, *args): - global verbose - verbose = env["verbose"] - - # Find NuGet - nuget_path = find_nuget_windows(env) if os.name == "nt" else find_nuget_unix() - if nuget_path is None: - raise RuntimeError("Cannot find NuGet executable") - - print("NuGet path: " + nuget_path) - - # Do NuGet restore - run_command(nuget_path, ["restore"] + list(args), name="nuget restore") - - def build_solution(env, solution_path, build_config, extra_msbuild_args=[]): global verbose verbose = env["verbose"] @@ -183,38 +118,31 @@ def build_solution(env, solution_path, build_config, extra_msbuild_args=[]): if "PLATFORM" in msbuild_env: del msbuild_env["PLATFORM"] - # 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("msbuild") - if msbuild_path is None: - xbuild_fallback = env["xbuild_fallback"] - - if xbuild_fallback and os.name == "nt": - print("Option 'xbuild_fallback' not supported on Windows") - xbuild_fallback = False - - if xbuild_fallback: - print("Cannot find MSBuild executable, trying with xbuild") - print("Warning: xbuild is deprecated") + msbuild_args = [] - msbuild_path = find_msbuild_unix("xbuild") + dotnet_cli = find_dotnet_cli() - if msbuild_path is None: - raise RuntimeError("Cannot find xbuild executable") - else: + 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, "/p:Configuration=" + build_config] + 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") diff --git a/modules/mono/class_db_api_json.cpp b/modules/mono/class_db_api_json.cpp index 384685d04b..d7b2028204 100644 --- a/modules/mono/class_db_api_json.cpp +++ b/modules/mono/class_db_api_json.cpp @@ -45,18 +45,17 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { const StringName *k = nullptr; 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) + if (t->api != p_api || !t->exposed) { continue; + } Dictionary class_dict; classes_dict[t->name] = class_dict; @@ -70,13 +69,13 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { k = nullptr; while ((k = t->method_map.next(k))) { - String name = k->operator String(); ERR_CONTINUE(name.empty()); - if (name[0] == '_') + if (name[0] == '_') { continue; // Ignore non-virtual methods that start with an underscore + } snames.push_back(*k); } @@ -135,7 +134,6 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { k = nullptr; while ((k = t->constant_map.next(k))) { - snames.push_back(*k); } @@ -163,7 +161,6 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { k = nullptr; while ((k = t->signal_map.next(k))) { - snames.push_back(*k); } @@ -199,7 +196,6 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { k = nullptr; while ((k = t->property_setget.next(k))) { - snames.push_back(*k); } diff --git a/modules/mono/config.py b/modules/mono/config.py index 106ca6e028..cd659057ef 100644 --- a/modules/mono/config.py +++ b/modules/mono/config.py @@ -30,12 +30,12 @@ def configure(env): ) 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", False ) ) - envvars.Add(BoolVariable("xbuild_fallback", "If MSBuild is not found, fallback to xbuild", False)) # TODO: It would be great if this could be detected automatically instead envvars.Add( @@ -57,7 +57,6 @@ def configure(env): def get_doc_classes(): return [ - "@C#", "CSharpScript", "GodotSharp", ] diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index f5911275c9..bbdec224f0 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -44,7 +44,6 @@ #ifdef TOOLS_ENABLED #include "editor/bindings_generator.h" -#include "editor/csharp_project.h" #include "editor/editor_node.h" #include "editor/node_dock.h" #endif @@ -67,7 +66,6 @@ #ifdef TOOLS_ENABLED static bool _create_project_solution_if_needed() { - String sln_path = GodotSharpDirs::get_project_sln_path(); String csproj_path = GodotSharpDirs::get_project_csproj_path(); @@ -85,28 +83,23 @@ static bool _create_project_solution_if_needed() { CSharpLanguage *CSharpLanguage::singleton = nullptr; String CSharpLanguage::get_name() const { - return "C#"; } String CSharpLanguage::get_type() const { - return "CSharpScript"; } String CSharpLanguage::get_extension() const { - return "cs"; } Error CSharpLanguage::execute_file(const String &p_path) { - // ?? return OK; } void CSharpLanguage::init() { - #ifdef DEBUG_METHODS_ENABLED if (OS::get_singleton()->get_cmdline_args().find("--class-db-json")) { class_db_api_to_json("user://class_db_api.json", ClassDB::API_CORE); @@ -131,8 +124,9 @@ void CSharpLanguage::init() { print_line("Run this binary with '--generate-mono-glue path/to/modules/mono/glue'"); #endif - if (gdmono->is_runtime_initialized()) + if (gdmono->is_runtime_initialized()) { gdmono->initialize_load_assemblies(); + } #ifdef TOOLS_ENABLED EditorNode::add_init_callback(&_editor_init_callback); @@ -140,9 +134,13 @@ void CSharpLanguage::init() { } void CSharpLanguage::finish() { + finalize(); +} - if (finalized) +void CSharpLanguage::finalize() { + if (finalized) { return; + } finalizing = true; @@ -184,7 +182,6 @@ void CSharpLanguage::finish() { } void CSharpLanguage::get_reserved_words(List<String> *p_words) const { - static const char *_reserved_words[] = { // Reserved keywords "abstract", @@ -295,7 +292,7 @@ void CSharpLanguage::get_reserved_words(List<String> *p_words) const { "when", "where", "yield", - 0 + nullptr }; const char **w = _reserved_words; @@ -307,13 +304,11 @@ void CSharpLanguage::get_reserved_words(List<String> *p_words) const { } void CSharpLanguage::get_comment_delimiters(List<String> *p_delimiters) const { - p_delimiters->push_back("//"); // single-line comment p_delimiters->push_back("/* */"); // delimited comment } void CSharpLanguage::get_string_delimiters(List<String> *p_delimiters) const { - p_delimiters->push_back("' '"); // character literal p_delimiters->push_back("\" \""); // regular string literal // Verbatim string literals (`@" "`) don't render correctly, so don't highlight them. @@ -321,7 +316,6 @@ void CSharpLanguage::get_string_delimiters(List<String> *p_delimiters) const { } static String get_base_class_name(const String &p_base_class_name, const String p_class_name) { - String base_class = p_base_class_name; if (p_class_name == base_class) { base_class = "Godot." + base_class; @@ -330,7 +324,6 @@ static String get_base_class_name(const String &p_base_class_name, const String } Ref<Script> CSharpLanguage::get_template(const String &p_class_name, const String &p_base_class_name) const { - String script_template = "using " BINDINGS_NAMESPACE ";\n" "using System;\n" "\n" @@ -366,12 +359,10 @@ Ref<Script> CSharpLanguage::get_template(const String &p_class_name, const Strin } bool CSharpLanguage::is_using_templates() { - return true; } void CSharpLanguage::make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script) { - String src = p_script->get_source_code(); String base_class_name = get_base_class_name(p_base_class_name, p_class_name); src = src.replace("%BASE%", base_class_name) @@ -381,7 +372,6 @@ void CSharpLanguage::make_template(const String &p_class_name, const String &p_b } String CSharpLanguage::validate_path(const String &p_path) const { - String class_name = p_path.get_file().get_basename(); List<String> keywords; get_reserved_words(&keywords); @@ -392,32 +382,30 @@ String CSharpLanguage::validate_path(const String &p_path) const { } Script *CSharpLanguage::create_script() const { - return memnew(CSharpScript); } bool CSharpLanguage::has_named_classes() const { - return false; } bool CSharpLanguage::supports_builtin_mode() const { - return false; } #ifdef TOOLS_ENABLED static String variant_type_to_managed_name(const String &p_var_type_name) { - - if (p_var_type_name.empty()) + if (p_var_type_name.empty()) { return "object"; + } if (!ClassDB::class_exists(p_var_type_name)) { return p_var_type_name; } - if (p_var_type_name == Variant::get_type_name(Variant::OBJECT)) + if (p_var_type_name == Variant::get_type_name(Variant::OBJECT)) { return "Godot.Object"; + } if (p_var_type_name == Variant::get_type_name(Variant::FLOAT)) { #ifdef REAL_T_IS_DOUBLE @@ -427,36 +415,49 @@ static String variant_type_to_managed_name(const String &p_var_type_name) { #endif } - if (p_var_type_name == Variant::get_type_name(Variant::STRING)) + if (p_var_type_name == Variant::get_type_name(Variant::STRING)) { return "string"; // I prefer this one >:[ + } - if (p_var_type_name == Variant::get_type_name(Variant::DICTIONARY)) + if (p_var_type_name == Variant::get_type_name(Variant::DICTIONARY)) { return "Collections.Dictionary"; + } - if (p_var_type_name == Variant::get_type_name(Variant::ARRAY)) + if (p_var_type_name == Variant::get_type_name(Variant::ARRAY)) { return "Collections.Array"; + } - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_BYTE_ARRAY)) + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_BYTE_ARRAY)) { return "byte[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_INT32_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_INT32_ARRAY)) { return "int[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_INT64_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_INT64_ARRAY)) { return "long[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_FLOAT32_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_FLOAT32_ARRAY)) { return "float[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_FLOAT64_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_FLOAT64_ARRAY)) { return "double[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_STRING_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_STRING_ARRAY)) { return "string[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_VECTOR2_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_VECTOR2_ARRAY)) { return "Vector2[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_VECTOR3_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_VECTOR3_ARRAY)) { return "Vector3[]"; - if (p_var_type_name == Variant::get_type_name(Variant::PACKED_COLOR_ARRAY)) + } + if (p_var_type_name == Variant::get_type_name(Variant::PACKED_COLOR_ARRAY)) { return "Color[]"; + } - if (p_var_type_name == Variant::get_type_name(Variant::SIGNAL)) + if (p_var_type_name == Variant::get_type_name(Variant::SIGNAL)) { return "SignalInfo"; + } Variant::Type var_types[] = { Variant::BOOL, @@ -481,8 +482,9 @@ static String variant_type_to_managed_name(const String &p_var_type_name) { }; for (unsigned int i = 0; i < sizeof(var_types) / sizeof(Variant::Type); i++) { - if (p_var_type_name == Variant::get_type_name(var_types[i])) + if (p_var_type_name == Variant::get_type_name(var_types[i])) { return p_var_type_name; + } } return "object"; @@ -496,8 +498,9 @@ String CSharpLanguage::make_function(const String &, const String &p_name, const for (int i = 0; i < p_args.size(); i++) { const String &arg = p_args[i]; - if (i > 0) + if (i > 0) { s += ", "; + } s += variant_type_to_managed_name(arg.get_slice(":", 1)) + " " + escape_csharp_keyword(arg.get_slice(":", 0)); } @@ -531,60 +534,60 @@ String CSharpLanguage::_get_indentation() const { } String CSharpLanguage::debug_get_error() const { - return _debug_error; } int CSharpLanguage::debug_get_stack_level_count() const { - - if (_debug_parse_err_line >= 0) + if (_debug_parse_err_line >= 0) { return 1; + } // TODO: StackTrace return 1; } int CSharpLanguage::debug_get_stack_level_line(int p_level) const { - - if (_debug_parse_err_line >= 0) + if (_debug_parse_err_line >= 0) { return _debug_parse_err_line; + } // TODO: StackTrace return 1; } String CSharpLanguage::debug_get_stack_level_function(int p_level) const { - - if (_debug_parse_err_line >= 0) + if (_debug_parse_err_line >= 0) { return String(); + } // TODO: StackTrace return String(); } String CSharpLanguage::debug_get_stack_level_source(int p_level) const { - - if (_debug_parse_err_line >= 0) + if (_debug_parse_err_line >= 0) { return _debug_parse_err_file; + } // TODO: StackTrace return String(); } Vector<ScriptLanguage::StackInfo> CSharpLanguage::debug_get_current_stack_info() { - #ifdef DEBUG_ENABLED // Printing an error here will result in endless recursion, so we must be careful static thread_local bool _recursion_flag_ = false; - if (_recursion_flag_) + if (_recursion_flag_) { return Vector<StackInfo>(); + } _recursion_flag_ = true; SCOPE_EXIT { _recursion_flag_ = false; }; GD_MONO_SCOPE_THREAD_ATTACH; - if (!gdmono->is_runtime_initialized() || !GDMono::get_singleton()->get_core_api_assembly() || !GDMonoCache::cached_data.corlib_cache_updated) + if (!gdmono->is_runtime_initialized() || !GDMono::get_singleton()->get_core_api_assembly() || !GDMonoCache::cached_data.corlib_cache_updated) { return Vector<StackInfo>(); + } MonoObject *stack_trace = mono_object_new(mono_domain_get(), CACHED_CLASS(System_Diagnostics_StackTrace)->get_mono_ptr()); @@ -604,11 +607,11 @@ Vector<ScriptLanguage::StackInfo> CSharpLanguage::debug_get_current_stack_info() #ifdef DEBUG_ENABLED Vector<ScriptLanguage::StackInfo> CSharpLanguage::stack_trace_get_info(MonoObject *p_stack_trace) { - // Printing an error here will result in endless recursion, so we must be careful static thread_local bool _recursion_flag_ = false; - if (_recursion_flag_) + if (_recursion_flag_) { return Vector<StackInfo>(); + } _recursion_flag_ = true; SCOPE_EXIT { _recursion_flag_ = false; }; @@ -625,8 +628,9 @@ Vector<ScriptLanguage::StackInfo> CSharpLanguage::stack_trace_get_info(MonoObjec int frame_count = mono_array_length(frames); - if (frame_count <= 0) + if (frame_count <= 0) { return Vector<StackInfo>(); + } Vector<StackInfo> si; si.resize(frame_count); @@ -672,13 +676,13 @@ void CSharpLanguage::pre_unsafe_unreference(Object *p_obj) { ObjectID id = p_obj->get_instance_id(); Map<ObjectID, int>::Element *elem = unsafe_object_references.find(id); ERR_FAIL_NULL(elem); - if (--elem->value() == 0) + if (--elem->value() == 0) { unsafe_object_references.erase(elem); + } #endif } void CSharpLanguage::frame() { - if (gdmono && gdmono->is_runtime_initialized() && gdmono->get_core_api_assembly() != nullptr) { const Ref<MonoGCHandleRef> &task_scheduler_handle = GDMonoCache::cached_data.task_scheduler_handle; @@ -698,11 +702,11 @@ void CSharpLanguage::frame() { } struct CSharpScriptDepSort { - // must support sorting so inheritance works properly (parent must be reloaded first) bool operator()(const Ref<CSharpScript> &A, const Ref<CSharpScript> &B) const { - if (A == B) + if (A == B) { return false; // shouldn't happen but.. + } GDMonoClass *I = B->base; while (I) { if (I == A->script_class) { @@ -718,7 +722,6 @@ struct CSharpScriptDepSort { }; void CSharpLanguage::reload_all_scripts() { - #ifdef GD_MONO_HOT_RELOAD if (is_assembly_reloading_needed()) { GD_MONO_SCOPE_THREAD_ATTACH; @@ -728,7 +731,6 @@ void CSharpLanguage::reload_all_scripts() { } void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) { - (void)p_script; // UNUSED CRASH_COND(!Engine::get_singleton()->is_editor_hint()); @@ -747,9 +749,9 @@ void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft #ifdef GD_MONO_HOT_RELOAD bool CSharpLanguage::is_assembly_reloading_needed() { - - if (!gdmono->is_runtime_initialized()) + if (!gdmono->is_runtime_initialized()) { return false; + } GDMonoAssembly *proj_assembly = gdmono->get_project_assembly(); @@ -767,24 +769,27 @@ bool CSharpLanguage::is_assembly_reloading_needed() { if (!FileAccess::exists(proj_asm_path)) { // Maybe it wasn't loaded from the default path, so check this as well proj_asm_path = GodotSharpDirs::get_res_temp_assemblies_dir().plus_file(appname_safe); - if (!FileAccess::exists(proj_asm_path)) + if (!FileAccess::exists(proj_asm_path)) { return false; // No assembly to load + } } - if (FileAccess::get_modified_time(proj_asm_path) <= proj_assembly->get_modified_time()) + if (FileAccess::get_modified_time(proj_asm_path) <= proj_assembly->get_modified_time()) { return false; // Already up to date + } } else { - if (!FileAccess::exists(GodotSharpDirs::get_res_temp_assemblies_dir().plus_file(appname_safe))) + if (!FileAccess::exists(GodotSharpDirs::get_res_temp_assemblies_dir().plus_file(appname_safe))) { return false; // No assembly to load + } } return true; } void CSharpLanguage::reload_assemblies(bool p_soft_reload) { - - if (!gdmono->is_runtime_initialized()) + if (!gdmono->is_runtime_initialized()) { return; + } // There is no soft reloading with Mono. It's always hard reloading. @@ -850,7 +855,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { to_reload.push_back(script); if (script->get_path().empty()) { - script->tied_class_name_for_reload = script->script_class->get_name(); + script->tied_class_name_for_reload = script->script_class->get_name_for_lookup(); script->tied_class_namespace_for_reload = script->script_class->get_namespace(); } @@ -890,8 +895,9 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { CSharpInstance *csi = static_cast<CSharpInstance *>(obj->get_script_instance()); // Call OnBeforeSerialize - if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) - obj->get_script_instance()->call_multilevel(string_names.on_before_serialize); + if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) { + obj->get_script_instance()->call(string_names.on_before_serialize); + } // Save instance info CSharpScript::StateBackup state; @@ -926,8 +932,9 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { for (const Map<ObjectID, CSharpScript::StateBackup>::Element *F = scr->pending_reload_state.front(); F; F = F->next()) { Object *obj = ObjectDB::get_instance(F->key()); - if (!obj) + if (!obj) { continue; + } ObjectID obj_id = obj->get_instance_id(); @@ -1124,8 +1131,9 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { } // Call OnAfterDeserialization - if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) - obj->get_script_instance()->call_multilevel(string_names.on_after_deserialize); + if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) { + obj->get_script_instance()->call(string_names.on_after_deserialize); + } } } @@ -1173,7 +1181,6 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { #endif void CSharpLanguage::_load_scripts_metadata() { - scripts_metadata.clear(); String scripts_metadata_filename = "scripts_metadata."; @@ -1218,24 +1225,20 @@ void CSharpLanguage::_load_scripts_metadata() { } void CSharpLanguage::get_recognized_extensions(List<String> *p_extensions) const { - p_extensions->push_back("cs"); } #ifdef TOOLS_ENABLED Error CSharpLanguage::open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col) { - return (Error)(int)get_godotsharp_editor()->call("OpenInExternalEditor", p_script, p_line, p_col); } bool CSharpLanguage::overrides_external_editor() { - return get_godotsharp_editor()->call("OverridesExternalEditor"); } #endif void CSharpLanguage::thread_enter() { - #if 0 if (gdmono->is_runtime_initialized()) { GDMonoUtils::attach_current_thread(); @@ -1244,7 +1247,6 @@ void CSharpLanguage::thread_enter() { } void CSharpLanguage::thread_exit() { - #if 0 if (gdmono->is_runtime_initialized()) { GDMonoUtils::detach_current_thread(); @@ -1253,7 +1255,6 @@ void CSharpLanguage::thread_exit() { } bool CSharpLanguage::debug_break_parse(const String &p_file, int p_line, const String &p_error) { - // Not a parser error in our case, but it's still used for other type of errors if (EngineDebugger::is_active() && Thread::get_caller_id() == Thread::get_main_id()) { _debug_parse_err_line = p_line; @@ -1267,7 +1268,6 @@ bool CSharpLanguage::debug_break_parse(const String &p_file, int p_line, const S } bool CSharpLanguage::debug_break(const String &p_error, bool p_allow_continue) { - if (EngineDebugger::is_active() && Thread::get_caller_id() == Thread::get_main_id()) { _debug_parse_err_line = -1; _debug_parse_err_file = ""; @@ -1286,6 +1286,7 @@ void CSharpLanguage::_on_scripts_domain_unloaded() { script_binding.inited = false; } +#ifdef GD_MONO_HOT_RELOAD { MutexLock lock(ManagedCallable::instances_mutex); @@ -1295,13 +1296,13 @@ void CSharpLanguage::_on_scripts_domain_unloaded() { managed_callable->delegate_invoke = nullptr; } } +#endif scripts_metadata_invalidated = true; } #ifdef TOOLS_ENABLED void CSharpLanguage::_editor_init_callback() { - register_editor_internal_calls(); // Initialize GodotSharpEditor @@ -1316,7 +1317,8 @@ void CSharpLanguage::_editor_init_callback() { GDMonoUtils::runtime_object_init(mono_object, editor_klass, &exc); UNHANDLED_EXCEPTION(exc); - EditorPlugin *godotsharp_editor = Object::cast_to<EditorPlugin>(GDMonoMarshal::mono_object_to_variant(mono_object)); + EditorPlugin *godotsharp_editor = Object::cast_to<EditorPlugin>( + GDMonoMarshal::mono_object_to_variant(mono_object).operator Object *()); CRASH_COND(godotsharp_editor == nullptr); // Enable it as a plugin @@ -1328,13 +1330,11 @@ void CSharpLanguage::_editor_init_callback() { #endif void CSharpLanguage::set_language_index(int p_idx) { - ERR_FAIL_COND(lang_idx != -1); lang_idx = p_idx; } void CSharpLanguage::release_script_gchandle(MonoGCHandleData &p_gchandle) { - if (!p_gchandle.is_released()) { // Do not lock unnecessarily MutexLock lock(get_singleton()->script_gchandle_release_mutex); p_gchandle.release(); @@ -1342,7 +1342,6 @@ void CSharpLanguage::release_script_gchandle(MonoGCHandleData &p_gchandle) { } void CSharpLanguage::release_script_gchandle(MonoObject *p_expected_obj, MonoGCHandleData &p_gchandle) { - uint32_t pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(p_expected_obj); // We might lock after this, so pin it if (!p_gchandle.is_released()) { // Do not lock unnecessarily @@ -1363,19 +1362,16 @@ void CSharpLanguage::release_script_gchandle(MonoObject *p_expected_obj, MonoGCH } CSharpLanguage::CSharpLanguage() { - ERR_FAIL_COND_MSG(singleton, "C# singleton already exist."); singleton = this; } CSharpLanguage::~CSharpLanguage() { - - finish(); + finalize(); singleton = nullptr; } bool CSharpLanguage::setup_csharp_script_binding(CSharpScriptBinding &r_script_binding, Object *p_object) { - #ifdef DEBUG_ENABLED // I don't trust you if (p_object->get_script_instance()) { @@ -1388,8 +1384,9 @@ bool CSharpLanguage::setup_csharp_script_binding(CSharpScriptBinding &r_script_b // ¯\_(ツ)_/¯ const ClassDB::ClassInfo *classinfo = ClassDB::classes.getptr(type_name); - while (classinfo && !classinfo->exposed) + while (classinfo && !classinfo->exposed) { classinfo = classinfo->inherits_ptr; + } ERR_FAIL_NULL_V(classinfo, false); type_name = classinfo->name; @@ -1424,28 +1421,27 @@ bool CSharpLanguage::setup_csharp_script_binding(CSharpScriptBinding &r_script_b } void *CSharpLanguage::alloc_instance_binding_data(Object *p_object) { - MutexLock lock(language_bind_mutex); Map<Object *, CSharpScriptBinding>::Element *match = script_bindings.find(p_object); - if (match) + if (match) { return (void *)match; + } CSharpScriptBinding script_binding; - if (!setup_csharp_script_binding(script_binding, p_object)) + if (!setup_csharp_script_binding(script_binding, p_object)) { return nullptr; + } return (void *)insert_script_binding(p_object, script_binding); } Map<Object *, CSharpScriptBinding>::Element *CSharpLanguage::insert_script_binding(Object *p_object, const CSharpScriptBinding &p_script_binding) { - return script_bindings.insert(p_object, p_script_binding); } void CSharpLanguage::free_instance_binding_data(void *p_data) { - if (GDMono::get_singleton() == nullptr) { #ifdef DEBUG_ENABLED CRASH_COND(!script_bindings.empty()); @@ -1454,8 +1450,9 @@ void CSharpLanguage::free_instance_binding_data(void *p_data) { return; } - if (finalizing) + if (finalizing) { return; // inside CSharpLanguage::finish(), all the gchandle bindings are released there + } GD_MONO_ASSERT_THREAD_ATTACHED; @@ -1481,7 +1478,6 @@ void CSharpLanguage::free_instance_binding_data(void *p_data) { } void CSharpLanguage::refcount_incremented_instance_binding(Object *p_object) { - Reference *ref_owner = Object::cast_to<Reference>(p_object); #ifdef DEBUG_ENABLED @@ -1495,8 +1491,9 @@ void CSharpLanguage::refcount_incremented_instance_binding(Object *p_object) { CSharpScriptBinding &script_binding = ((Map<Object *, CSharpScriptBinding>::Element *)data)->get(); MonoGCHandleData &gchandle = script_binding.gchandle; - if (!script_binding.inited) + if (!script_binding.inited) { return; + } if (ref_owner->reference_get_count() > 1 && gchandle.is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 GD_MONO_SCOPE_THREAD_ATTACH; @@ -1506,8 +1503,9 @@ void CSharpLanguage::refcount_incremented_instance_binding(Object *p_object) { // so the owner must hold the managed side alive again to avoid it from being GCed. MonoObject *target = gchandle.get_target(); - if (!target) + if (!target) { return; // Called after the managed side was collected, so nothing to do here + } // Release the current weak handle and replace it with a strong handle. MonoGCHandleData strong_gchandle = MonoGCHandleData::new_strong_handle(target); @@ -1517,7 +1515,6 @@ void CSharpLanguage::refcount_incremented_instance_binding(Object *p_object) { } bool CSharpLanguage::refcount_decremented_instance_binding(Object *p_object) { - Reference *ref_owner = Object::cast_to<Reference>(p_object); #ifdef DEBUG_ENABLED @@ -1533,8 +1530,9 @@ bool CSharpLanguage::refcount_decremented_instance_binding(Object *p_object) { int refcount = ref_owner->reference_get_count(); - if (!script_binding.inited) + if (!script_binding.inited) { return refcount == 0; + } if (refcount == 1 && !gchandle.is_released() && !gchandle.is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 GD_MONO_SCOPE_THREAD_ATTACH; @@ -1543,8 +1541,9 @@ bool CSharpLanguage::refcount_decremented_instance_binding(Object *p_object) { // the managed instance takes responsibility of deleting the owner when GCed. MonoObject *target = gchandle.get_target(); - if (!target) + if (!target) { return refcount == 0; // Called after the managed side was collected, so nothing to do here + } // Release the current strong handle and replace it with a weak handle. MonoGCHandleData weak_gchandle = MonoGCHandleData::new_weak_handle(target); @@ -1558,7 +1557,6 @@ bool CSharpLanguage::refcount_decremented_instance_binding(Object *p_object) { } CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpScript *p_script, const MonoGCHandleData &p_gchandle) { - CSharpInstance *instance = memnew(CSharpInstance(Ref<CSharpScript>(p_script))); Reference *ref = Object::cast_to<Reference>(p_owner); @@ -1567,8 +1565,9 @@ CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpS instance->owner = p_owner; instance->gchandle = p_gchandle; - if (instance->base_ref) + if (instance->base_ref) { instance->_reference_owner_unsafe(); + } p_script->instances.insert(p_owner); @@ -1576,7 +1575,6 @@ CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpS } MonoObject *CSharpInstance::get_mono_object() const { - ERR_FAIL_COND_V(gchandle.is_released(), nullptr); return gchandle.get_target(); } @@ -1586,7 +1584,6 @@ Object *CSharpInstance::get_owner() { } bool CSharpInstance::set(const StringName &p_name, const Variant &p_value) { - ERR_FAIL_COND_V(!script.is_valid(), false); GD_MONO_SCOPE_THREAD_ATTACH; @@ -1627,8 +1624,9 @@ bool CSharpInstance::set(const StringName &p_name, const Variant &p_value) { MonoObject *ret = method->invoke(mono_object, args); - if (ret && GDMonoMarshal::unbox<MonoBoolean>(ret)) + if (ret && GDMonoMarshal::unbox<MonoBoolean>(ret)) { return true; + } break; } @@ -1640,7 +1638,6 @@ bool CSharpInstance::set(const StringName &p_name, const Variant &p_value) { } bool CSharpInstance::get(const StringName &p_name, Variant &r_ret) const { - ERR_FAIL_COND_V(!script.is_valid(), false); GD_MONO_SCOPE_THREAD_ATTACH; @@ -1704,7 +1701,6 @@ bool CSharpInstance::get(const StringName &p_name, Variant &r_ret) const { } void CSharpInstance::get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state) { - List<PropertyInfo> pinfo; get_property_list(&pinfo); @@ -1715,8 +1711,9 @@ void CSharpInstance::get_properties_state_for_reloading(List<Pair<StringName, Va ManagedType managedType; GDMonoField *field = script->script_class->get_field(state_pair.first); - if (!field) + if (!field) { continue; // Properties ignored. We get the property baking fields instead. + } managedType = field->get_type(); @@ -1729,7 +1726,6 @@ void CSharpInstance::get_properties_state_for_reloading(List<Pair<StringName, Va } void CSharpInstance::get_event_signals_state_for_reloading(List<Pair<StringName, Array>> &r_state) { - MonoObject *owner_managed = get_mono_object(); ERR_FAIL_NULL(owner_managed); @@ -1737,8 +1733,9 @@ void CSharpInstance::get_event_signals_state_for_reloading(List<Pair<StringName, const CSharpScript::EventSignal &event_signal = E->value(); MonoDelegate *delegate_field_value = (MonoDelegate *)event_signal.field->get_value(owner_managed); - if (!delegate_field_value) + if (!delegate_field_value) { continue; // Empty + } Array serialized_data; MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); @@ -1760,7 +1757,6 @@ void CSharpInstance::get_event_signals_state_for_reloading(List<Pair<StringName, } void CSharpInstance::get_property_list(List<PropertyInfo> *p_properties) const { - for (Map<StringName, PropertyInfo>::Element *E = script->member_info.front(); E; E = E->next()) { p_properties->push_back(E->value()); } @@ -1784,8 +1780,9 @@ void CSharpInstance::get_property_list(List<PropertyInfo> *p_properties) const { if (ret) { Array array = Array(GDMonoMarshal::mono_object_to_variant(ret)); - for (int i = 0, size = array.size(); i < size; i++) + for (int i = 0, size = array.size(); i < size; i++) { p_properties->push_back(PropertyInfo::from_dict(array.get(i))); + } return; } @@ -1797,23 +1794,24 @@ void CSharpInstance::get_property_list(List<PropertyInfo> *p_properties) const { } Variant::Type CSharpInstance::get_property_type(const StringName &p_name, bool *r_is_valid) const { - if (script->member_info.has(p_name)) { - if (r_is_valid) + if (r_is_valid) { *r_is_valid = true; + } return script->member_info[p_name].type; } - if (r_is_valid) + if (r_is_valid) { *r_is_valid = false; + } return Variant::NIL; } bool CSharpInstance::has_method(const StringName &p_method) const { - - if (!script.is_valid()) + if (!script.is_valid()) { return false; + } GD_MONO_SCOPE_THREAD_ATTACH; @@ -1831,7 +1829,6 @@ bool CSharpInstance::has_method(const StringName &p_method) const { } Variant CSharpInstance::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - ERR_FAIL_COND_V(!script.is_valid(), Variant()); GD_MONO_SCOPE_THREAD_ATTACH; @@ -1868,46 +1865,7 @@ Variant CSharpInstance::call(const StringName &p_method, const Variant **p_args, return Variant(); } -void CSharpInstance::call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount) { - - GD_MONO_SCOPE_THREAD_ATTACH; - - if (script.is_valid()) { - MonoObject *mono_object = get_mono_object(); - - ERR_FAIL_NULL(mono_object); - - _call_multilevel(mono_object, p_method, p_args, p_argcount); - } -} - -void CSharpInstance::_call_multilevel(MonoObject *p_mono_object, const StringName &p_method, const Variant **p_args, int p_argcount) { - - GD_MONO_ASSERT_THREAD_ATTACHED; - - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(p_method, p_argcount); - - if (method) { - method->invoke(p_mono_object, p_args); - return; - } - - top = top->get_parent_class(); - } -} - -void CSharpInstance::call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount) { - - // Sorry, the method is the one that controls the call order - - call_multilevel(p_method, p_args, p_argcount); -} - bool CSharpInstance::_reference_owner_unsafe() { - #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); CRASH_COND(owner == nullptr); @@ -1929,14 +1887,14 @@ bool CSharpInstance::_reference_owner_unsafe() { } bool CSharpInstance::_unreference_owner_unsafe() { - #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); CRASH_COND(owner == nullptr); #endif - if (!unsafe_referenced) + if (!unsafe_referenced) { return false; // Already unreferenced + } unsafe_referenced = false; @@ -1969,7 +1927,7 @@ MonoObject *CSharpInstance::_internal_new_managed() { bool die = _unreference_owner_unsafe(); // Not ok for the owner to die here. If there is a situation where this can happen, it will be considered a bug. - CRASH_COND(die == true); + CRASH_COND(die); owner = nullptr; @@ -1979,8 +1937,9 @@ MonoObject *CSharpInstance::_internal_new_managed() { // Tie managed to unmanaged gchandle = MonoGCHandleData::new_strong_handle(mono_object); - if (base_ref) + if (base_ref) { _reference_owner_unsafe(); // Here, after assigning the gchandle (for the refcount_incremented callback) + } CACHED_FIELD(GodotObject, ptr)->set_value_raw(mono_object, owner); @@ -1991,7 +1950,6 @@ MonoObject *CSharpInstance::_internal_new_managed() { } void CSharpInstance::mono_object_disposed(MonoObject *p_obj) { - disconnect_event_signals(); #ifdef DEBUG_ENABLED @@ -2002,7 +1960,6 @@ void CSharpInstance::mono_object_disposed(MonoObject *p_obj) { } void CSharpInstance::mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance) { - #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); CRASH_COND(gchandle.is_released()); @@ -2063,7 +2020,6 @@ void CSharpInstance::disconnect_event_signals() { } void CSharpInstance::refcount_incremented() { - #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); CRASH_COND(owner == nullptr); @@ -2086,7 +2042,6 @@ void CSharpInstance::refcount_incremented() { } bool CSharpInstance::refcount_decremented() { - #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); CRASH_COND(owner == nullptr); @@ -2156,7 +2111,6 @@ MultiplayerAPI::RPCMode CSharpInstance::get_rset_mode(const StringName &p_variab } void CSharpInstance::notification(int p_notification) { - GD_MONO_SCOPE_THREAD_ATTACH; if (p_notification == Object::NOTIFICATION_PREDELETE) { @@ -2195,7 +2149,6 @@ void CSharpInstance::notification(int p_notification) { } void CSharpInstance::_call_notification(int p_notification) { - GD_MONO_ASSERT_THREAD_ATTACHED; MonoObject *mono_object = get_mono_object(); @@ -2203,7 +2156,7 @@ void CSharpInstance::_call_notification(int p_notification) { // Custom version of _call_multilevel, optimized for _notification - uint32_t arg = p_notification; + int32_t arg = p_notification; void *args[1] = { &arg }; StringName method_name = CACHED_STRING_NAME(_notification); @@ -2227,8 +2180,9 @@ String CSharpInstance::to_string(bool *r_valid) { MonoObject *mono_object = get_mono_object(); if (mono_object == nullptr) { - if (r_valid) + if (r_valid) { *r_valid = false; + } return String(); } @@ -2237,14 +2191,16 @@ String CSharpInstance::to_string(bool *r_valid) { if (exc) { GDMonoUtils::set_pending_exception(exc); - if (r_valid) + if (r_valid) { *r_valid = false; + } return String(); } if (result == nullptr) { - if (r_valid) + if (r_valid) { *r_valid = false; + } return String(); } @@ -2252,12 +2208,10 @@ String CSharpInstance::to_string(bool *r_valid) { } Ref<Script> CSharpInstance::get_script() const { - return script; } ScriptLanguage *CSharpInstance::get_language() { - return CSharpLanguage::get_singleton(); } @@ -2266,7 +2220,6 @@ CSharpInstance::CSharpInstance(const Ref<CSharpScript> &p_script) : } CSharpInstance::~CSharpInstance() { - GD_MONO_SCOPE_THREAD_ATTACH; destructing_script_instance = true; @@ -2309,7 +2262,7 @@ CSharpInstance::~CSharpInstance() { // Unreference the owner here, before the new "instance binding" references it. // Otherwise, the unsafe reference debug checks will incorrectly detect a bug. bool die = _unreference_owner_unsafe(); - CRASH_COND(die == true); // `owner_keep_alive` holds a reference, so it can't die + CRASH_COND(die); // `owner_keep_alive` holds a reference, so it can't die void *data = owner->get_script_instance_binding(CSharpLanguage::get_singleton()->get_language_index()); CRASH_COND(data == nullptr); @@ -2348,14 +2301,12 @@ CSharpInstance::~CSharpInstance() { #ifdef TOOLS_ENABLED void CSharpScript::_placeholder_erased(PlaceHolderScriptInstance *p_placeholder) { - placeholders.erase(p_placeholder); } #endif #ifdef TOOLS_ENABLED void CSharpScript::_update_exports_values(Map<StringName, Variant> &values, List<PropertyInfo> &propnames) { - if (base_cache.is_valid()) { base_cache->_update_exports_values(values, propnames); } @@ -2370,7 +2321,6 @@ void CSharpScript::_update_exports_values(Map<StringName, Variant> &values, List } void CSharpScript::_update_member_info_no_exports() { - if (exports_invalidated) { GD_MONO_ASSERT_THREAD_ATTACHED; @@ -2419,60 +2369,71 @@ void CSharpScript::_update_member_info_no_exports() { #endif bool CSharpScript::_update_exports() { - #ifdef TOOLS_ENABLED - if (!Engine::get_singleton()->is_editor_hint()) - return false; - - placeholder_fallback_enabled = true; // until proven otherwise - - if (!valid) + bool is_editor = Engine::get_singleton()->is_editor_hint(); + if (is_editor) { + placeholder_fallback_enabled = true; // until proven otherwise + } +#endif + if (!valid) { return false; + } bool changed = false; - if (exports_invalidated) { +#ifdef TOOLS_ENABLED + if (exports_invalidated) +#endif + { GD_MONO_SCOPE_THREAD_ATTACH; - exports_invalidated = false; - changed = true; member_info.clear(); - exported_members_cache.clear(); - exported_members_defval_cache.clear(); - // Here we create a temporary managed instance of the class to get the initial values +#ifdef TOOLS_ENABLED + MonoObject *tmp_object = nullptr; + Object *tmp_native = nullptr; + uint32_t tmp_pinned_gchandle = 0; + + if (is_editor) { + exports_invalidated = false; - MonoObject *tmp_object = mono_object_new(mono_domain_get(), script_class->get_mono_ptr()); + exported_members_cache.clear(); + exported_members_defval_cache.clear(); - if (!tmp_object) { - ERR_PRINT("Failed to allocate temporary MonoObject."); - return false; - } + // Here we create a temporary managed instance of the class to get the initial values + tmp_object = mono_object_new(mono_domain_get(), script_class->get_mono_ptr()); - uint32_t tmp_pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(tmp_object); // pin it (not sure if needed) + if (!tmp_object) { + ERR_PRINT("Failed to allocate temporary MonoObject."); + return false; + } - GDMonoMethod *ctor = script_class->get_method(CACHED_STRING_NAME(dotctor), 0); + tmp_pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(tmp_object); // pin it (not sure if needed) - ERR_FAIL_NULL_V_MSG(ctor, false, - "Cannot construct temporary MonoObject because the class does not define a parameterless constructor: '" + get_path() + "'."); + GDMonoMethod *ctor = script_class->get_method(CACHED_STRING_NAME(dotctor), 0); - MonoException *ctor_exc = nullptr; - ctor->invoke(tmp_object, nullptr, &ctor_exc); + ERR_FAIL_NULL_V_MSG(ctor, false, + "Cannot construct temporary MonoObject because the class does not define a parameterless constructor: '" + get_path() + "'."); - Object *tmp_native = GDMonoMarshal::unbox<Object *>(CACHED_FIELD(GodotObject, ptr)->get_value(tmp_object)); + MonoException *ctor_exc = nullptr; + ctor->invoke(tmp_object, nullptr, &ctor_exc); - if (ctor_exc) { - // TODO: Should we free 'tmp_native' if the exception was thrown after its creation? + tmp_native = GDMonoMarshal::unbox<Object *>(CACHED_FIELD(GodotObject, ptr)->get_value(tmp_object)); - GDMonoUtils::free_gchandle(tmp_pinned_gchandle); - tmp_object = nullptr; + if (ctor_exc) { + // TODO: Should we free 'tmp_native' if the exception was thrown after its creation? - ERR_PRINT("Exception thrown from constructor of temporary MonoObject:"); - GDMonoUtils::debug_print_unhandled_exception(ctor_exc); - return false; + GDMonoUtils::free_gchandle(tmp_pinned_gchandle); + tmp_object = nullptr; + + ERR_PRINT("Exception thrown from constructor of temporary MonoObject:"); + GDMonoUtils::debug_print_unhandled_exception(ctor_exc); + return false; + } } +#endif GDMonoClass *top = script_class; @@ -2488,15 +2449,22 @@ bool CSharpScript::_update_exports() { if (_get_member_export(field, /* inspect export: */ true, prop_info, exported)) { StringName member_name = field->get_name(); + member_info[member_name] = prop_info; + if (exported) { - member_info[member_name] = prop_info; - exported_members_cache.push_front(prop_info); +#ifdef TOOLS_ENABLED + if (is_editor) { + exported_members_cache.push_front(prop_info); - if (tmp_object) { - exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(field->get_value(tmp_object)); + if (tmp_object) { + exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(field->get_value(tmp_object)); + } } - } else { - member_info[member_name] = prop_info; +#endif + +#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED) + exported_members_names.insert(member_name); +#endif } } } @@ -2509,22 +2477,28 @@ bool CSharpScript::_update_exports() { if (_get_member_export(property, /* inspect export: */ true, prop_info, exported)) { StringName member_name = property->get_name(); + member_info[member_name] = prop_info; + if (exported) { - member_info[member_name] = prop_info; - exported_members_cache.push_front(prop_info); - - if (tmp_object) { - MonoException *exc = nullptr; - MonoObject *ret = property->get_value(tmp_object, &exc); - if (exc) { - exported_members_defval_cache[member_name] = Variant(); - GDMonoUtils::debug_print_unhandled_exception(exc); - } else { - exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(ret); +#ifdef TOOLS_ENABLED + if (is_editor) { + exported_members_cache.push_front(prop_info); + if (tmp_object) { + MonoException *exc = nullptr; + MonoObject *ret = property->get_value(tmp_object, &exc); + if (exc) { + exported_members_defval_cache[member_name] = Variant(); + GDMonoUtils::debug_print_unhandled_exception(exc); + } else { + exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(ret); + } } } - } else { - member_info[member_name] = prop_info; +#endif + +#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED) + exported_members_names.insert(member_name); +#endif } } } @@ -2532,52 +2506,57 @@ bool CSharpScript::_update_exports() { top = top->get_parent_class(); } - // Need to check this here, before disposal - bool base_ref = Object::cast_to<Reference>(tmp_native) != nullptr; +#ifdef TOOLS_ENABLED + if (is_editor) { + // Need to check this here, before disposal + bool base_ref = Object::cast_to<Reference>(tmp_native) != nullptr; - // Dispose the temporary managed instance + // Dispose the temporary managed instance - MonoException *exc = nullptr; - GDMonoUtils::dispose(tmp_object, &exc); + MonoException *exc = nullptr; + GDMonoUtils::dispose(tmp_object, &exc); - if (exc) { - ERR_PRINT("Exception thrown from method Dispose() of temporary MonoObject:"); - GDMonoUtils::debug_print_unhandled_exception(exc); - } + if (exc) { + ERR_PRINT("Exception thrown from method Dispose() of temporary MonoObject:"); + GDMonoUtils::debug_print_unhandled_exception(exc); + } - GDMonoUtils::free_gchandle(tmp_pinned_gchandle); - tmp_object = nullptr; + GDMonoUtils::free_gchandle(tmp_pinned_gchandle); + tmp_object = nullptr; - if (tmp_native && !base_ref) { - Node *node = Object::cast_to<Node>(tmp_native); - if (node && node->is_inside_tree()) { - ERR_PRINT("Temporary instance was added to the scene tree."); - } else { - memdelete(tmp_native); + if (tmp_native && !base_ref) { + Node *node = Object::cast_to<Node>(tmp_native); + if (node && node->is_inside_tree()) { + ERR_PRINT("Temporary instance was added to the scene tree."); + } else { + memdelete(tmp_native); + } } } +#endif } - placeholder_fallback_enabled = false; +#ifdef TOOLS_ENABLED + if (is_editor) { + placeholder_fallback_enabled = false; - if (placeholders.size()) { - // Update placeholders if any - Map<StringName, Variant> values; - List<PropertyInfo> propnames; - _update_exports_values(values, propnames); + if (placeholders.size()) { + // Update placeholders if any + Map<StringName, Variant> values; + List<PropertyInfo> propnames; + _update_exports_values(values, propnames); - for (Set<PlaceHolderScriptInstance *>::Element *E = placeholders.front(); E; E = E->next()) { - E->get()->update(propnames, values); + for (Set<PlaceHolderScriptInstance *>::Element *E = placeholders.front(); E; E = E->next()) { + E->get()->update(propnames, values); + } } } +#endif return changed; -#endif - return false; } void CSharpScript::load_script_signals(GDMonoClass *p_class, GDMonoClass *p_native_class) { - // no need to load the script's signals more than once if (!signals_invalidated) { return; @@ -2595,8 +2574,9 @@ void CSharpScript::load_script_signals(GDMonoClass *p_class, GDMonoClass *p_nati for (int i = delegates.size() - 1; i >= 0; --i) { GDMonoClass *delegate = delegates[i]; - if (!delegate->has_attribute(CACHED_CLASS(SignalAttribute))) + if (!delegate->has_attribute(CACHED_CLASS(SignalAttribute))) { continue; + } // Arguments are accessibles as arguments of .Invoke method GDMonoMethod *invoke_method = delegate->get_method(mono_get_delegate_invoke(delegate->get_mono_ptr())); @@ -2629,11 +2609,13 @@ void CSharpScript::load_script_signals(GDMonoClass *p_class, GDMonoClass *p_nati GDMonoClass *field_class = field->get_type().type_class; - if (!mono_class_is_delegate(field_class->get_mono_ptr())) + if (!mono_class_is_delegate(field_class->get_mono_ptr())) { continue; + } - if (!found_event_signals.find(field->get_name())) + if (!found_event_signals.find(field->get_name())) { continue; + } GDMonoMethod *invoke_method = field_class->get_method(mono_get_delegate_invoke(field_class->get_mono_ptr())); @@ -2679,13 +2661,11 @@ bool CSharpScript::_get_signal(GDMonoClass *p_class, GDMonoMethod *p_delegate_in return true; } -#ifdef TOOLS_ENABLED /** * Returns false if there was an error, otherwise true. * If there was an error, r_prop_info and r_exported are not assigned any value. */ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect_export, PropertyInfo &r_prop_info, bool &r_exported) { - GD_MONO_ASSERT_THREAD_ATTACHED; // Goddammit, C++. All I wanted was some nested functions. @@ -2693,13 +2673,17 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect (m_member->get_enclosing_class()->get_full_name() + "." + (String)m_member->get_name()) if (p_member->is_static()) { - if (p_member->has_attribute(CACHED_CLASS(ExportAttribute))) +#ifdef TOOLS_ENABLED + if (p_member->has_attribute(CACHED_CLASS(ExportAttribute))) { ERR_PRINT("Cannot export member because it is static: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); + } +#endif return false; } - if (member_info.has(p_member->get_name())) + if (member_info.has(p_member->get_name())) { return false; + } ManagedType type; @@ -2716,13 +2700,19 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect if (p_member->get_member_type() == IMonoClassMember::MEMBER_TYPE_PROPERTY) { GDMonoProperty *property = static_cast<GDMonoProperty *>(p_member); if (!property->has_getter()) { - if (exported) +#ifdef TOOLS_ENABLED + if (exported) { ERR_PRINT("Read-only property cannot be exported: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); + } +#endif return false; } if (!property->has_setter()) { - if (exported) +#ifdef TOOLS_ENABLED + if (exported) { ERR_PRINT("Write-only property (without getter) cannot be exported: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); + } +#endif return false; } } @@ -2736,16 +2726,21 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect return true; } +#ifdef TOOLS_ENABLED MonoObject *attr = p_member->get_attribute(CACHED_CLASS(ExportAttribute)); +#endif PropertyHint hint = PROPERTY_HINT_NONE; String hint_string; if (variant_type == Variant::NIL && !nil_is_variant) { +#ifdef TOOLS_ENABLED ERR_PRINT("Unknown exported member type: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); +#endif return false; } +#ifdef TOOLS_ENABLED int hint_res = _try_get_member_export_hint(p_member, type, variant_type, /* allow_generics: */ true, hint, hint_string); ERR_FAIL_COND_V_MSG(hint_res == -1, false, @@ -2756,6 +2751,7 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect hint = PropertyHint(CACHED_FIELD(ExportAttribute, hint)->get_int_value(attr)); hint_string = CACHED_FIELD(ExportAttribute, hintString)->get_string_value(attr); } +#endif uint32_t prop_usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE; @@ -2772,8 +2768,8 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect #undef MEMBER_FULL_QUALIFIED_NAME } +#ifdef TOOLS_ENABLED int CSharpScript::_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) { - if (p_variant_type == Variant::NIL) { // System.Object (Variant) return 1; @@ -2844,8 +2840,9 @@ int CSharpScript::_try_get_member_export_hint(IMonoClassMember *p_member, Manage ManagedType elem_type; - if (!GDMonoMarshal::try_get_array_element_type(p_type, elem_type)) + if (!GDMonoMarshal::try_get_array_element_type(p_type, elem_type)) { return 0; + } Variant::Type elem_variant_type = GDMonoMarshal::managed_to_variant_type(elem_type); @@ -2872,18 +2869,7 @@ int CSharpScript::_try_get_member_export_hint(IMonoClassMember *p_member, Manage } #endif -void CSharpScript::_clear() { - - tool = false; - valid = false; - - base = nullptr; - native = nullptr; - script_class = nullptr; -} - Variant CSharpScript::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - if (unlikely(GDMono::get_singleton() == nullptr)) { // Probably not the best error but eh. r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; @@ -2915,18 +2901,11 @@ Variant CSharpScript::call(const StringName &p_method, const Variant **p_args, i } void CSharpScript::_resource_path_changed() { - - String path = get_path(); - - if (!path.empty()) { - name = get_path().get_file().get_basename(); - } + _update_name(); } bool CSharpScript::_get(const StringName &p_name, Variant &r_ret) const { - if (p_name == CSharpLanguage::singleton->string_names._script_source) { - r_ret = get_source_code(); return true; } @@ -2935,9 +2914,7 @@ bool CSharpScript::_get(const StringName &p_name, Variant &r_ret) const { } bool CSharpScript::_set(const StringName &p_name, const Variant &p_value) { - if (p_name == CSharpLanguage::singleton->string_names._script_source) { - set_source_code(p_value); reload(); return true; @@ -2947,17 +2924,14 @@ bool CSharpScript::_set(const StringName &p_name, const Variant &p_value) { } void CSharpScript::_get_property_list(List<PropertyInfo> *p_properties) const { - p_properties->push_back(PropertyInfo(Variant::STRING, CSharpLanguage::singleton->string_names._script_source, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL)); } void CSharpScript::_bind_methods() { - ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "new", &CSharpScript::_new, MethodInfo("new")); } Ref<CSharpScript> CSharpScript::create_for_managed_type(GDMonoClass *p_class, GDMonoClass *p_native) { - // This method should not fail, only assertions allowed CRASH_COND(p_class == nullptr); @@ -2971,7 +2945,6 @@ Ref<CSharpScript> CSharpScript::create_for_managed_type(GDMonoClass *p_class, GD } void CSharpScript::initialize_for_managed_type(Ref<CSharpScript> p_script, GDMonoClass *p_class, GDMonoClass *p_native) { - // This method should not fail, only assertions allowed CRASH_COND(p_class == nullptr); @@ -2984,8 +2957,9 @@ void CSharpScript::initialize_for_managed_type(Ref<CSharpScript> p_script, GDMon GDMonoClass *base = p_script->script_class->get_parent_class(); - if (base != p_script->native) + if (base != p_script->native) { p_script->base = base; + } p_script->valid = true; p_script->tool = p_script->script_class->has_attribute(CACHED_CLASS(ToolAttribute)); @@ -3011,8 +2985,9 @@ void CSharpScript::initialize_for_managed_type(Ref<CSharpScript> p_script, GDMon while (native_top) { native_top->fetch_methods_with_godot_api_checks(p_script->native); - if (native_top == CACHED_CLASS(GodotObject)) + if (native_top == CACHED_CLASS(GodotObject)) { break; + } native_top = native_top->get_parent_class(); } @@ -3035,7 +3010,6 @@ void CSharpScript::initialize_for_managed_type(Ref<CSharpScript> p_script, GDMon } bool CSharpScript::can_instance() const { - #ifdef TOOLS_ENABLED bool extra_cond = tool || ScriptServer::is_scripting_enabled(); #else @@ -3059,15 +3033,14 @@ bool CSharpScript::can_instance() const { } StringName CSharpScript::get_instance_base_type() const { - - if (native) + if (native) { return native->get_name(); - else + } else { return StringName(); + } } CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_isref, Callable::CallError &r_error) { - GD_MONO_ASSERT_THREAD_ATTACHED; /* STEP 1, CREATE */ @@ -3127,7 +3100,7 @@ CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_arg bool die = instance->_unreference_owner_unsafe(); // Not ok for the owner to die here. If there is a situation where this can happen, it will be considered a bug. - CRASH_COND(die == true); + CRASH_COND(die); p_owner->set_script_instance(nullptr); r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; @@ -3137,8 +3110,9 @@ CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_arg // Tie managed to unmanaged instance->gchandle = MonoGCHandleData::new_strong_handle(mono_object); - if (instance->base_ref) + if (instance->base_ref) { instance->_reference_owner_unsafe(); // Here, after assigning the gchandle (for the refcount_incremented callback) + } { MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); @@ -3157,7 +3131,6 @@ CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_arg } Variant CSharpScript::_new(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - if (!valid) { r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; return Variant(); @@ -3193,7 +3166,6 @@ Variant CSharpScript::_new(const Variant **p_args, int p_argcount, Callable::Cal } ScriptInstance *CSharpScript::instance_create(Object *p_this) { - #ifdef DEBUG_ENABLED CRASH_COND(!valid); #endif @@ -3218,7 +3190,6 @@ ScriptInstance *CSharpScript::instance_create(Object *p_this) { } PlaceHolderScriptInstance *CSharpScript::placeholder_instance_create(Object *p_this) { - #ifdef TOOLS_ENABLED PlaceHolderScriptInstance *si = memnew(PlaceHolderScriptInstance(CSharpLanguage::get_singleton(), Ref<Script>(this), p_this)); placeholders.insert(si); @@ -3230,25 +3201,22 @@ PlaceHolderScriptInstance *CSharpScript::placeholder_instance_create(Object *p_t } bool CSharpScript::instance_has(const Object *p_this) const { - MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); return instances.has((Object *)p_this); } bool CSharpScript::has_source_code() const { - return !source.empty(); } String CSharpScript::get_source_code() const { - return source; } void CSharpScript::set_source_code(const String &p_code) { - - if (source == p_code) + if (source == p_code) { return; + } source = p_code; #ifdef TOOLS_ENABLED source_changed_cache = true; @@ -3256,9 +3224,9 @@ void CSharpScript::set_source_code(const String &p_code) { } void CSharpScript::get_script_method_list(List<MethodInfo> *p_list) const { - - if (!script_class) + if (!script_class) { return; + } GD_MONO_SCOPE_THREAD_ATTACH; @@ -3270,9 +3238,9 @@ void CSharpScript::get_script_method_list(List<MethodInfo> *p_list) const { } bool CSharpScript::has_method(const StringName &p_method) const { - - if (!script_class) + if (!script_class) { return false; + } GD_MONO_SCOPE_THREAD_ATTACH; @@ -3280,9 +3248,9 @@ bool CSharpScript::has_method(const StringName &p_method) const { } MethodInfo CSharpScript::get_method_info(const StringName &p_method) const { - - if (!script_class) + if (!script_class) { return MethodInfo(); + } GD_MONO_SCOPE_THREAD_ATTACH; @@ -3301,7 +3269,6 @@ MethodInfo CSharpScript::get_method_info(const StringName &p_method) const { } Error CSharpScript::reload(bool p_keep_state) { - bool has_instances; { MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); @@ -3323,9 +3290,7 @@ Error CSharpScript::reload(bool p_keep_state) { ERR_FAIL_NULL_V(namespace_, ERR_BUG); ERR_FAIL_NULL_V(class_name, ERR_BUG); GDMonoClass *klass = project_assembly->get_class(namespace_->operator String(), class_name->operator String()); - if (klass) { - bool obj_type = CACHED_CLASS(GodotObject)->is_assignable_from(klass); - ERR_FAIL_COND_V(!obj_type, ERR_BUG); + if (klass && CACHED_CLASS(GodotObject)->is_assignable_from(klass)) { script_class = klass; } } else { @@ -3359,8 +3324,9 @@ Error CSharpScript::reload(bool p_keep_state) { GDMonoClass *base_class = script_class->get_parent_class(); - if (base_class != native) + if (base_class != native) { base = base_class; + } #ifdef DEBUG_ENABLED // For debug builds, we must fetch from all native base methods as well. @@ -3372,8 +3338,9 @@ Error CSharpScript::reload(bool p_keep_state) { while (native_top) { native_top->fetch_methods_with_godot_api_checks(native); - if (native_top == CACHED_CLASS(GodotObject)) + if (native_top == CACHED_CLASS(GodotObject)) { break; + } native_top = native_top->get_parent_class(); } @@ -3463,12 +3430,10 @@ Error CSharpScript::reload(bool p_keep_state) { } ScriptLanguage *CSharpScript::get_language() const { - return CSharpLanguage::get_singleton(); } bool CSharpScript::get_property_default_value(const StringName &p_property, Variant &r_value) const { - #ifdef TOOLS_ENABLED const Map<StringName, Variant>::Element *E = exported_members_defval_cache.find(p_property); @@ -3486,7 +3451,6 @@ bool CSharpScript::get_property_default_value(const StringName &p_property, Vari } void CSharpScript::update_exports() { - #ifdef TOOLS_ENABLED _update_exports(); #endif @@ -3497,7 +3461,6 @@ bool CSharpScript::has_script_signal(const StringName &p_signal) const { } void CSharpScript::get_script_signal_list(List<MethodInfo> *r_signals) const { - for (const Map<StringName, Vector<SignalParameter>>::Element *E = _signals.front(); E; E = E->next()) { MethodInfo mi; mi.name = E->key(); @@ -3507,8 +3470,9 @@ void CSharpScript::get_script_signal_list(List<MethodInfo> *r_signals) const { const SignalParameter ¶m = params[i]; PropertyInfo arg_info = PropertyInfo(param.type, param.name); - if (param.type == Variant::NIL && param.nil_is_variant) + if (param.type == Variant::NIL && param.nil_is_variant) { arg_info.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + } mi.arguments.push_back(arg_info); } @@ -3526,8 +3490,9 @@ void CSharpScript::get_script_signal_list(List<MethodInfo> *r_signals) const { const SignalParameter ¶m = params[i]; PropertyInfo arg_info = PropertyInfo(param.type, param.name); - if (param.type == Variant::NIL && param.nil_is_variant) + if (param.type == Variant::NIL && param.nil_is_variant) { arg_info.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + } mi.arguments.push_back(arg_info); } @@ -3542,45 +3507,52 @@ bool CSharpScript::inherits_script(const Ref<Script> &p_script) const { return false; } -#ifndef _MSC_VER -#warning TODO: Implement CSharpScript::inherits_script and other relevant changes after GH-38063. -#endif - return false; + if (script_class == nullptr || cs->script_class == nullptr) { + return false; + } + + if (script_class == cs->script_class) { + return true; + } + + return cs->script_class->is_assignable_from(script_class); } Ref<Script> CSharpScript::get_base_script() const { - // TODO search in metadata file once we have it, not important any way? return Ref<Script>(); } void CSharpScript::get_script_property_list(List<PropertyInfo> *p_list) const { - for (Map<StringName, PropertyInfo>::Element *E = member_info.front(); E; E = E->next()) { p_list->push_back(E->value()); } } int CSharpScript::get_member_line(const StringName &p_member) const { - // TODO omnisharp return -1; } MultiplayerAPI::RPCMode CSharpScript::_member_get_rpc_mode(IMonoClassMember *p_member) const { - - if (p_member->has_attribute(CACHED_CLASS(RemoteAttribute))) + if (p_member->has_attribute(CACHED_CLASS(RemoteAttribute))) { return MultiplayerAPI::RPC_MODE_REMOTE; - if (p_member->has_attribute(CACHED_CLASS(MasterAttribute))) + } + if (p_member->has_attribute(CACHED_CLASS(MasterAttribute))) { return MultiplayerAPI::RPC_MODE_MASTER; - if (p_member->has_attribute(CACHED_CLASS(PuppetAttribute))) + } + if (p_member->has_attribute(CACHED_CLASS(PuppetAttribute))) { return MultiplayerAPI::RPC_MODE_PUPPET; - if (p_member->has_attribute(CACHED_CLASS(RemoteSyncAttribute))) + } + if (p_member->has_attribute(CACHED_CLASS(RemoteSyncAttribute))) { return MultiplayerAPI::RPC_MODE_REMOTESYNC; - if (p_member->has_attribute(CACHED_CLASS(MasterSyncAttribute))) + } + if (p_member->has_attribute(CACHED_CLASS(MasterSyncAttribute))) { return MultiplayerAPI::RPC_MODE_MASTERSYNC; - if (p_member->has_attribute(CACHED_CLASS(PuppetSyncAttribute))) + } + if (p_member->has_attribute(CACHED_CLASS(PuppetSyncAttribute))) { return MultiplayerAPI::RPC_MODE_PUPPETSYNC; + } return MultiplayerAPI::RPC_MODE_DISABLED; } @@ -3640,7 +3612,6 @@ MultiplayerAPI::RPCMode CSharpScript::get_rset_mode(const StringName &p_variable } Error CSharpScript::load_source_code(const String &p_path) { - Error ferr = read_all_file_utf8(p_path, source); ERR_FAIL_COND_V_MSG(ferr != OK, ferr, @@ -3656,16 +3627,27 @@ Error CSharpScript::load_source_code(const String &p_path) { return OK; } -StringName CSharpScript::get_script_name() const { +void CSharpScript::_update_name() { + String path = get_path(); - return name; + if (!path.empty()) { + name = get_path().get_file().get_basename(); + } } -CSharpScript::CSharpScript() { +void CSharpScript::_clear() { + tool = false; + valid = false; + base = nullptr; + native = nullptr; + script_class = nullptr; +} + +CSharpScript::CSharpScript() { _clear(); - _resource_path_changed(); + _update_name(); #ifdef DEBUG_ENABLED { @@ -3676,19 +3658,28 @@ CSharpScript::CSharpScript() { } CSharpScript::~CSharpScript() { - #ifdef DEBUG_ENABLED MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); CSharpLanguage::get_singleton()->script_list.remove(&this->script_list); #endif } +void CSharpScript::get_members(Set<StringName> *p_members) { +#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED) + if (p_members) { + for (Set<StringName>::Element *E = exported_members_names.front(); E; E = E->next()) { + p_members->insert(E->get()); + } + } +#endif +} + /*************** RESOURCE ***************/ RES ResourceFormatLoaderCSharpScript::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, bool p_no_cache) { - - if (r_error) + if (r_error) { *r_error = ERR_FILE_CANT_OPEN; + } // TODO ignore anything inside bin/ and obj/ in tools builds? @@ -3705,29 +3696,26 @@ RES ResourceFormatLoaderCSharpScript::load(const String &p_path, const String &p script->reload(); - if (r_error) + if (r_error) { *r_error = OK; + } return scriptres; } void ResourceFormatLoaderCSharpScript::get_recognized_extensions(List<String> *p_extensions) const { - p_extensions->push_back("cs"); } bool ResourceFormatLoaderCSharpScript::handles_type(const String &p_type) const { - return p_type == "Script" || p_type == CSharpLanguage::get_singleton()->get_type(); } String ResourceFormatLoaderCSharpScript::get_resource_type(const String &p_path) const { - return p_path.get_extension().to_lower() == "cs" ? CSharpLanguage::get_singleton()->get_type() : ""; } Error ResourceFormatSaverCSharpScript::save(const String &p_path, const RES &p_resource, uint32_t p_flags) { - Ref<CSharpScript> sqscr = p_resource; ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER); @@ -3735,13 +3723,9 @@ Error ResourceFormatSaverCSharpScript::save(const String &p_path, const RES &p_r #ifdef TOOLS_ENABLED if (!FileAccess::exists(p_path)) { - // The file does not yet exists, let's assume the user just created this script - - if (_create_project_solution_if_needed()) { - CSharpProject::add_item(GodotSharpDirs::get_project_csproj_path(), - "Compile", - ProjectSettings::get_singleton()->globalize_path(p_path)); - } else { + // The file does not yet exist, let's assume the user just created this script. In such + // cases we need to check whether the solution and csproj were already created or not. + if (!_create_project_solution_if_needed()) { ERR_PRINT("C# project could not be created; cannot add file: '" + p_path + "'."); } } @@ -3771,19 +3755,16 @@ Error ResourceFormatSaverCSharpScript::save(const String &p_path, const RES &p_r } void ResourceFormatSaverCSharpScript::get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const { - if (Object::cast_to<CSharpScript>(p_resource.ptr())) { p_extensions->push_back("cs"); } } bool ResourceFormatSaverCSharpScript::recognize(const RES &p_resource) const { - return Object::cast_to<CSharpScript>(p_resource.ptr()) != nullptr; } CSharpLanguage::StringNameCache::StringNameCache() { - _signal_callback = StaticCString::create("_signal_callback"); _set = StaticCString::create("_set"); _get = StaticCString::create("_get"); diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 05e2857538..f0b43a40f9 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -66,7 +66,6 @@ TScriptInstance *cast_script_instance(ScriptInstance *p_inst) { #define CAST_CSHARP_INSTANCE(m_inst) (cast_script_instance<CSharpInstance, CSharpLanguage>(m_inst)) class CSharpScript : public Script { - GDCLASS(CSharpScript, Script); public: @@ -136,19 +135,26 @@ private: bool exports_invalidated = true; void _update_exports_values(Map<StringName, Variant> &values, List<PropertyInfo> &propnames); void _update_member_info_no_exports(); - virtual void _placeholder_erased(PlaceHolderScriptInstance *p_placeholder); + void _placeholder_erased(PlaceHolderScriptInstance *p_placeholder) override; +#endif + +#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED) + Set<StringName> exported_members_names; #endif Map<StringName, PropertyInfo> member_info; void _clear(); + void _update_name(); + void load_script_signals(GDMonoClass *p_class, GDMonoClass *p_native_class); bool _get_signal(GDMonoClass *p_class, GDMonoMethod *p_delegate_invoke, Vector<SignalParameter> ¶ms); bool _update_exports(); -#ifdef TOOLS_ENABLED + 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 @@ -165,72 +171,71 @@ private: protected: static void _bind_methods(); - Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error); - virtual void _resource_path_changed(); + Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + void _resource_path_changed() override; 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; + bool can_instance() const override; + StringName get_instance_base_type() const override; + ScriptInstance *instance_create(Object *p_this) override; + PlaceHolderScriptInstance *placeholder_instance_create(Object *p_this) override; + bool instance_has(const Object *p_this) const override; - virtual bool has_source_code() const; - virtual String get_source_code() const; - virtual void set_source_code(const String &p_code); + bool has_source_code() const override; + String get_source_code() const override; + void set_source_code(const String &p_code) override; - virtual Error reload(bool p_keep_state = false); + Error reload(bool p_keep_state = false) override; - virtual bool has_script_signal(const StringName &p_signal) const; - virtual void get_script_signal_list(List<MethodInfo> *r_signals) const; + bool has_script_signal(const StringName &p_signal) const override; + void get_script_signal_list(List<MethodInfo> *r_signals) const override; - 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(); + bool get_property_default_value(const StringName &p_property, Variant &r_value) const override; + void get_script_property_list(List<PropertyInfo> *p_list) const override; + void update_exports() override; - virtual bool is_tool() const { return tool; } - virtual bool is_valid() const { return valid; } + void get_members(Set<StringName> *p_members) override; - bool inherits_script(const Ref<Script> &p_script) const; + bool is_tool() const override { return tool; } + bool is_valid() const override { return valid; } - virtual Ref<Script> get_base_script() const; - virtual ScriptLanguage *get_language() const; + bool inherits_script(const Ref<Script> &p_script) const override; - 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; + Ref<Script> get_base_script() const override; + ScriptLanguage *get_language() const override; - virtual int get_member_line(const StringName &p_member) const; + void get_script_method_list(List<MethodInfo> *p_list) const override; + bool has_method(const StringName &p_method) const override; + MethodInfo get_method_info(const StringName &p_method) const override; - virtual Vector<ScriptNetData> get_rpc_methods() const; - virtual uint16_t get_rpc_method_id(const StringName &p_method) const; - virtual StringName get_rpc_method(const uint16_t p_rpc_method_id) const; - virtual MultiplayerAPI::RPCMode get_rpc_mode_by_id(const uint16_t p_rpc_method_id) const; - virtual MultiplayerAPI::RPCMode get_rpc_mode(const StringName &p_method) const; + int get_member_line(const StringName &p_member) const override; - virtual Vector<ScriptNetData> get_rset_properties() const; - virtual uint16_t get_rset_property_id(const StringName &p_variable) const; - virtual StringName get_rset_property(const uint16_t p_variable_id) const; - virtual MultiplayerAPI::RPCMode get_rset_mode_by_id(const uint16_t p_variable_id) const; - virtual MultiplayerAPI::RPCMode get_rset_mode(const StringName &p_variable) const; + Vector<ScriptNetData> get_rpc_methods() const override; + uint16_t get_rpc_method_id(const StringName &p_method) const override; + StringName get_rpc_method(const uint16_t p_rpc_method_id) const override; + MultiplayerAPI::RPCMode get_rpc_mode_by_id(const uint16_t p_rpc_method_id) const override; + MultiplayerAPI::RPCMode get_rpc_mode(const StringName &p_method) const override; + + Vector<ScriptNetData> get_rset_properties() const override; + uint16_t get_rset_property_id(const StringName &p_variable) const override; + StringName get_rset_property(const uint16_t p_variable_id) const override; + MultiplayerAPI::RPCMode get_rset_mode_by_id(const uint16_t p_variable_id) const override; + MultiplayerAPI::RPCMode get_rset_mode(const StringName &p_variable) const override; #ifdef TOOLS_ENABLED - virtual bool is_placeholder_fallback_enabled() const { return placeholder_fallback_enabled; } + bool is_placeholder_fallback_enabled() const override { 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; @@ -244,8 +249,6 @@ class CSharpInstance : public ScriptInstance { Ref<CSharpScript> script; MonoGCHandleData gchandle; - Vector<Callable> event_signal_callables; - bool _reference_owner_unsafe(); /* @@ -262,8 +265,6 @@ class CSharpInstance : public ScriptInstance { friend void GDMonoInternals::tie_managed_to_unmanaged(MonoObject *, Object *); static CSharpInstance *create_for_managed_type(Object *p_owner, CSharpScript *p_script, const MonoGCHandleData &p_gchandle); - void _call_multilevel(MonoObject *p_mono_object, const StringName &p_method, const Variant **p_args, int p_argcount); - void get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state); void get_event_signals_state_for_reloading(List<Pair<StringName, Array>> &r_state); @@ -272,18 +273,16 @@ public: _FORCE_INLINE_ bool is_destructing_script_instance() { return destructing_script_instance; } - virtual Object *get_owner(); + Object *get_owner() override; - 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; + bool set(const StringName &p_name, const Variant &p_value) override; + bool get(const StringName &p_name, Variant &r_ret) const override; + void get_property_list(List<PropertyInfo> *p_properties) const override; + Variant::Type get_property_type(const StringName &p_name, bool *r_is_valid) const override; - /* TODO */ 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, Callable::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); + /* TODO */ void get_method_list(List<MethodInfo> *p_list) const override {} + bool has_method(const StringName &p_method) const override; + Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; void mono_object_disposed(MonoObject *p_obj); @@ -296,46 +295,42 @@ public: void connect_event_signals(); void disconnect_event_signals(); - virtual void refcount_incremented(); - virtual bool refcount_decremented(); + void refcount_incremented() override; + bool refcount_decremented() override; - virtual Vector<ScriptNetData> get_rpc_methods() const; - virtual uint16_t get_rpc_method_id(const StringName &p_method) const; - virtual StringName get_rpc_method(const uint16_t p_rpc_method_id) const; - virtual MultiplayerAPI::RPCMode get_rpc_mode_by_id(const uint16_t p_rpc_method_id) const; - virtual MultiplayerAPI::RPCMode get_rpc_mode(const StringName &p_method) const; + Vector<ScriptNetData> get_rpc_methods() const override; + uint16_t get_rpc_method_id(const StringName &p_method) const override; + StringName get_rpc_method(const uint16_t p_rpc_method_id) const override; + MultiplayerAPI::RPCMode get_rpc_mode_by_id(const uint16_t p_rpc_method_id) const override; + MultiplayerAPI::RPCMode get_rpc_mode(const StringName &p_method) const override; - virtual Vector<ScriptNetData> get_rset_properties() const; - virtual uint16_t get_rset_property_id(const StringName &p_variable) const; - virtual StringName get_rset_property(const uint16_t p_variable_id) const; - virtual MultiplayerAPI::RPCMode get_rset_mode_by_id(const uint16_t p_variable_id) const; - virtual MultiplayerAPI::RPCMode get_rset_mode(const StringName &p_variable) const; + Vector<ScriptNetData> get_rset_properties() const override; + uint16_t get_rset_property_id(const StringName &p_variable) const override; + StringName get_rset_property(const uint16_t p_variable_id) const override; + MultiplayerAPI::RPCMode get_rset_mode_by_id(const uint16_t p_variable_id) const override; + MultiplayerAPI::RPCMode get_rset_mode(const StringName &p_variable) const override; - virtual void notification(int p_notification); + void notification(int p_notification) override; void _call_notification(int p_notification); - virtual String to_string(bool *r_valid); + String to_string(bool *r_valid) override; - virtual Ref<Script> get_script() const; + Ref<Script> get_script() const override; - virtual ScriptLanguage *get_language(); + ScriptLanguage *get_language() override; CSharpInstance(const Ref<CSharpScript> &p_script); ~CSharpInstance(); }; struct CSharpScriptBinding { - bool inited; + bool inited = false; StringName type_name; - GDMonoClass *wrapper_class; + GDMonoClass *wrapper_class = nullptr; MonoGCHandleData gchandle; - Object *owner; + Object *owner = nullptr; - CSharpScriptBinding() : - inited(false), - wrapper_class(nullptr), - owner(nullptr) { - } + CSharpScriptBinding() {} }; class ManagedCallableMiddleman : public Object { @@ -343,7 +338,6 @@ class ManagedCallableMiddleman : public Object { }; class CSharpLanguage : public ScriptLanguage { - friend class CSharpScript; friend class CSharpInstance; @@ -370,7 +364,6 @@ class CSharpLanguage : public ScriptLanguage { ManagedCallableMiddleman *managed_callable_middleman = memnew(ManagedCallableMiddleman); struct StringNameCache { - StringName _signal_callback; StringName _set; StringName _get; @@ -438,83 +431,90 @@ public: } _FORCE_INLINE_ const Dictionary &get_scripts_metadata() { - if (scripts_metadata_invalidated) + if (scripts_metadata_invalidated) { _load_scripts_metadata(); + } return scripts_metadata; } _FORCE_INLINE_ ManagedCallableMiddleman *get_managed_callable_middleman() const { return managed_callable_middleman; } - virtual String get_name() const; + String get_name() const override; /* 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(); + String get_type() const override; + String get_extension() const override; + Error execute_file(const String &p_path) override; + void init() override; + void finish() override; + + void finalize(); /* EDITOR FUNCTIONS */ - virtual void get_reserved_words(List<String> *p_words) 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 = nullptr, Set<int> *r_safe_lines = nullptr) 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 PackedStringArray &p_args) const; + void get_reserved_words(List<String> *p_words) const override; + void get_comment_delimiters(List<String> *p_delimiters) const override; + void get_string_delimiters(List<String> *p_delimiters) const override; + Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const override; + bool is_using_templates() override; + void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script) override; + /* TODO */ 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 = nullptr, Set<int> *r_safe_lines = nullptr) const override { + return true; + } + String validate_path(const String &p_path) const override; + Script *create_script() const override; + bool has_named_classes() const override; + bool supports_builtin_mode() const override; + /* TODO? */ int find_function(const String &p_function, const String &p_code) const override { return -1; } + String make_function(const String &p_class, const String &p_name, const PackedStringArray &p_args) const override; 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) {} + /* TODO? */ void auto_indent_code(String &p_code, int p_from_line, int p_to_line) const override {} + /* TODO */ void add_global_constant(const StringName &p_variable, const Variant &p_value) override {} /* 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(); + String debug_get_error() const override; + int debug_get_stack_level_count() const override; + int debug_get_stack_level_line(int p_level) const override; + String debug_get_stack_level_function(int p_level) const override; + String debug_get_stack_level_source(int p_level) const override; + /* TODO */ 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) override {} + /* TODO */ 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) override {} + /* TODO */ void debug_get_globals(List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) override {} + /* TODO */ String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems, int p_max_depth) override { return ""; } + Vector<StackInfo> debug_get_current_stack_info() override; /* 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; } + /* TODO */ void profiling_start() override {} + /* TODO */ void profiling_stop() override {} + /* TODO */ int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) override { return 0; } + /* TODO */ int profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_info_max) override { return 0; } - virtual void frame(); + void frame() override; - /* TODO? */ virtual void get_public_functions(List<MethodInfo> *p_functions) const {} - /* TODO? */ virtual void get_public_constants(List<Pair<String, Variant>> *p_constants) const {} + /* TODO? */ void get_public_functions(List<MethodInfo> *p_functions) const override {} + /* TODO? */ void get_public_constants(List<Pair<String, Variant>> *p_constants) const override {} - virtual void reload_all_scripts(); - virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload); + void reload_all_scripts() override; + void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) override; /* LOADER FUNCTIONS */ - virtual void get_recognized_extensions(List<String> *p_extensions) const; + void get_recognized_extensions(List<String> *p_extensions) const override; #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(); + Error open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col) override; + bool overrides_external_editor() override; #endif /* THREAD ATTACHING */ - virtual void thread_enter(); - virtual void thread_exit(); + void thread_enter() override; + void thread_exit() override; // 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); + void *alloc_instance_binding_data(Object *p_object) override; + void free_instance_binding_data(void *p_data) override; + void refcount_incremented_instance_binding(Object *p_object) override; + bool refcount_decremented_instance_binding(Object *p_object) override; 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); @@ -532,17 +532,17 @@ public: class ResourceFormatLoaderCSharpScript : public ResourceFormatLoader { public: - virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = nullptr, bool p_use_sub_threads = false, float *r_progress = nullptr, bool p_no_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; + RES load(const String &p_path, const String &p_original_path = "", Error *r_error = nullptr, bool p_use_sub_threads = false, float *r_progress = nullptr, bool p_no_cache = false) override; + void get_recognized_extensions(List<String> *p_extensions) const override; + bool handles_type(const String &p_type) const override; + String get_resource_type(const String &p_path) const override; }; 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; + Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0) override; + void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const override; + bool recognize(const RES &p_resource) const override; }; #endif // CSHARP_SCRIPT_H diff --git a/modules/mono/doc_classes/@C#.xml b/modules/mono/doc_classes/@C#.xml deleted file mode 100644 index 83a7fbf02c..0000000000 --- a/modules/mono/doc_classes/@C#.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="@C#" version="4.0"> - <brief_description> - </brief_description> - <description> - </description> - <tutorials> - </tutorials> - <methods> - </methods> - <constants> - </constants> -</class> diff --git a/modules/mono/doc_classes/CSharpScript.xml b/modules/mono/doc_classes/CSharpScript.xml index 1eb3404f9e..e1e9d1381f 100644 --- a/modules/mono/doc_classes/CSharpScript.xml +++ b/modules/mono/doc_classes/CSharpScript.xml @@ -1,16 +1,21 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="CSharpScript" inherits="Script" version="4.0"> <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>https://docs.godotengine.org/en/latest/getting_started/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> diff --git a/modules/mono/doc_classes/GodotSharp.xml b/modules/mono/doc_classes/GodotSharp.xml index 19a08d2036..417f8ac704 100644 --- a/modules/mono/doc_classes/GodotSharp.xml +++ b/modules/mono/doc_classes/GodotSharp.xml @@ -1,8 +1,11 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="GodotSharp" inherits="Object" version="4.0"> <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> @@ -11,26 +14,30 @@ <return type="void"> </return> <description> - Attaches the current thread to the mono runtime. + 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. + 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"> @@ -39,26 +46,28 @@ <argument index="0" name="domain_id" type="int"> </argument> <description> - Returns whether the domain is being finalized. + 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 whether the scripts domain is loaded. + Returns [code]true[/code] if the scripts domain is loaded, [code]false[/code] otherwise. </description> </method> </methods> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln new file mode 100644 index 0000000000..56c0cb7703 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj new file mode 100644 index 0000000000..86a0a4393e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -0,0 +1,35 @@ +<Project Sdk="Microsoft.Build.NoTargets/2.0.1"> + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + + <Description>MSBuild .NET Sdk for Godot projects.</Description> + <Authors>Godot Engine contributors</Authors> + + <PackageId>Godot.NET.Sdk</PackageId> + <Version>4.0.0</Version> + <PackageVersion>4.0.0-dev2</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> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec new file mode 100644 index 0000000000..5b5cefe80e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8" ?> +<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd"> + <metadata> + <id>$id$</id> + <version>$version$</version> + <description>$description$</description> + <authors>$authors$</authors> + <owners>$authors$</owners> + <projectUrl>$projecturl$</projectUrl> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <license type="expression">MIT</license> + <licenseUrl>https://licenses.nuget.org/MIT</licenseUrl> + <tags>$tags$</tags> + <packageTypes> + <packageType name="$packagetype$" /> + </packageTypes> + <repository url="$projecturl$" /> + </metadata> + <files> + <file src="Sdk\**" target="Sdk" />\ + </files> +</package> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props new file mode 100644 index 0000000000..dfc59e6ccb --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -0,0 +1,112 @@ +<Project> + <PropertyGroup> + <!-- Determines if we should import Microsoft.NET.Sdk, if it wasn't already imported. --> + <GodotSdkImportsMicrosoftNetSdk Condition=" '$(UsingMicrosoftNETSdk)' != 'true' ">true</GodotSdkImportsMicrosoftNetSdk> + + <GodotProjectTypeGuid>{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}</GodotProjectTypeGuid> + </PropertyGroup> + + <PropertyGroup> + <Configurations>Debug;ExportDebug;ExportRelease</Configurations> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + + <GodotProjectDir Condition=" '$(SolutionDir)' != '' ">$(SolutionDir)</GodotProjectDir> + <GodotProjectDir Condition=" '$(SolutionDir)' == '' ">$(MSBuildProjectDirectory)</GodotProjectDir> + <GodotProjectDir>$([MSBuild]::EnsureTrailingSlash('$(GodotProjectDir)'))</GodotProjectDir> + + <!-- Custom output paths for Godot projects. In brief, 'bin\' and 'obj\' are moved to '$(GodotProjectDir)\.mono\temp\'. --> + <BaseOutputPath>$(GodotProjectDir).mono\temp\bin\</BaseOutputPath> + <OutputPath>$(GodotProjectDir).mono\temp\bin\$(Configuration)\</OutputPath> + <!-- + Use custom IntermediateOutputPath and BaseIntermediateOutputPath only if it wasn't already set. + Otherwise the old values may have already been changed by MSBuild which can cause problems with NuGet. + --> + <IntermediateOutputPath Condition=" '$(IntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\$(Configuration)\</IntermediateOutputPath> + <BaseIntermediateOutputPath Condition=" '$(BaseIntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\</BaseIntermediateOutputPath> + + <!-- Do not append the target framework name to the output path. --> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> + </PropertyGroup> + + <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " /> + + <PropertyGroup> + <EnableDefaultNoneItems>false</EnableDefaultNoneItems> + </PropertyGroup> + + <!-- + The Microsoft.NET.Sdk only understands of the Debug and Release configurations. + We need to set the following properties manually for ExportDebug and ExportRelease. + --> + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' or '$(Configuration)' == 'ExportDebug' "> + <DebugSymbols Condition=" '$(DebugSymbols)' == '' ">true</DebugSymbols> + <Optimize Condition=" '$(Optimize)' == '' ">false</Optimize> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'ExportRelease' "> + <Optimize Condition=" '$(Optimize)' == '' ">true</Optimize> + </PropertyGroup> + + <PropertyGroup> + <GodotApiConfiguration Condition=" '$(Configuration)' != 'ExportRelease' ">Debug</GodotApiConfiguration> + <GodotApiConfiguration Condition=" '$(Configuration)' == 'ExportRelease' ">Release</GodotApiConfiguration> + </PropertyGroup> + + <!-- Auto-detect the target Godot platform if it was not specified. --> + <PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' "> + <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">linuxbsd</GodotTargetPlatform> + <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">linuxbsd</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)' == 'linuxbsd' ">GODOT_LINUXBSD;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'osx' ">GODOT_OSX;GODOT_MACOS;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'server' ">GODOT_SERVER;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'uwp' ">GODOT_UWP;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'haiku' ">GODOT_HAIKU;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'android' ">GODOT_ANDROID;GODOT_MOBILE</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'iphone' ">GODOT_IPHONE;GODOT_IOS;GODOT_MOBILE</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'javascript' ">GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB</GodotPlatformConstants> + + <GodotDefineConstants>$(GodotDefineConstants);$(GodotPlatformConstants)</GodotDefineConstants> + </PropertyGroup> + + <PropertyGroup> + <!-- ExportDebug also defines DEBUG like Debug does. --> + <DefineConstants Condition=" '$(Configuration)' == 'ExportDebug' ">$(DefineConstants);DEBUG</DefineConstants> + <!-- Debug defines TOOLS to differenciate between Debug and ExportDebug configurations. --> + <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">$(DefineConstants);TOOLS</DefineConstants> + + <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants> + </PropertyGroup> + + <ItemGroup> + <!-- + TODO: + We should consider a nuget package for reference assemblies. This is difficult because the + Godot scripting API is continuaslly breaking backwards compatibility even in patch releases. + --> + <Reference Include="GodotSharp"> + <Private>false</Private> + <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath> + </Reference> + <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' "> + <Private>false</Private> + <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath> + </Reference> + </ItemGroup> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets new file mode 100644 index 0000000000..f5afd75505 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets @@ -0,0 +1,17 @@ +<Project> + <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " /> + + <PropertyGroup> + <EnableGodotProjectTypeGuid Condition=" '$(EnableGodotProjectTypeGuid)' == '' ">true</EnableGodotProjectTypeGuid> + <ProjectTypeGuids Condition=" '$(EnableGodotProjectTypeGuid)' == 'true' ">$(GodotProjectTypeGuid);$(DefaultProjectTypeGuid)</ProjectTypeGuids> + </PropertyGroup> + + <PropertyGroup> + <!-- + Define constant to determine whether the real_t type in Godot is double precision or not. + By default this is false, like the official Godot builds. If someone is using a custom + Godot build where real_t is double, they can override the GodotRealTIsDouble property. + --> + <DefineConstants Condition=" '$(GodotRealTIsDouble)' == 'true' ">GODOT_REAL_T_IS_DOUBLE;$(DefineConstants)</DefineConstants> + </PropertyGroup> +</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index 6015cb22b6..c2549b4ad5 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Security; using Microsoft.Build.Framework; -using GodotTools.Core; namespace GodotTools.BuildLogger { @@ -183,4 +182,17 @@ namespace GodotTools.BuildLogger private StreamWriter issuesStreamWriter; private int indent; } + + internal static class StringExtensions + { + public static string CsvEscape(this string value, char delimiter = ',') + { + bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1; + + if (hasSpecialChar) + return "\"" + value.Replace("\"", "\"\"") + "\""; + + return value; + } + } } diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index 8fdd485209..0afec970c6 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -1,60 +1,10 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid> - <OutputType>Library</OutputType> - <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>GodotTools.BuildLogger</RootNamespace> - <AssemblyName>GodotTools.BuildLogger</AssemblyName> - <TargetFrameworkVersion>v4.7</TargetFrameworkVersion> - <FileAlignment>512</FileAlignment> - <LangVersion>7</LangVersion> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>7.2</LangVersion> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <PlatformTarget>AnyCPU</PlatformTarget> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <PlatformTarget>AnyCPU</PlatformTarget> - <DebugType>portable</DebugType> - <Optimize>true</Optimize> - <OutputPath>bin\Release\</OutputPath> - <DefineConstants>TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <ItemGroup> - <Reference Include="Microsoft.Build.Framework" /> - <Reference Include="System" /> - <Reference Include="System.Core" /> - <Reference Include="System.Data" /> - <Reference Include="System.Xml" /> - </ItemGroup> - <ItemGroup> - <Compile Include="GodotBuildLogger.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - </ItemGroup> <ItemGroup> - <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj"> - <Project>{639e48bd-44e5-4091-8edd-22d36dc0768d}</Project> - <Name>GodotTools.Core</Name> - </ProjectReference> + <PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" /> </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <!-- To modify your build process, add your task inside one of the targets below and uncomment it. - Other similar extension points exist, see Microsoft.Common.targets. - <Target Name="BeforeBuild"> - </Target> - <Target Name="AfterBuild"> - </Target> - --> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs deleted file mode 100644 index 4374f21cfa..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GodotTools.BuildLogger")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Godot Engine contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6CE9A984-37B1-4F8A-8FE9-609F05F071B3")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs index 85760a3705..e1ccf0454a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs @@ -19,7 +19,10 @@ namespace GodotTools.Core } if (attempt > maxAttempts + 1) - return; + { + // Overwrite the oldest one + backupPath = backupPathBase; + } File.Copy(filePath, backupPath, overwrite: true); } diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index c9ea7d3a2c..d6d8962f90 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -1,40 +1,7 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid> - <OutputType>Library</OutputType> - <RootNamespace>GodotTools.Core</RootNamespace> - <AssemblyName>GodotTools.Core</AssemblyName> - <TargetFrameworkVersion>v4.7</TargetFrameworkVersion> - <LangVersion>7</LangVersion> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>7.2</LangVersion> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>full</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug</OutputPath> - <DefineConstants>DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <Optimize>true</Optimize> - <OutputPath>bin\Release</OutputPath> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <ItemGroup> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> - <Compile Include="FileUtils.cs" /> - <Compile Include="ProcessExtensions.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="StringExtensions.cs" /> - </ItemGroup> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index 699ae6e741..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotTools.Core")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Godot Engine contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 326c49f096..012b69032e 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -25,31 +25,22 @@ namespace GodotTools.Core 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); + string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim(); return rooted ? Path.DirectorySeparatorChar + path : path; } - private static readonly string driveRoot = Path.GetPathRoot(Environment.CurrentDirectory); + private static readonly string DriveRoot = Path.GetPathRoot(Environment.CurrentDirectory); public static bool IsAbsolutePath(this string path) { return path.StartsWith("/", StringComparison.Ordinal) || path.StartsWith("\\", StringComparison.Ordinal) || - path.StartsWith(driveRoot, StringComparison.Ordinal); - } - - public static string CsvEscape(this string value, char delimiter = ',') - { - bool hasSpecialChar = value.IndexOfAny(new char[] { '\"', '\n', '\r', delimiter }) != -1; - - if (hasSpecialChar) - return "\"" + value.Replace("\"", "\"\"") + "\""; - - return value; + path.StartsWith(DriveRoot, StringComparison.Ordinal); } public static string ToSafeDirName(this string dirName, bool allowDirSeparator) diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs deleted file mode 100644 index 7a2ff2ca56..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace GodotTools.IdeConnection -{ - public class ConsoleLogger : ILogger - { - public void LogDebug(string message) - { - Console.WriteLine("DEBUG: " + message); - } - - public void LogInfo(string message) - { - Console.WriteLine("INFO: " + message); - } - - public void LogWarning(string message) - { - Console.WriteLine("WARN: " + message); - } - - public void LogError(string message) - { - Console.WriteLine("ERROR: " + message); - } - - public void LogError(string message, Exception e) - { - Console.WriteLine("EXCEPTION: " + message); - Console.WriteLine(e); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs deleted file mode 100644 index be89638241..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using Path = System.IO.Path; - -namespace GodotTools.IdeConnection -{ - public class GodotIdeBase : IDisposable - { - private ILogger logger; - - public ILogger Logger - { - get => logger ?? (logger = new ConsoleLogger()); - set => logger = value; - } - - private readonly string projectMetadataDir; - - protected const string MetaFileName = "ide_server_meta.txt"; - protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName); - - private GodotIdeConnection connection; - protected readonly object ConnectionLock = new object(); - - public bool IsDisposed { get; private set; } = false; - - public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected; - - public event Action Connected - { - add - { - if (connection != null && !connection.IsDisposed) - connection.Connected += value; - } - remove - { - if (connection != null && !connection.IsDisposed) - connection.Connected -= value; - } - } - - protected GodotIdeConnection Connection - { - get => connection; - set - { - connection?.Dispose(); - connection = value; - } - } - - protected GodotIdeBase(string projectMetadataDir) - { - this.projectMetadataDir = projectMetadataDir; - } - - protected void DisposeConnection() - { - lock (ConnectionLock) - { - connection?.Dispose(); - } - } - - ~GodotIdeBase() - { - Dispose(disposing: false); - } - - public void Dispose() - { - if (IsDisposed) - return; - - lock (ConnectionLock) - { - if (IsDisposed) // lock may not be fair - return; - IsDisposed = true; - } - - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - connection?.Dispose(); - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs deleted file mode 100644 index 2bf3b83c75..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Threading; - -namespace GodotTools.IdeConnection -{ - public abstract class GodotIdeClient : GodotIdeBase - { - protected GodotIdeMetadata GodotIdeMetadata; - - private readonly FileSystemWatcher fsWatcher; - - protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir) - { - messageHandlers = InitializeMessageHandlers(); - - // FileSystemWatcher requires an existing directory - if (!File.Exists(projectMetadataDir)) - Directory.CreateDirectory(projectMetadataDir); - - fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName); - } - - private void OnMetaFileChanged(object sender, FileSystemEventArgs e) - { - if (IsDisposed) - return; - - lock (ConnectionLock) - { - if (IsDisposed) - return; - - if (!File.Exists(MetaFilePath)) - return; - - var metadata = ReadMetadataFile(); - - if (metadata != null && metadata != GodotIdeMetadata) - { - GodotIdeMetadata = metadata.Value; - ConnectToServer(); - } - } - } - - private void OnMetaFileDeleted(object sender, FileSystemEventArgs e) - { - if (IsDisposed) - return; - - if (IsConnected) - DisposeConnection(); - - // The file may have been re-created - - lock (ConnectionLock) - { - if (IsDisposed) - return; - - if (IsConnected || !File.Exists(MetaFilePath)) - return; - - var metadata = ReadMetadataFile(); - - if (metadata != null) - { - GodotIdeMetadata = metadata.Value; - ConnectToServer(); - } - } - } - - private GodotIdeMetadata? ReadMetadataFile() - { - using (var reader = File.OpenText(MetaFilePath)) - { - string portStr = reader.ReadLine(); - - if (portStr == null) - return null; - - string editorExecutablePath = reader.ReadLine(); - - if (editorExecutablePath == null) - return null; - - if (!int.TryParse(portStr, out int port)) - return null; - - return new GodotIdeMetadata(port, editorExecutablePath); - } - } - - private void ConnectToServer() - { - var tcpClient = new TcpClient(); - - Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage); - Connection.Logger = Logger; - - try - { - Logger.LogInfo("Connecting to Godot Ide Server"); - - tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port); - - Logger.LogInfo("Connection open with Godot Ide Server"); - - var clientThread = new Thread(Connection.Start) - { - IsBackground = true, - Name = "Godot Ide Connection Client" - }; - clientThread.Start(); - } - catch (SocketException e) - { - if (e.SocketErrorCode == SocketError.ConnectionRefused) - Logger.LogError("The connection to the Godot Ide Server was refused"); - else - throw; - } - } - - public void Start() - { - Logger.LogInfo("Starting Godot Ide Client"); - - fsWatcher.Changed += OnMetaFileChanged; - fsWatcher.Deleted += OnMetaFileDeleted; - fsWatcher.EnableRaisingEvents = true; - - lock (ConnectionLock) - { - if (IsDisposed) - return; - - if (!File.Exists(MetaFilePath)) - { - Logger.LogInfo("There is no Godot Ide Server running"); - return; - } - - var metadata = ReadMetadataFile(); - - if (metadata != null) - { - GodotIdeMetadata = metadata.Value; - ConnectToServer(); - } - else - { - Logger.LogError("Failed to read Godot Ide metadata file"); - } - } - } - - public bool WriteMessage(Message message) - { - return Connection.WriteMessage(message); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - fsWatcher?.Dispose(); - } - } - - protected virtual bool HandleMessage(Message message) - { - if (messageHandlers.TryGetValue(message.Id, out var action)) - { - action(message.Arguments); - return true; - } - - return false; - } - - private readonly Dictionary<string, Action<string[]>> messageHandlers; - - private Dictionary<string, Action<string[]>> InitializeMessageHandlers() - { - return new Dictionary<string, Action<string[]>> - { - ["OpenFile"] = args => - { - switch (args.Length) - { - case 1: - OpenFile(file: args[0]); - return; - case 2: - OpenFile(file: args[0], line: int.Parse(args[1])); - return; - case 3: - OpenFile(file: args[0], line: int.Parse(args[1]), column: int.Parse(args[2])); - return; - default: - throw new ArgumentException(); - } - } - }; - } - - protected abstract void OpenFile(string file); - protected abstract void OpenFile(string file, int line); - protected abstract void OpenFile(string file, int line, int column); - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs deleted file mode 100644 index 6441be8d6e..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Net.Sockets; -using System.Text; - -namespace GodotTools.IdeConnection -{ - public abstract class GodotIdeConnection : IDisposable - { - protected const string Version = "1.0"; - - protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}"; - protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}"; - - private const int ClientWriteTimeout = 8000; - private readonly TcpClient tcpClient; - - private TextReader clientReader; - private TextWriter clientWriter; - - private readonly object writeLock = new object(); - - private readonly Func<Message, bool> messageHandler; - - public event Action Connected; - - private ILogger logger; - - public ILogger Logger - { - get => logger ?? (logger = new ConsoleLogger()); - set => logger = value; - } - - public bool IsDisposed { get; private set; } = false; - - public bool IsConnected => tcpClient.Client != null && tcpClient.Client.Connected; - - protected GodotIdeConnection(TcpClient tcpClient, Func<Message, bool> messageHandler) - { - this.tcpClient = tcpClient; - this.messageHandler = messageHandler; - } - - public void Start() - { - try - { - if (!StartConnection()) - return; - - string messageLine; - while ((messageLine = ReadLine()) != null) - { - if (!MessageParser.TryParse(messageLine, out Message msg)) - { - Logger.LogError($"Received message with invalid format: {messageLine}"); - continue; - } - - Logger.LogDebug($"Received message: {msg}"); - - if (msg.Id == "close") - { - Logger.LogInfo("Closing connection"); - return; - } - - try - { - try - { - Debug.Assert(messageHandler != null); - - if (!messageHandler(msg)) - Logger.LogError($"Received unknown message: {msg}"); - } - catch (Exception e) - { - Logger.LogError($"Message handler for '{msg}' failed with exception", e); - } - } - catch (Exception e) - { - Logger.LogError($"Exception thrown from message handler. Message: {msg}", e); - } - } - } - catch (Exception e) - { - Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e); - } - finally - { - Dispose(); - } - } - - private bool StartConnection() - { - NetworkStream clientStream = tcpClient.GetStream(); - - clientReader = new StreamReader(clientStream, Encoding.UTF8); - - lock (writeLock) - clientWriter = new StreamWriter(clientStream, Encoding.UTF8); - - clientStream.WriteTimeout = ClientWriteTimeout; - - if (!WriteHandshake()) - { - Logger.LogError("Could not write handshake"); - return false; - } - - if (!IsValidResponseHandshake(ReadLine())) - { - Logger.LogError("Received invalid handshake"); - return false; - } - - Connected?.Invoke(); - - Logger.LogInfo("Godot Ide connection started"); - - return true; - } - - private string ReadLine() - { - try - { - return clientReader?.ReadLine(); - } - catch (Exception e) - { - if (IsDisposed) - { - var se = e as SocketException ?? e.InnerException as SocketException; - if (se != null && se.SocketErrorCode == SocketError.Interrupted) - return null; - } - - throw; - } - } - - public bool WriteMessage(Message message) - { - Logger.LogDebug($"Sending message {message}"); - - var messageComposer = new MessageComposer(); - - messageComposer.AddArgument(message.Id); - foreach (string argument in message.Arguments) - messageComposer.AddArgument(argument); - - return WriteLine(messageComposer.ToString()); - } - - protected bool WriteLine(string text) - { - if (clientWriter == null || IsDisposed || !IsConnected) - return false; - - lock (writeLock) - { - try - { - clientWriter.WriteLine(text); - clientWriter.Flush(); - } - catch (Exception e) - { - if (!IsDisposed) - { - var se = e as SocketException ?? e.InnerException as SocketException; - if (se != null && se.SocketErrorCode == SocketError.Shutdown) - Logger.LogInfo("Client disconnected ungracefully"); - else - Logger.LogError("Exception thrown when trying to write to client", e); - - Dispose(); - } - } - } - - return true; - } - - protected abstract bool WriteHandshake(); - protected abstract bool IsValidResponseHandshake(string handshakeLine); - - public void Dispose() - { - if (IsDisposed) - return; - - IsDisposed = true; - - clientReader?.Dispose(); - clientWriter?.Dispose(); - ((IDisposable)tcpClient)?.Dispose(); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs deleted file mode 100644 index 1b11a14358..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace GodotTools.IdeConnection -{ - public class GodotIdeConnectionClient : GodotIdeConnection - { - public GodotIdeConnectionClient(TcpClient tcpClient, Func<Message, bool> messageHandler) - : base(tcpClient, messageHandler) - { - } - - protected override bool WriteHandshake() - { - return WriteLine(ClientHandshake); - } - - protected override bool IsValidResponseHandshake(string handshakeLine) - { - return handshakeLine == ServerHandshake; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs deleted file mode 100644 index aa98dc7ca3..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace GodotTools.IdeConnection -{ - public class GodotIdeConnectionServer : GodotIdeConnection - { - public GodotIdeConnectionServer(TcpClient tcpClient, Func<Message, bool> messageHandler) - : base(tcpClient, messageHandler) - { - } - - protected override bool WriteHandshake() - { - return WriteLine(ServerHandshake); - } - - protected override bool IsValidResponseHandshake(string handshakeLine) - { - return handshakeLine == ClientHandshake; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj deleted file mode 100644 index 8454535fba..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj +++ /dev/null @@ -1,53 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> - <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid> - <OutputType>Library</OutputType> - <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>GodotTools.IdeConnection</RootNamespace> - <AssemblyName>GodotTools.IdeConnection</AssemblyName> - <TargetFrameworkVersion>v4.7</TargetFrameworkVersion> - <FileAlignment>512</FileAlignment> - <LangVersion>7</LangVersion> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <PlatformTarget>AnyCPU</PlatformTarget> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <PlatformTarget>AnyCPU</PlatformTarget> - <DebugType>portable</DebugType> - <Optimize>true</Optimize> - <OutputPath>bin\Release\</OutputPath> - <DefineConstants>TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <ItemGroup> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> - <Compile Include="ConsoleLogger.cs" /> - <Compile Include="GodotIdeMetadata.cs" /> - <Compile Include="GodotIdeBase.cs" /> - <Compile Include="GodotIdeClient.cs" /> - <Compile Include="GodotIdeConnection.cs" /> - <Compile Include="GodotIdeConnectionClient.cs" /> - <Compile Include="GodotIdeConnectionServer.cs" /> - <Compile Include="ILogger.cs" /> - <Compile Include="Message.cs" /> - <Compile Include="MessageComposer.cs" /> - <Compile Include="MessageParser.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> -</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs deleted file mode 100644 index f24d324ae3..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Linq; - -namespace GodotTools.IdeConnection -{ - public struct Message - { - public string Id { get; set; } - public string[] Arguments { get; set; } - - public Message(string id, params string[] arguments) - { - Id = id; - Arguments = arguments; - } - - public override string ToString() - { - return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')"; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs deleted file mode 100644 index 30ffe7a06e..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq; -using System.Text; - -namespace GodotTools.IdeConnection -{ - public class MessageComposer - { - private readonly StringBuilder stringBuilder = new StringBuilder(); - - private static readonly char[] CharsToEscape = { '\\', '"' }; - - public void AddArgument(string argument) - { - AddArgument(argument, quoted: argument.Contains(",")); - } - - public void AddArgument(string argument, bool quoted) - { - if (stringBuilder.Length > 0) - stringBuilder.Append(','); - - if (quoted) - { - stringBuilder.Append('"'); - - foreach (char @char in argument) - { - if (CharsToEscape.Contains(@char)) - stringBuilder.Append('\\'); - stringBuilder.Append(@char); - } - - stringBuilder.Append('"'); - } - else - { - stringBuilder.Append(argument); - } - } - - public override string ToString() - { - return stringBuilder.ToString(); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs deleted file mode 100644 index 4365d69989..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace GodotTools.IdeConnection -{ - public static class MessageParser - { - public static bool TryParse(string messageLine, out Message message) - { - var arguments = new List<string>(); - var stringBuilder = new StringBuilder(); - - bool expectingArgument = true; - - for (int i = 0; i < messageLine.Length; i++) - { - char @char = messageLine[i]; - - if (@char == ',') - { - if (expectingArgument) - arguments.Add(string.Empty); - - expectingArgument = true; - continue; - } - - bool quoted = false; - - if (messageLine[i] == '"') - { - quoted = true; - i++; - } - - while (i < messageLine.Length) - { - @char = messageLine[i]; - - if (quoted && @char == '"') - { - i++; - break; - } - - if (@char == '\\') - { - i++; - if (i < messageLine.Length) - break; - - stringBuilder.Append(messageLine[i]); - } - else if (!quoted && @char == ',') - { - break; // We don't increment the counter to allow the colon to be parsed after this - } - else - { - stringBuilder.Append(@char); - } - - i++; - } - - arguments.Add(stringBuilder.ToString()); - stringBuilder.Clear(); - - expectingArgument = false; - } - - if (arguments.Count == 0) - { - message = new Message(); - return false; - } - - message = new Message - { - Id = arguments[0], - Arguments = arguments.Skip(1).ToArray() - }; - - return true; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs deleted file mode 100644 index 0806d02ca0..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GodotTools.IdeConnection")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Godot Engine contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("92600954-25F0-4291-8E11-1FEE9FC4BE20")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs new file mode 100644 index 0000000000..3cb6a6687e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs @@ -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} ======="); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj new file mode 100644 index 0000000000..ae78da27bc --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj @@ -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="12.0.3" /> + </ItemGroup> +</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs new file mode 100644 index 0000000000..4db71500da --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs @@ -0,0 +1,218 @@ +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); + } + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs new file mode 100644 index 0000000000..572c541412 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs @@ -0,0 +1,351 @@ +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 DateTime? metaFileModifiedTime; + private GodotIdeMetadata godotIdeMetadata; + private readonly FileSystemWatcher fsWatcher; + + public string GodotEditorExecutablePath => godotIdeMetadata.EditorExecutablePath; + + 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 (!Directory.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 lastWriteTime = File.GetLastWriteTime(MetaFilePath); + + if (lastWriteTime == metaFileModifiedTime) + return; + + metaFileModifiedTime = lastWriteTime; + + 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 lastWriteTime = File.GetLastWriteTime(MetaFilePath); + + if (lastWriteTime == metaFileModifiedTime) + return; + + metaFileModifiedTime = lastWriteTime; + + var metadata = ReadMetadataFile(); + + if (metadata != null) + { + godotIdeMetadata = metadata.Value; + _ = Task.Run(ConnectToServer); + } + } + } + + private GodotIdeMetadata? ReadMetadataFile() + { + using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = new StreamReader(fileStream)) + { + 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.Created += OnMetaFileChanged; + 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); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs new file mode 100644 index 0000000000..43041be7be --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs @@ -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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs new file mode 100644 index 0000000000..64bcfd824c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs @@ -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); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs index d16daba0e2..686202e81e 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs @@ -1,10 +1,12 @@ -namespace GodotTools.IdeConnection +namespace GodotTools.IdeMessaging { - public struct GodotIdeMetadata + 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; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj new file mode 100644 index 0000000000..dad6b9ae7a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj @@ -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.1</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="12.0.3" /> + </ItemGroup> +</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs new file mode 100644 index 0000000000..6387145a28 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs @@ -0,0 +1,8 @@ +namespace GodotTools.IdeMessaging +{ + public interface IHandshake + { + string GetHandshakeLine(string identity); + bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs index 614bb30271..d2855f93a1 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs @@ -1,6 +1,6 @@ using System; -namespace GodotTools.IdeConnection +namespace GodotTools.IdeMessaging { public interface ILogger { diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs new file mode 100644 index 0000000000..9622fcc96d --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs @@ -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); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs new file mode 100644 index 0000000000..6903ec197b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs @@ -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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs new file mode 100644 index 0000000000..a00575a2a1 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs @@ -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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs new file mode 100644 index 0000000000..10d7e1898e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs @@ -0,0 +1,298 @@ +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 + { + 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) + { + if (!IsDisposed || !(e is SocketException || e.InnerException is SocketException)) + { + 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(); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs new file mode 100644 index 0000000000..e93db9377b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs @@ -0,0 +1,129 @@ +// 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 StopPlayRequest : Request + { + public new const string Id = "StopPlay"; + + public StopPlayRequest() : base(Id) + { + } + } + + public sealed class StopPlayResponse : 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 + { + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs new file mode 100644 index 0000000000..548e7f06ee --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs @@ -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}); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs index 700b786752..d84a63c83c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs @@ -1,9 +1,9 @@ using System; using System.Runtime.CompilerServices; -namespace GodotTools.Utils +namespace GodotTools.IdeMessaging.Utils { - public sealed class NotifyAwaiter<T> : INotifyCompletion + public class NotifyAwaiter<T> : INotifyCompletion { private Action continuation; private Exception exception; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs new file mode 100644 index 0000000000..9d593fbf8a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs @@ -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(); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj new file mode 100644 index 0000000000..5b3ed0b1b7 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -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> diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs new file mode 100644 index 0000000000..ce2b378623 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -0,0 +1,270 @@ +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 + + var visualStudioDteType = Type.GetTypeFromProgID("VisualStudio.DTE.16.0", throwOnError: true); + dte = (DTE)Activator.CreateInstance(visualStudioDteType); + + 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 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.16.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 + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index b60e501beb..9cb50014b0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -1,57 +1,23 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid> - <OutputType>Library</OutputType> - <RootNamespace>GodotTools.ProjectEditor</RootNamespace> - <AssemblyName>GodotTools.ProjectEditor</AssemblyName> - <TargetFrameworkVersion>v4.7</TargetFrameworkVersion> - <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath> - <LangVersion>7</LangVersion> + <TargetFramework>net472</TargetFramework> + <LangVersion>7.2</LangVersion> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug</OutputPath> - <DefineConstants>DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <Optimize>true</Optimize> - <OutputPath>bin\Release</OutputPath> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <ItemGroup> - <Reference Include="System" /> - <Reference Include="Microsoft.Build" /> - <Reference Include="DotNet.Glob, Version=2.1.1.0, Culture=neutral, PublicKeyToken=b68cc888b4f632d1, processorArchitecture=MSIL"> - <HintPath>$(SolutionDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll</HintPath> - </Reference> - </ItemGroup> <ItemGroup> - <Compile Include="ApiAssembliesInfo.cs" /> - <Compile Include="DotNetSolution.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="IdentifierUtils.cs" /> - <Compile Include="ProjectExtensions.cs" /> - <Compile Include="ProjectGenerator.cs" /> - <Compile Include="ProjectUtils.cs" /> + <PackageReference Include="Microsoft.Build" Version="16.5.0" /> + <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> </ItemGroup> <ItemGroup> - <None Include="packages.config" /> + <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj"> - <Project>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</Project> - <Name>GodotTools.Core</Name> - </ProjectReference> + <!-- + The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described + here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 + We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when + searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. + --> + <None Include="MSBuild.exe" CopyToOutputDirectory="Always" /> </ItemGroup> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs index f93eb9a1fa..ed77076df3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs @@ -22,6 +22,37 @@ namespace GodotTools.ProjectEditor 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)) @@ -44,30 +75,7 @@ namespace GodotTools.ProjectEditor startIndex += 1; } - for (int i = startIndex; i < identifier.Length; i++) - { - char @char = identifier[i]; - - switch (Char.GetUnicodeCategory(@char)) - { - case UnicodeCategory.UppercaseLetter: - case UnicodeCategory.LowercaseLetter: - case UnicodeCategory.TitlecaseLetter: - case UnicodeCategory.ModifierLetter: - case UnicodeCategory.LetterNumber: - case UnicodeCategory.OtherLetter: - identifierBuilder.Append(@char); - break; - case UnicodeCategory.NonSpacingMark: - case UnicodeCategory.SpacingCombiningMark: - case UnicodeCategory.ConnectorPunctuation: - case UnicodeCategory.DecimalDigitNumber: - // Identifiers may start with underscore - if (identifierBuilder.Length > startIndex || @char == '_') - identifierBuilder.Append(@char); - break; - } - } + SkipInvalidCharacters(identifier, startIndex, identifierBuilder); if (identifierBuilder.Length == startIndex) { diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs deleted file mode 100644 index f0e0d1b33d..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -using GodotTools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using DotNet.Globbing; -using Microsoft.Build.Construction; - -namespace GodotTools.ProjectEditor -{ - public static class ProjectExtensions - { - public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}}; - - string normalizedInclude = include.NormalizePath(); - - 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 = Glob.Parse(item.Include.NormalizePath(), globOptions); - - if (glob.IsMatch(normalizedInclude)) - return item; - } - } - - return null; - } - public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}}; - - string normalizedInclude = Path.GetFullPath(include).NormalizePath(); - - 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 = Glob.Parse(Path.GetFullPath(item.Include).NormalizePath(), globOptions); - - 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; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index cbe3afaedd..5541876f9e 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,162 +1,49 @@ -using GodotTools.Core; using System; -using System.Collections.Generic; using System.IO; -using System.Reflection; using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; namespace GodotTools.ProjectEditor { public static class ProjectGenerator { - private const string CoreApiProjectName = "GodotSharp"; - private const string EditorApiProjectName = "GodotSharpEditor"; + public const string GodotSdkVersionToUse = "4.0.0-dev2"; - public static string GenGameProject(string dir, string name, IEnumerable<string> compileItems) - { - string path = Path.Combine(dir, name + ".csproj"); - - ProjectPropertyGroupElement mainGroup; - var root = CreateLibraryProject(name, "Debug", out mainGroup); - - mainGroup.SetProperty("OutputPath", Path.Combine(".mono", "temp", "bin", "$(Configuration)")); - mainGroup.SetProperty("BaseIntermediateOutputPath", Path.Combine(".mono", "temp", "obj")); - mainGroup.SetProperty("IntermediateOutputPath", Path.Combine("$(BaseIntermediateOutputPath)", "$(Configuration)")); - mainGroup.SetProperty("ApiConfiguration", "Debug").Condition = " '$(Configuration)' != 'ExportRelease' "; - mainGroup.SetProperty("ApiConfiguration", "Release").Condition = " '$(Configuration)' == 'ExportRelease' "; - - var debugGroup = root.AddPropertyGroup(); - debugGroup.Condition = " '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "; - debugGroup.AddProperty("DebugSymbols", "true"); - debugGroup.AddProperty("DebugType", "portable"); - debugGroup.AddProperty("Optimize", "false"); - debugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;TOOLS;"); - debugGroup.AddProperty("ErrorReport", "prompt"); - debugGroup.AddProperty("WarningLevel", "4"); - debugGroup.AddProperty("ConsolePause", "false"); - - var coreApiRef = root.AddItem("Reference", CoreApiProjectName); - coreApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", CoreApiProjectName + ".dll")); - coreApiRef.AddMetadata("Private", "False"); - - var editorApiRef = root.AddItem("Reference", EditorApiProjectName); - editorApiRef.Condition = " '$(Configuration)' == 'Debug' "; - editorApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", EditorApiProjectName + ".dll")); - editorApiRef.AddMetadata("Private", "False"); - - GenAssemblyInfoFile(root, dir, name); + public static string GodotSdkAttrValue => $"Godot.NET.Sdk/{GodotSdkVersionToUse}"; - foreach (var item in compileItems) - { - root.AddItem("Compile", item.RelativeToPath(dir).Replace("/", "\\")); - } - - root.Save(path); - - return root.GetGuid().ToString().ToUpper(); - } - - private static void GenAssemblyInfoFile(ProjectRootElement root, string dir, string name, string[] assemblyLines = null, string[] usingDirectives = null) + public static ProjectRootElement GenGameProject(string name) { - string propertiesDir = Path.Combine(dir, "Properties"); - if (!Directory.Exists(propertiesDir)) - Directory.CreateDirectory(propertiesDir); - - string usingDirectivesText = string.Empty; - - if (usingDirectives != null) - { - foreach (var usingDirective in usingDirectives) - usingDirectivesText += "\nusing " + usingDirective + ";"; - } + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - string assemblyLinesText = string.Empty; + var root = ProjectRootElement.Create(NewProjectFileOptions.None); - if (assemblyLines != null) - assemblyLinesText += string.Join("\n", assemblyLines) + "\n"; + root.Sdk = GodotSdkAttrValue; - string content = string.Format(AssemblyInfoTemplate, usingDirectivesText, name, assemblyLinesText); + var mainGroup = root.AddPropertyGroup(); + mainGroup.AddProperty("TargetFramework", "netstandard2.1"); - string assemblyInfoFile = Path.Combine(propertiesDir, "AssemblyInfo.cs"); + string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); - File.WriteAllText(assemblyInfoFile, content); + // If the name is not a valid namespace, manually set RootNamespace to a sanitized one. + if (sanitizedName != name) + mainGroup.AddProperty("RootNamespace", sanitizedName); - root.AddItem("Compile", assemblyInfoFile.RelativeToPath(dir).Replace("/", "\\")); + return root; } - public static ProjectRootElement CreateLibraryProject(string name, string defaultConfig, out ProjectPropertyGroupElement mainGroup) + public static string GenAndSaveGameProject(string dir, string name) { - if (string.IsNullOrEmpty(name)) - throw new ArgumentException($"{nameof(name)} cannot be empty", nameof(name)); - - var root = ProjectRootElement.Create(); - root.DefaultTargets = "Build"; - - mainGroup = root.AddPropertyGroup(); - mainGroup.AddProperty("Configuration", defaultConfig).Condition = " '$(Configuration)' == '' "; - mainGroup.AddProperty("Platform", "AnyCPU").Condition = " '$(Platform)' == '' "; - mainGroup.AddProperty("ProjectGuid", "{" + Guid.NewGuid().ToString().ToUpper() + "}"); - mainGroup.AddProperty("OutputType", "Library"); - mainGroup.AddProperty("OutputPath", Path.Combine("bin", "$(Configuration)")); - mainGroup.AddProperty("RootNamespace", IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true)); - mainGroup.AddProperty("AssemblyName", name); - mainGroup.AddProperty("TargetFrameworkVersion", "v4.7"); - mainGroup.AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); - - var exportDebugGroup = root.AddPropertyGroup(); - exportDebugGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportDebug|AnyCPU' "; - exportDebugGroup.AddProperty("DebugSymbols", "true"); - exportDebugGroup.AddProperty("DebugType", "portable"); - exportDebugGroup.AddProperty("Optimize", "false"); - exportDebugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;"); - exportDebugGroup.AddProperty("ErrorReport", "prompt"); - exportDebugGroup.AddProperty("WarningLevel", "4"); - exportDebugGroup.AddProperty("ConsolePause", "false"); + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - var exportReleaseGroup = root.AddPropertyGroup(); - exportReleaseGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportRelease|AnyCPU' "; - exportReleaseGroup.AddProperty("DebugType", "portable"); - exportReleaseGroup.AddProperty("Optimize", "true"); - exportReleaseGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;"); - exportReleaseGroup.AddProperty("ErrorReport", "prompt"); - exportReleaseGroup.AddProperty("WarningLevel", "4"); - exportReleaseGroup.AddProperty("ConsolePause", "false"); + string path = Path.Combine(dir, name + ".csproj"); - // References - var referenceGroup = root.AddItemGroup(); - referenceGroup.AddItem("Reference", "System"); + var root = GenGameProject(name); - root.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\")); + root.Save(path); - return root; + return Guid.NewGuid().ToString().ToUpper(); } - - private const string AssemblyInfoTemplate = - @"using System.Reflection;{0} - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle(""{1}"")] -[assembly: AssemblyDescription("""")] -[assembly: AssemblyConfiguration("""")] -[assembly: AssemblyCompany("""")] -[assembly: AssemblyProduct("""")] -[assembly: AssemblyCopyright("""")] -[assembly: AssemblyTrademark("""")] -[assembly: AssemblyCulture("""")] - -// The assembly version has the format ""{{Major}}.{{Minor}}.{{Build}}.{{Revision}}"". -// The form ""{{Major}}.{{Minor}}.*"" will automatically update the build and revision, -// and ""{{Major}}.{{Minor}}.{{Build}}.*"" will update just the revision. - -[assembly: AssemblyVersion(""1.0.*"")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("""")] -{2}"; } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index f2ebef1a7d..4041c56597 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -1,17 +1,17 @@ +using System; using GodotTools.Core; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; -using DotNet.Globbing; using Microsoft.Build.Construction; +using Microsoft.Build.Globbing; namespace GodotTools.ProjectEditor { public sealed class MSBuildProject { - public ProjectRootElement Root { get; } + internal ProjectRootElement Root { get; set; } public bool HasUnsavedChanges { get; set; } @@ -31,91 +31,7 @@ namespace GodotTools.ProjectEditor return root != null ? new MSBuildProject(root) : null; } - 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); - - 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); - - var normalizedOldInclude = oldInclude.NormalizePath(); - var normalizedNewInclude = newInclude.NormalizePath(); - - var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude); - - if (item == null) - 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); - - var normalizedInclude = include.NormalizePath(); - - if (root.RemoveItemChecked(itemType, normalizedInclude)) - 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); - - 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); - - 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) + private static List<string> GetAllFilesRecursive(string rootDirectory, string mask) { string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories); @@ -125,231 +41,60 @@ namespace GodotTools.ProjectEditor files[i] = files[i].RelativeToPath(rootDirectory); } - return files; + return new List<string>(files); } - public static string[] GetIncludeFiles(string projectPath, string itemType) + // NOTE: Assumes auto-including items. Only used by the scripts metadata generator, which will be replaced with source generators in the future. + public static IEnumerable<string> GetIncludeFiles(string projectPath, string itemType) { - var result = new List<string>(); - var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); - - var globOptions = new GlobOptions(); - globOptions.Evaluation.CaseInsensitive = false; + var excluded = new List<string>(); + var includedFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); - foreach (var itemGroup in root.ItemGroups) + foreach (var item in root.Items) { - if (itemGroup.Condition.Length != 0) + if (string.IsNullOrEmpty(item.Condition)) continue; - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - string normalizedInclude = item.Include.NormalizePath(); + if (item.ItemType != itemType) + continue; - var glob = Glob.Parse(normalizedInclude, globOptions); + string normalizedExclude = item.Exclude.NormalizePath(); - // TODO Check somehow if path has no blob to avoid the following loop... + var glob = MSBuildGlob.Parse(normalizedExclude); - foreach (var existingFile in existingFiles) - { - if (glob.IsMatch(existingFile)) - { - result.Add(existingFile); - } - } - } + excluded.AddRange(includedFiles.Where(includedFile => glob.IsMatch(includedFile))); } - return result.ToArray(); + includedFiles.RemoveAll(f => excluded.Contains(f)); + + return includedFiles; } - /// Simple function to make sure the Api assembly references are configured correctly - public static void FixApiHintPath(MSBuildProject project) + public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName) { - var root = project.Root; - - void AddPropertyIfNotPresent(string name, string condition, string value) - { - if (root.PropertyGroups - .Any(g => (g.Condition == string.Empty || g.Condition.Trim() == condition) && - g.Properties - .Any(p => p.Name == name && - p.Value == value && - (p.Condition.Trim() == condition || g.Condition.Trim() == condition)))) - { - return; - } - - root.AddProperty(name, value).Condition = " " + condition + " "; - project.HasUnsavedChanges = true; - } - - AddPropertyIfNotPresent(name: "ApiConfiguration", - condition: "'$(Configuration)' != 'ExportRelease'", - value: "Debug"); - AddPropertyIfNotPresent(name: "ApiConfiguration", - condition: "'$(Configuration)' == 'ExportRelease'", - value: "Release"); - - void SetReferenceHintPath(string referenceName, string condition, string hintPath) - { - foreach (var itemGroup in root.ItemGroups.Where(g => - g.Condition.Trim() == string.Empty || g.Condition.Trim() == condition)) - { - var references = itemGroup.Items.Where(item => - item.ItemType == "Reference" && - item.Include == referenceName && - (item.Condition.Trim() == condition || itemGroup.Condition.Trim() == condition)); - - var referencesWithHintPath = references.Where(reference => - reference.Metadata.Any(m => m.Name == "HintPath")); - - if (referencesWithHintPath.Any(reference => reference.Metadata - .Any(m => m.Name == "HintPath" && m.Value == hintPath))) - { - // Found a Reference item with the right HintPath - return; - } - - var referenceWithHintPath = referencesWithHintPath.FirstOrDefault(); - if (referenceWithHintPath != null) - { - // Found a Reference item with a wrong HintPath - foreach (var metadata in referenceWithHintPath.Metadata.ToList() - .Where(m => m.Name == "HintPath")) - { - // Safe to remove as we duplicate with ToList() to loop - referenceWithHintPath.RemoveChild(metadata); - } - - referenceWithHintPath.AddMetadata("HintPath", hintPath); - project.HasUnsavedChanges = true; - return; - } - - var referenceWithoutHintPath = references.FirstOrDefault(); - if (referenceWithoutHintPath != null) - { - // Found a Reference item without a HintPath - referenceWithoutHintPath.AddMetadata("HintPath", hintPath); - project.HasUnsavedChanges = true; - return; - } - } - - // Found no Reference item at all. Add it. - root.AddItem("Reference", referenceName).Condition = " " + condition + " "; - project.HasUnsavedChanges = true; - } - - const string coreProjectName = "GodotSharp"; - const string editorProjectName = "GodotSharpEditor"; + var origRoot = project.Root; - const string coreCondition = ""; - const string editorCondition = "'$(Configuration)' == 'Debug'"; - - var coreHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{coreProjectName}.dll"; - var editorHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{editorProjectName}.dll"; + if (!string.IsNullOrEmpty(origRoot.Sdk)) + return; - SetReferenceHintPath(coreProjectName, coreCondition, coreHintPath); - SetReferenceHintPath(editorProjectName, editorCondition, editorHintPath); + project.Root = ProjectGenerator.GenGameProject(projectName); + project.Root.FullPath = origRoot.FullPath; + project.HasUnsavedChanges = true; } - public static void MigrateFromOldConfigNames(MSBuildProject project) + public static void EnsureGodotSdkIsUpToDate(MSBuildProject project) { var root = project.Root; + string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; - bool hasGodotProjectGeneratorVersion = false; - bool foundOldConfiguration = false; - - foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition == string.Empty)) - { - if (!hasGodotProjectGeneratorVersion && propertyGroup.Properties.Any(p => p.Name == "GodotProjectGeneratorVersion")) - hasGodotProjectGeneratorVersion = true; - - foreach (var configItem in propertyGroup.Properties - .Where(p => p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Tools")) - { - configItem.Value = "Debug"; - foundOldConfiguration = true; - project.HasUnsavedChanges = true; - } - } - - if (!hasGodotProjectGeneratorVersion) - { - root.PropertyGroups.First(g => g.Condition == string.Empty)? - .AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); - project.HasUnsavedChanges = true; - } - - if (!foundOldConfiguration) - { - var toolsConditions = new[] - { - "'$(Configuration)|$(Platform)' == 'Tools|AnyCPU'", - "'$(Configuration)|$(Platform)' != 'Tools|AnyCPU'", - "'$(Configuration)' == 'Tools'", - "'$(Configuration)' != 'Tools'" - }; - - foundOldConfiguration = root.PropertyGroups - .Any(g => toolsConditions.Any(c => c == g.Condition.Trim())); - } - - if (foundOldConfiguration) - { - void MigrateConfigurationConditions(string oldConfiguration, string newConfiguration) - { - void MigrateConditions(string oldCondition, string newCondition) - { - foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition.Trim() == oldCondition)) - { - propertyGroup.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - - foreach (var propertyGroup in root.PropertyGroups) - { - foreach (var prop in propertyGroup.Properties.Where(p => p.Condition.Trim() == oldCondition)) - { - prop.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - } - - foreach (var itemGroup in root.ItemGroups.Where(g => g.Condition.Trim() == oldCondition)) - { - itemGroup.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - - foreach (var itemGroup in root.ItemGroups) - { - foreach (var item in itemGroup.Items.Where(item => item.Condition.Trim() == oldCondition)) - { - item.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - } - } - - foreach (var op in new[] {"==", "!="}) - { - MigrateConditions($"'$(Configuration)|$(Platform)' {op} '{oldConfiguration}|AnyCPU'", $"'$(Configuration)|$(Platform)' {op} '{newConfiguration}|AnyCPU'"); - MigrateConditions($"'$(Configuration)' {op} '{oldConfiguration}'", $"'$(Configuration)' {op} '{newConfiguration}'"); - } - } + if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) + return; - MigrateConfigurationConditions("Debug", "ExportDebug"); - MigrateConfigurationConditions("Release", "ExportRelease"); - MigrateConfigurationConditions("Tools", "Debug"); // Must be last - } + root.Sdk = godotSdkAttrValue; + project.HasUnsavedChanges = true; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs deleted file mode 100644 index 3a0464c9bc..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotTools.ProjectEditor")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Godot Engine contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] - diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config deleted file mode 100644 index 2db030f9d8..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> - <package id="DotNet.Glob" version="2.1.1" targetFramework="net45" /> -</packages> diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index a3438ea5f3..ba5379e562 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -9,7 +9,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotToo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeConnection", "GodotTools.IdeConnection\GodotTools.IdeConnection.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeMessaging", "GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio", "GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj", "{EAFFF236-FA96-4A4D-BD23-0E51EF988277}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -37,5 +39,9 @@ Global {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.Build.0 = Debug|Any CPU {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.ActiveCfg = Release|Any CPU {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.Build.0 = Release|Any CPU + {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 diff --git a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs index 3cf495f025..3ab669a9f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs @@ -20,52 +20,54 @@ namespace GodotTools private ItemList buildTabsList; private TabContainer buildTabs; - private ToolButton warningsBtn; - private ToolButton errorsBtn; + private Button warningsBtn; + private Button errorsBtn; private Button viewLogBtn; - private void _UpdateBuildTabsList() + private void _UpdateBuildTab(int index, int? currentTab) { - buildTabsList.Clear(); + var tab = (BuildTab)buildTabs.GetChild(index); - int currentTab = buildTabs.CurrentTab; + string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution); + itemName += " [" + tab.BuildInfo.Configuration + "]"; - bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount(); + buildTabsList.AddItem(itemName, tab.IconTexture); - for (int i = 0; i < buildTabs.GetChildCount(); i++) - { - var tab = (BuildTab)buildTabs.GetChild(i); + string itemTooltip = "Solution: " + tab.BuildInfo.Solution; + itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration; + itemTooltip += "\nStatus: "; - if (tab == null) - continue; + if (tab.BuildExited) + itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored"; + else + itemTooltip += "Running"; - string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution); - itemName += " [" + tab.BuildInfo.Configuration + "]"; + if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error) + itemTooltip += $"\nErrors: {tab.ErrorCount}"; - buildTabsList.AddItem(itemName, tab.IconTexture); + itemTooltip += $"\nWarnings: {tab.WarningCount}"; - string itemTooltip = "Solution: " + tab.BuildInfo.Solution; - itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration; - itemTooltip += "\nStatus: "; + buildTabsList.SetItemTooltip(index, itemTooltip); - if (tab.BuildExited) - itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored"; - else - itemTooltip += "Running"; + // If this tab was already selected before the changes or if no tab was selected + if (currentTab == null || currentTab == index) + { + buildTabsList.Select(index); + _BuildTabsItemSelected(index); + } + } - if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error) - itemTooltip += $"\nErrors: {tab.ErrorCount}"; + private void _UpdateBuildTabsList() + { + buildTabsList.Clear(); - itemTooltip += $"\nWarnings: {tab.WarningCount}"; + int? currentTab = buildTabs.CurrentTab; - buildTabsList.SetItemTooltip(i, itemTooltip); + if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) + currentTab = null; - if (noCurrentTab || currentTab == i) - { - buildTabsList.Select(i); - _BuildTabsItemSelected(i); - } - } + for (int i = 0; i < buildTabs.GetChildCount(); i++) + _UpdateBuildTab(i, currentTab); } public BuildTab GetBuildTabFor(BuildInfo buildInfo) @@ -160,13 +162,7 @@ namespace GodotTools } } - var godotDefines = new[] - { - OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - bool buildSuccess = BuildManager.BuildProjectBlocking("Debug", godotDefines); + bool buildSuccess = BuildManager.BuildProjectBlocking("Debug"); if (!buildSuccess) return; @@ -272,7 +268,7 @@ namespace GodotTools }; panelTabs.AddChild(panelBuildsTab); - var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; + var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill}; panelBuildsTab.AddChild(toolBarHBox); var buildProjectBtn = new Button @@ -285,7 +281,7 @@ namespace GodotTools toolBarHBox.AddSpacer(begin: false); - warningsBtn = new ToolButton + warningsBtn = new Button { Text = "Warnings".TTR(), ToggleMode = true, @@ -296,7 +292,7 @@ namespace GodotTools warningsBtn.Toggled += _WarningsToggled; toolBarHBox.AddChild(warningsBtn); - errorsBtn = new ToolButton + errorsBtn = new Button { Text = "Errors".TTR(), ToggleMode = true, @@ -325,7 +321,7 @@ namespace GodotTools }; panelBuildsTab.AddChild(hsc); - buildTabsList = new ItemList { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; + buildTabsList = new ItemList {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill}; buildTabsList.ItemSelected += _BuildTabsItemSelected; buildTabsList.NothingSelected += _BuildTabsNothingSelected; hsc.AddChild(buildTabsList); diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 43c96d2e30..d9862ae361 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -14,16 +14,6 @@ namespace GodotTools.Build { public static class BuildSystem { - private static string GetMsBuildPath() - { - string msbuildPath = MsBuildFinder.FindMsBuild(); - - if (msbuildPath == null) - throw new FileNotFoundException("Cannot find the MSBuild executable."); - - return msbuildPath; - } - private static string MonoWindowsBinDir { get @@ -46,8 +36,8 @@ namespace GodotTools.Build { if (OS.IsWindows) { - return (BuildManager.BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") - == BuildManager.BuildTool.MsBuildMono; + return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") + == BuildTool.MsBuildMono; } return false; @@ -57,16 +47,16 @@ namespace GodotTools.Build private static bool PrintBuildOutput => (bool)EditorSettings.GetSetting("mono/builds/print_build_output"); - private static Process LaunchBuild(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) + private static Process LaunchBuild(BuildInfo buildInfo) { - var customPropertiesList = new List<string>(); + (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); - if (customProperties != null) - customPropertiesList.AddRange(customProperties); + if (msbuildPath == null) + throw new FileNotFoundException("Cannot find the MSBuild executable."); - string compilerArgs = BuildArguments(solution, config, loggerOutputDir, customPropertiesList); + string compilerArgs = BuildArguments(buildTool, buildInfo); - var startInfo = new ProcessStartInfo(GetMsBuildPath(), compilerArgs); + var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput; @@ -90,7 +80,7 @@ namespace GodotTools.Build // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); - var process = new Process { StartInfo = startInfo }; + var process = new Process {StartInfo = startInfo}; process.Start(); @@ -105,19 +95,7 @@ namespace GodotTools.Build public static int Build(BuildInfo buildInfo) { - return Build(buildInfo.Solution, buildInfo.Configuration, - buildInfo.LogsDirPath, buildInfo.CustomProperties); - } - - public static async Task<int> BuildAsync(BuildInfo buildInfo) - { - return await BuildAsync(buildInfo.Solution, buildInfo.Configuration, - buildInfo.LogsDirPath, buildInfo.CustomProperties); - } - - public static int Build(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) - { - using (var process = LaunchBuild(solution, config, loggerOutputDir, customProperties)) + using (var process = LaunchBuild(buildInfo)) { process.WaitForExit(); @@ -125,9 +103,9 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) + public static async Task<int> BuildAsync(BuildInfo buildInfo) { - using (var process = LaunchBuild(solution, config, loggerOutputDir, customProperties)) + using (var process = LaunchBuild(buildInfo)) { await process.WaitForExitAsync(); @@ -135,12 +113,23 @@ namespace GodotTools.Build } } - private static string BuildArguments(string solution, string config, string loggerOutputDir, List<string> customProperties) + private static string BuildArguments(BuildTool buildTool, BuildInfo buildInfo) { - string arguments = $@"""{solution}"" /v:normal /t:Build ""/p:{"Configuration=" + config}"" " + - $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{loggerOutputDir}"""; + 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 customProperties) + foreach (string customProperty in buildInfo.CustomProperties) { arguments += " /p:" + customProperty; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs new file mode 100644 index 0000000000..837c8adddb --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs @@ -0,0 +1,10 @@ +namespace GodotTools.Build +{ + public enum BuildTool : long + { + MsBuildMono, + MsBuildVs, + JetBrainsMsBuild, + DotnetCli + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index af8d070cbd..f36e581a5f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -17,75 +17,96 @@ namespace GodotTools.Build private static string _msbuildToolsPath = string.Empty; private static string _msbuildUnixPath = string.Empty; - public static string FindMsBuild() + public static (string, BuildTool) FindMsBuild() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildManager.BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); + var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); if (OS.IsWindows) { switch (buildTool) { - case BuildManager.BuildTool.MsBuildVs: + case BuildTool.DotnetCli: { - if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath)) + string dotnetCliPath = OS.PathWhich("dotnet"); + if (!string.IsNullOrEmpty(dotnetCliPath)) + return (dotnetCliPath, BuildTool.DotnetCli); + GD.PushError("Cannot find dotnet CLI executable. 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 (_msbuildToolsPath.Empty()) - { - throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildVs}'."); - } + if (string.IsNullOrEmpty(_msbuildToolsPath)) + throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'."); } if (!_msbuildToolsPath.EndsWith("\\")) _msbuildToolsPath += "\\"; - return Path.Combine(_msbuildToolsPath, "MSBuild.exe"); + return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs); } - case BuildManager.BuildTool.MsBuildMono: + 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}"); - } + throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}"); - return msbuildPath; + return (msbuildPath, BuildTool.MsBuildMono); } - case BuildManager.BuildTool.JetBrainsMsBuild: + case BuildTool.JetBrainsMsBuild: + { var 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; - return Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe"); + + 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()) + if (OS.IsUnixLike) { - if (buildTool == BuildManager.BuildTool.MsBuildMono) + switch (buildTool) { - if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath)) + case BuildTool.DotnetCli: { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildUnixPath = FindBuildEngineOnUnix("msbuild"); + string dotnetCliPath = OS.PathWhich("dotnet"); + if (!string.IsNullOrEmpty(dotnetCliPath)) + return (dotnetCliPath, BuildTool.DotnetCli); + GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono."); + goto case BuildTool.MsBuildMono; } - - if (_msbuildUnixPath.Empty()) + case BuildTool.MsBuildMono: { - throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'"); - } + 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"); + } - return _msbuildUnixPath; - } - else - { - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); + 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"); } } @@ -114,12 +135,12 @@ namespace GodotTools.Build { string ret = OS.PathWhich(name); - if (!ret.Empty()) + if (!string.IsNullOrEmpty(ret)) return ret; string retFallback = OS.PathWhich($"{name}.exe"); - if (!retFallback.Empty()) + if (!string.IsNullOrEmpty(retFallback)) return retFallback; foreach (string hintDir in MsBuildHintDirs) @@ -143,7 +164,7 @@ namespace GodotTools.Build string vsWherePath = Environment.GetEnvironmentVariable(Internal.GodotIs32Bits() ? "ProgramFiles" : "ProgramFiles(x86)"); vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe"; - var vsWhereArgs = new[] { "-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild" }; + var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"}; var outputArray = new Godot.Collections.Array<string>(); int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs, @@ -171,7 +192,7 @@ namespace GodotTools.Build string value = line.Substring(sepIdx + 1).StripEdges(); - if (value.Empty()) + if (string.IsNullOrEmpty(value)) throw new FormatException("installationPath value is empty"); if (!value.EndsWith("\\")) diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs index 70bd552f2f..ab090c46e7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs @@ -10,7 +10,9 @@ namespace GodotTools 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; } public Array<string> CustomProperties { get; } = new Array<string>(); // TODO Use List once we have proper serialization public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); @@ -38,10 +40,12 @@ namespace GodotTools { } - public BuildInfo(string solution, string configuration) + public BuildInfo(string solution, string[] targets, string configuration, bool restore) { Solution = solution; + Targets = targets; Configuration = configuration; + Restore = restore; } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index 520e665595..6399991b84 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -6,6 +6,7 @@ using GodotTools.Build; using GodotTools.Ides.Rider; using GodotTools.Internals; using GodotTools.Utils; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; @@ -15,20 +16,14 @@ namespace GodotTools { private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>(); - public const string PropNameMsbuildMono = "MSBuild (Mono)"; - public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)"; - public const string PropNameMsbuildJetBrains = "MSBuild (JetBrains Rider)"; + 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 enum BuildTool - { - MsBuildMono, - MsBuildVs, - JetBrainsMsBuild - } - private static void RemoveOldIssuesFile(BuildInfo buildInfo) { var issuesFile = GetIssuesFilePath(buildInfo); @@ -158,7 +153,7 @@ namespace GodotTools } } - public static bool BuildProjectBlocking(string config, IEnumerable<string> godotDefines) + public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null) { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return true; // No solution to build @@ -174,27 +169,18 @@ namespace GodotTools return false; } - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) { pr.Step("Building project solution", 0); - var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, config); - - // Add Godot defines - string constants = buildTool != BuildTool.MsBuildMono ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Build"}, config, restore: true); - foreach (var godotDefine in godotDefines) - constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; + // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. + if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform)) + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); if (Internal.GodotIsRealTDouble()) - constants += "GODOT_REAL_T_IS_DOUBLE;"; - - constants += buildTool != BuildTool.MsBuildMono ? "\"" : "\\\""; - - buildInfo.CustomProperties.Add(constants); + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); if (!Build(buildInfo)) { @@ -219,7 +205,7 @@ namespace GodotTools if (File.Exists(editorScriptsMetadataPath)) File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); - var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest; + var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings; if (currentPlayRequest != null) { @@ -233,38 +219,55 @@ namespace GodotTools ",server=n"); } - return true; // Requested play from an external editor/IDE which already built the project + if (!currentPlayRequest.Value.BuildBeforePlaying) + return true; // Requested play from an external editor/IDE which already built the project } - var godotDefines = new[] - { - Godot.OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - return BuildProjectBlocking("Debug", godotDefines); + return BuildProjectBlocking("Debug"); } public static void Initialize() { // Build tool settings var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var msbuild = BuildTool.MsBuildMono; + + BuildTool msbuildDefault; + if (OS.IsWindows) - msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings) - ? BuildTool.JetBrainsMsBuild - : BuildTool.MsBuildVs; + { + 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; - EditorDef("mono/builds/build_tool", msbuild); + 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"] = OS.IsWindows ? - $"{PropNameMsbuildMono},{PropNameMsbuildVs},{PropNameMsbuildJetBrains}" : - $"{PropNameMsbuildMono}" + ["hint_string"] = hintString }); EditorDef("mono/builds/print_build_output", false); diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs index 938c3d8be1..8596cd24af 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs @@ -72,7 +72,7 @@ namespace GodotTools { string[] csvColumns = file.GetCsvLine(); - if (csvColumns.Length == 1 && csvColumns[0].Empty()) + if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0])) return; if (csvColumns.Length != 7) @@ -113,14 +113,14 @@ namespace GodotTools throw new IndexOutOfRangeException("Item list index out of range"); // Get correct issue idx from issue list - int issueIndex = (int)issuesList.GetItemMetadata(idx); + int issueIndex = (int)(long)issuesList.GetItemMetadata(idx); - if (idx < 0 || idx >= issues.Count) + if (issueIndex < 0 || issueIndex >= issues.Count) throw new IndexOutOfRangeException("Issue index out of range"); BuildIssue issue = issues[issueIndex]; - if (issue.ProjectFile.Empty() && issue.File.Empty()) + if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File)) return; string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir(); @@ -158,14 +158,14 @@ namespace GodotTools string tooltip = string.Empty; tooltip += $"Message: {issue.Message}"; - if (!issue.Code.Empty()) + if (!string.IsNullOrEmpty(issue.Code)) tooltip += $"\nCode: {issue.Code}"; tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}"; string text = string.Empty; - if (!issue.File.Empty()) + if (!string.IsNullOrEmpty(issue.File)) { text += $"{issue.File}({issue.Line},{issue.Column}): "; @@ -174,7 +174,7 @@ namespace GodotTools tooltip += $"\nColumn: {issue.Column}"; } - if (!issue.ProjectFile.Empty()) + if (!string.IsNullOrEmpty(issue.ProjectFile)) tooltip += $"\nProject: {issue.ProjectFile}"; text += issue.Message; diff --git a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs index 421729cc11..a8afb38728 100644 --- a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs +++ b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs @@ -1,9 +1,9 @@ using Godot; using System; +using System.Linq; using Godot.Collections; using GodotTools.Internals; using GodotTools.ProjectEditor; -using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; using Directory = GodotTools.Utils.Directory; @@ -15,7 +15,7 @@ namespace GodotTools { try { - return ProjectGenerator.GenGameProject(dir, name, compileItems: new string[] { }); + return ProjectGenerator.GenAndSaveGameProject(dir, name); } catch (Exception e) { @@ -24,14 +24,6 @@ namespace GodotTools } } - public static void AddItem(string projectPath, string itemType, string include) - { - if (!(bool)GlobalDef("mono/project/auto_update_project", true)) - return; - - ProjectUtils.AddItemToProjectChecked(projectPath, itemType, include); - } - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static ulong ConvertToTimestamp(this DateTime value) @@ -40,81 +32,77 @@ namespace GodotTools return (ulong)elapsedTime.TotalSeconds; } - public static void GenerateScriptsMetadata(string projectPath, string outputPath) + private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata) { - if (File.Exists(outputPath)) - File.Delete(outputPath); + fileMetadata = null; - var oldDict = Internal.GetScriptsMetadataOrNothing(); - var newDict = new Godot.Collections.Dictionary<string, object>(); + var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr); - foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile")) + if (parseError != Error.Ok) { - string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath(); + GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}"); + return false; + } - ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp(); + string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile); - if (oldDict.TryGetValue(projectIncludeFile, out var oldFileVar)) - { - var oldFileDict = (Dictionary)oldFileVar; - - if (ulong.TryParse(oldFileDict["modified_time"] as string, out ulong storedModifiedTime)) - { - if (storedModifiedTime == modifiedTime) - { - // No changes so no need to parse again - newDict[projectIncludeFile] = oldFileDict; - continue; - } - } - } + var firstMatch = classes.FirstOrDefault(classDecl => + classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object. + classDecl.SearchName != searchName // Filter by the name we're looking for + ); + + if (firstMatch == null) + return false; // Not found - Error parseError = ScriptClassParser.ParseFile(projectIncludeFile, out var classes, out string errorStr); - if (parseError != Error.Ok) + fileMetadata = new Dictionary + { + ["modified_time"] = $"{modifiedTime}", + ["class"] = new Dictionary { - GD.PushError($"Failed to determine namespace and class for script: {projectIncludeFile}. Parse error: {errorStr ?? parseError.ToString()}"); - continue; + ["namespace"] = firstMatch.Namespace, + ["class_name"] = firstMatch.Name, + ["nested"] = firstMatch.Nested } + }; - string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile); - - var classDict = new Dictionary(); + return true; + } - foreach (var classDecl in classes) - { - if (classDecl.BaseCount == 0) - continue; // Does not inherit nor implement anything, so it can't be a script class + public static void GenerateScriptsMetadata(string projectPath, string outputPath) + { + var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate(); - string classCmp = classDecl.Nested ? - classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : - classDecl.Name; + bool IsUpToDate(string includeFile, ulong modifiedTime) + { + return metadataDict.TryGetValue(includeFile, out var oldFileVar) && + ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string, + out ulong storedModifiedTime) && storedModifiedTime == modifiedTime; + } - if (classCmp != searchName) - continue; + var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile") + .Select(path => ("res://" + path).SimplifyGodotPath()) + .ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp()) + .Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value)) + .ToArray(); - classDict["namespace"] = classDecl.Namespace; - classDict["class_name"] = classDecl.Name; - classDict["nested"] = classDecl.Nested; - break; - } + foreach (var pair in outdatedFiles) + { + metadataDict.Remove(pair.Key); - if (classDict.Count == 0) - continue; // Not found + string includeFile = pair.Key; - newDict[projectIncludeFile] = new Dictionary { ["modified_time"] = $"{modifiedTime}", ["class"] = classDict }; + if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata)) + metadataDict[includeFile] = fileMetadata; } - if (newDict.Count > 0) - { - string json = JSON.Print(newDict); + string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict); - string baseDir = outputPath.GetBaseDir(); + string baseDir = outputPath.GetBaseDir(); - if (!Directory.Exists(baseDir)) - Directory.CreateDirectory(baseDir); + if (!Directory.Exists(baseDir)) + Directory.CreateDirectory(baseDir); - File.WriteAllText(outputPath, json); - } + File.WriteAllText(outputPath, json); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs index f1765f7e19..f60e469503 100755 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -587,7 +587,7 @@ MONO_AOT_MODE_LAST = 1000, string arch = "x86_64"; return $"{platform}-{arch}"; } - case OS.Platforms.X11: + case OS.Platforms.LinuxBSD: case OS.Platforms.Server: { string arch = bits == "64" ? "x86_64" : "i686"; diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 2ceb4888a2..554763eecb 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Runtime.CompilerServices; using GodotTools.Core; using GodotTools.Internals; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; @@ -145,9 +146,7 @@ namespace GodotTools.Export if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; - string platform = DeterminePlatformFromFeatures(features); - - if (platform == null) + if (!DeterminePlatformFromFeatures(features, out string platform)) throw new NotSupportedException("Target platform not supported"); string outputDir = new FileInfo(path).Directory?.FullName ?? @@ -160,10 +159,7 @@ namespace GodotTools.Export AddFile(scriptsMetadataPath, scriptsMetadataPath); - // Turn export features into defines - var godotDefines = features; - - if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) + if (!BuildManager.BuildProjectBlocking(buildConfig, platform)) throw new Exception("Failed to build project"); // Add dependency assemblies @@ -289,6 +285,7 @@ namespace GodotTools.Export } } + [NotNull] private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir) { string target = isDebug ? "release_debug" : "release"; @@ -343,18 +340,19 @@ namespace GodotTools.Export 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); + return !new[] {OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform); } - private static string DeterminePlatformFromFeatures(IEnumerable<string> features) + private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) { foreach (var feature in features) { - if (OS.PlatformNameMap.TryGetValue(feature, out string platform)) - return platform; + if (OS.PlatformNameMap.TryGetValue(feature, out platform)) + return true; } - return null; + platform = null; + return false; } private static string GetBclProfileDir(string profile) @@ -391,7 +389,7 @@ namespace GodotTools.Export /// </summary> private static bool PlatformRequiresCustomBcl(string platform) { - if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform)) + if (new[] {OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform)) return true; // The 'net_4_x' BCL is not compatible between Windows and the other platforms. @@ -414,7 +412,7 @@ namespace GodotTools.Export case OS.Platforms.UWP: return "net_4_x_win"; case OS.Platforms.OSX: - case OS.Platforms.X11: + case OS.Platforms.LinuxBSD: case OS.Platforms.Server: case OS.Platforms.Haiku: return "net_4_x"; diff --git a/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs b/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs index bb218c2f19..90d6eb960e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs +++ b/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs @@ -1,6 +1,6 @@ namespace GodotTools { - public enum ExternalEditorId + public enum ExternalEditorId : long { None, VisualStudio, // TODO (Windows-only) diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index c070cb16d9..a363ecc920 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using GodotTools.Ides; using GodotTools.Ides.Rider; using GodotTools.Internals; @@ -29,7 +30,7 @@ namespace GodotTools private AcceptDialog aboutDialog; private CheckBox aboutDialogCheckBox; - private ToolButton bottomPanelBtn; + private Button bottomPanelBtn; public GodotIdeManager GodotIdeManager { get; private set; } @@ -37,6 +38,8 @@ namespace GodotTools public BottomPanel BottomPanel { get; private set; } + public PlaySettings? CurrentPlaySettings { get; set; } + public static string ProjectAssemblyName { get @@ -172,36 +175,6 @@ namespace GodotTools // Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on. aboutDialog.Exclusive = false; } - - var fileSystemDock = GetEditorInterface().GetFileSystemDock(); - - fileSystemDock.FilesMoved += (file, newFile) => - { - if (Path.GetExtension(file) == Internal.CSharpLanguageExtension) - { - ProjectUtils.RenameItemInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(file), ProjectSettings.GlobalizePath(newFile)); - } - }; - - fileSystemDock.FileRemoved += file => - { - if (Path.GetExtension(file) == Internal.CSharpLanguageExtension) - ProjectUtils.RemoveItemFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(file)); - }; - - fileSystemDock.FolderMoved += (oldFolder, newFolder) => - { - ProjectUtils.RenameItemsToNewFolderInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(oldFolder), ProjectSettings.GlobalizePath(newFolder)); - }; - - fileSystemDock.FolderRemoved += oldFolder => - { - ProjectUtils.RemoveItemsInFolderFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(oldFolder)); - }; } } @@ -228,15 +201,39 @@ namespace GodotTools [UsedImplicitly] public Error OpenInExternalEditor(Script script, int line, int col) { - var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor"); + var editorId = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor"); - switch (editor) + switch (editorId) { case ExternalEditorId.None: - // Tells the caller to fallback to the global external editor settings or the built-in editor + // 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: - throw new NotSupportedException(); + { + 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: @@ -249,17 +246,20 @@ namespace GodotTools { string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); - if (line >= 0) - GodotIdeManager.SendOpenFile(scriptPath, line + 1, col); - else - GodotIdeManager.SendOpenFile(scriptPath); + 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 (_vsCodePath.Empty() || !File.Exists(_vsCodePath)) + if (string.IsNullOrEmpty(_vsCodePath) || !File.Exists(_vsCodePath)) { // Try to search it again if it wasn't found last time or if it was removed from its location _vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty); @@ -300,7 +300,7 @@ namespace GodotTools if (line >= 0) { args.Add("-g"); - args.Add($"{scriptPath}:{line + 1}:{col}"); + args.Add($"{scriptPath}:{line}:{col}"); } else { @@ -311,7 +311,7 @@ namespace GodotTools if (OS.IsOSX) { - if (!osxAppBundleInstalled && _vsCodePath.Empty()) + if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath)) { GD.PushError("Cannot find code editor: VSCode"); return Error.FileNotFound; @@ -321,7 +321,7 @@ namespace GodotTools } else { - if (_vsCodePath.Empty()) + if (string.IsNullOrEmpty(_vsCodePath)) { GD.PushError("Cannot find code editor: VSCode"); return Error.FileNotFound; @@ -341,7 +341,6 @@ namespace GodotTools break; } - default: throw new ArgumentOutOfRangeException(); } @@ -360,6 +359,37 @@ namespace GodotTools return BuildManager.EditorBuildCallback(); } + private void ApplyNecessaryChangesToSolution() + { + try + { + // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease + DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); + + var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) + ?? throw new Exception("Cannot open C# project"); + + // NOTE: The order in which changes are made to the project is important + + // Migrate to MSBuild project Sdks style if using the old style + ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, ProjectAssemblyName); + + ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject); + + if (msbuildProject.HasUnsavedChanges) + { + // Save a copy of the project before replacing it + FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath); + + msbuildProject.Save(); + } + } + catch (Exception e) + { + GD.PushError(e.ToString()); + } + } + public override void EnablePlugin() { base.EnablePlugin(); @@ -417,7 +447,7 @@ namespace GodotTools aboutLabel.Text = "C# support in Godot Engine is in late alpha stage and, while already usable, " + "it is not meant for use in production.\n\n" + - "Projects can be exported to Linux, macOS, Windows and Android, but not yet to iOS, HTML5 or UWP. " + + "Projects can be exported to Linux, macOS, Windows, Android, iOS and HTML5, but not yet to UWP. " + "Bugs and usability issues will be addressed gradually over future releases, " + "potentially including compatibility breaking changes as new features are implemented for a better overall C# experience.\n\n" + "If you experience issues with this Mono build, please report them on Godot's issue tracker with details about your system, MSBuild version, IDE, etc.:\n\n" + @@ -439,36 +469,7 @@ namespace GodotTools if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) { - try - { - // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease - DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); - - var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) - ?? throw new Exception("Cannot open C# project"); - - // NOTE: The order in which changes are made to the project is important - - // Migrate csproj from old configuration names to: Debug, ExportDebug and ExportRelease - ProjectUtils.MigrateFromOldConfigNames(msbuildProject); - - // Apply the other fixes only after configurations have been migrated - - // Make sure the existing project has Api assembly references configured correctly - ProjectUtils.FixApiHintPath(msbuildProject); - - if (msbuildProject.HasUnsavedChanges) - { - // Save a copy of the project before replacing it - FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath); - - msbuildProject.Save(); - } - } - catch (Exception e) - { - GD.PushError(e.ToString()); - } + ApplyNecessaryChangesToSolution(); } else { @@ -478,7 +479,7 @@ namespace GodotTools menuPopup.IdPressed += _MenuOptionPressed; - var buildButton = new ToolButton + var buildButton = new Button { Text = "Build", HintTooltip = "Build solution", @@ -494,7 +495,8 @@ namespace GodotTools if (OS.IsWindows) { - settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" + + settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudio}" + + $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" + $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" + $",JetBrains Rider:{(int)ExternalEditorId.Rider}"; } @@ -505,7 +507,7 @@ namespace GodotTools $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" + $",JetBrains Rider:{(int)ExternalEditorId.Rider}"; } - else if (OS.IsUnixLike()) + else if (OS.IsUnixLike) { settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" + $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" + diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index ac9379adf8..3f14629b11 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -1,123 +1,39 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid> - <OutputType>Library</OutputType> - <RootNamespace>GodotTools</RootNamespace> - <AssemblyName>GodotTools</AssemblyName> - <TargetFrameworkVersion>v4.7</TargetFrameworkVersion> + <TargetFramework>net472</TargetFramework> + <LangVersion>7.2</LangVersion> + <GodotApiConfiguration>Debug</GodotApiConfiguration> <!-- The Godot editor uses the Debug Godot API assemblies --> <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> - <DataDirToolsOutputPath>$(GodotSourceRootPath)/bin/GodotSharp/Tools</DataDirToolsOutputPath> - <GodotApiConfiguration>Debug</GodotApiConfiguration> - <LangVersion>7</LangVersion> + <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> + <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug</OutputPath> - <DefineConstants>DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <Optimize>true</Optimize> - <OutputPath>bin\Release</OutputPath> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> + <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> - <Reference Include="JetBrains.Annotations, Version=2019.1.3.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325"> - <HintPath>..\packages\JetBrains.Annotations.2019.1.3\lib\net20\JetBrains.Annotations.dll</HintPath> - <Private>True</Private> - </Reference> - <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed"> - <HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> - </Reference> - <Reference Include="System" /> + <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="12.0.3" /> <Reference Include="GodotSharp"> - <HintPath>$(GodotSourceRootPath)/bin/GodotSharp/Api/$(GodotApiConfiguration)/GodotSharp.dll</HintPath> + <HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath> <Private>False</Private> </Reference> <Reference Include="GodotSharpEditor"> - <HintPath>$(GodotSourceRootPath)/bin/GodotSharp/Api/$(GodotApiConfiguration)/GodotSharpEditor.dll</HintPath> + <HintPath>$(GodotApiAssembliesDir)/GodotSharpEditor.dll</HintPath> <Private>False</Private> </Reference> </ItemGroup> <ItemGroup> - <Compile Include="Build\MsBuildFinder.cs" /> - <Compile Include="Export\AotBuilder.cs" /> - <Compile Include="Export\ExportPlugin.cs" /> - <Compile Include="Export\XcodeHelper.cs" /> - <Compile Include="ExternalEditorId.cs" /> - <Compile Include="Ides\GodotIdeManager.cs" /> - <Compile Include="Ides\GodotIdeServer.cs" /> - <Compile Include="Ides\MonoDevelop\EditorId.cs" /> - <Compile Include="Ides\MonoDevelop\Instance.cs" /> - <Compile Include="Ides\Rider\RiderPathLocator.cs" /> - <Compile Include="Ides\Rider\RiderPathManager.cs" /> - <Compile Include="Internals\EditorProgress.cs" /> - <Compile Include="Internals\GodotSharpDirs.cs" /> - <Compile Include="Internals\Internal.cs" /> - <Compile Include="Internals\ScriptClassParser.cs" /> - <Compile Include="Internals\Globals.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Build\BuildSystem.cs" /> - <Compile Include="Utils\Directory.cs" /> - <Compile Include="Utils\File.cs" /> - <Compile Include="Utils\NotifyAwaiter.cs" /> - <Compile Include="Utils\OS.cs" /> - <Compile Include="GodotSharpEditor.cs" /> - <Compile Include="BuildManager.cs" /> - <Compile Include="HotReloadAssemblyWatcher.cs" /> - <Compile Include="BuildInfo.cs" /> - <Compile Include="BuildTab.cs" /> - <Compile Include="BottomPanel.cs" /> - <Compile Include="CsProjOperations.cs" /> - <Compile Include="Utils\CollectionExtensions.cs" /> - <Compile Include="Utils\User32Dll.cs" /> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj"> - <Project>{6ce9a984-37b1-4f8a-8fe9-609f05f071b3}</Project> - <Name>GodotTools.BuildLogger</Name> - </ProjectReference> - <ProjectReference Include="..\GodotTools.IdeConnection\GodotTools.IdeConnection.csproj"> - <Project>{92600954-25f0-4291-8e11-1fee9fc4be20}</Project> - <Name>GodotTools.IdeConnection</Name> - </ProjectReference> - <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj"> - <Project>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</Project> - <Name>GodotTools.ProjectEditor</Name> - </ProjectReference> - <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj"> - <Project>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</Project> - <Name>GodotTools.Core</Name> - </ProjectReference> - </ItemGroup> - <ItemGroup> - <None Include="packages.config" /> + <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" /> + <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.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> - <Target Name="CopyToDataDir" AfterTargets="Build"> - <ItemGroup> - <GodotToolsCopy Include="$(OutputPath)\GodotTools*.dll" /> - <GodotToolsCopy Include="$(OutputPath)\Newtonsoft.Json.dll" /> - <GodotToolsCopy Include="$(OutputPath)\DotNet.Glob.dll" /> - </ItemGroup> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <GodotToolsCopy Include="$(OutputPath)\GodotTools*.pdb" /> - </ItemGroup> - <Copy SourceFiles="@(GodotToolsCopy)" DestinationFolder="$(DataDirToolsOutputPath)" ContinueOnError="false" /> - </Target> - <Target Name="BuildAlwaysCopyToDataDir"> - <!-- Custom target run by SCons to make sure the CopyToDataDir target is always executed, without having to use DisableFastUpToDateCheck --> - <CallTarget Targets="Build" /> - <CallTarget Targets="CopyToDataDir" /> - </Target> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs index ae05710f4f..b30c857c64 100644 --- a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs +++ b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs @@ -10,7 +10,7 @@ namespace GodotTools public override void _Notification(int what) { - if (what == Node.NotificationWmFocusIn) + if (what == Node.NotificationWmWindowFocusIn) { RestartTimer(); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 54f0ffab96..e4932ca217 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -1,73 +1,104 @@ using System; using System.IO; +using System.Threading.Tasks; using Godot; -using GodotTools.IdeConnection; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; using GodotTools.Internals; namespace GodotTools.Ides { - public class GodotIdeManager : Node, ISerializationListener + public sealed class GodotIdeManager : Node, ISerializationListener { - public GodotIdeServer GodotIdeServer { get; private set; } + private MessagingServer MessagingServer { get; set; } private MonoDevelop.Instance monoDevelInstance; private MonoDevelop.Instance vsForMacInstance; - private GodotIdeServer GetRunningServer() + private MessagingServer GetRunningOrNewServer() { - if (GodotIdeServer != null && !GodotIdeServer.IsDisposed) - return GodotIdeServer; - StartServer(); - return GodotIdeServer; + 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() { - StartServer(); + _ = GetRunningOrNewServer(); } public void OnBeforeSerialize() { - GodotIdeServer?.Dispose(); } public void OnAfterDeserialize() { - StartServer(); + _ = GetRunningOrNewServer(); } - private ILogger logger; + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); - protected ILogger Logger + if (disposing) + { + MessagingServer?.Dispose(); + } + } + + private string GetExternalEditorIdentity(ExternalEditorId editorId) { - get => logger ?? (logger = new GodotLogger()); + // 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(); + } } - private void StartServer() + public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000) { - GodotIdeServer?.Dispose(); - GodotIdeServer = new GodotIdeServer(LaunchIde, - OS.GetExecutablePath(), - ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir)); + var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface() + .GetEditorSettings().GetSetting("mono/editor/external_editor"); + string editorIdentity = GetExternalEditorIdentity(editorId); - GodotIdeServer.Logger = Logger; + var runningServer = GetRunningOrNewServer(); - GodotIdeServer.StartServer(); - } + if (runningServer.IsAnyConnected(editorIdentity)) + return new EditorPick(editorIdentity); - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); + LaunchIde(editorId, editorIdentity); + + var timeoutTask = Task.Delay(millisecondsTimeout); + var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity)); + + if (completedTask != timeoutTask) + return new EditorPick(editorIdentity); - GodotIdeServer?.Dispose(); + return null; } - private void LaunchIde() + private void LaunchIde(ExternalEditorId editorId, string editorIdentity) { - var editor = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface() - .GetEditorSettings().GetSetting("mono/editor/external_editor"); - - switch (editor) + switch (editorId) { case ExternalEditorId.None: case ExternalEditorId.VisualStudio: @@ -80,14 +111,14 @@ namespace GodotTools.Ides { MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath) { - if (Utils.OS.IsOSX && editor == ExternalEditorId.VisualStudioForMac) + if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac) { - vsForMacInstance = vsForMacInstance ?? + vsForMacInstance = (vsForMacInstance?.IsDisposed ?? true ? null : vsForMacInstance) ?? new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac); return vsForMacInstance; } - monoDevelInstance = monoDevelInstance ?? + monoDevelInstance = (monoDevelInstance?.IsDisposed ?? true ? null : monoDevelInstance) ?? new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop); return monoDevelInstance; } @@ -96,12 +127,25 @@ namespace GodotTools.Ides { var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath); - if (!instance.IsRunning) + 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 = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; + string editorName = editorId == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; GD.PushError($"Cannot find code editor: {editorName}"); } @@ -113,26 +157,45 @@ namespace GodotTools.Ides } } - private void WriteMessage(string id, params string[] arguments) + public readonly struct EditorPick { - GetRunningServer().WriteMessage(new Message(id, arguments)); - } + private readonly string identity; - public void SendOpenFile(string file) - { - WriteMessage("OpenFile", file); - } + public EditorPick(string identity) + { + this.identity = identity; + } - public void SendOpenFile(string file, int line) - { - WriteMessage("OpenFile", file, line.ToString()); - } + public bool IsAnyConnected() => + GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(identity); - public void SendOpenFile(string file, int line, int column) - { - WriteMessage("OpenFile", file, line.ToString(), column.ToString()); + 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) diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs deleted file mode 100644 index 72676a8b24..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using GodotTools.IdeConnection; -using GodotTools.Internals; -using GodotTools.Utils; -using Directory = System.IO.Directory; -using File = System.IO.File; -using Thread = System.Threading.Thread; - -namespace GodotTools.Ides -{ - public class GodotIdeServer : GodotIdeBase - { - private readonly TcpListener listener; - private readonly FileStream metaFile; - private readonly Action launchIdeAction; - private readonly NotifyAwaiter<bool> clientConnectedAwaiter = new NotifyAwaiter<bool>(); - - private async Task<bool> AwaitClientConnected() - { - return await clientConnectedAwaiter.Reset(); - } - - public GodotIdeServer(Action launchIdeAction, string editorExecutablePath, string projectMetadataDir) - : base(projectMetadataDir) - { - messageHandlers = InitializeMessageHandlers(); - - this.launchIdeAction = launchIdeAction; - - // 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); - } - - StartServer(); - } - - public void StartServer() - { - var serverThread = new Thread(RunServerThread) { Name = "Godot Ide Connection Server" }; - serverThread.Start(); - } - - private void RunServerThread() - { - SynchronizationContext.SetSynchronizationContext(Godot.Dispatcher.SynchronizationContext); - - try - { - while (!IsDisposed) - { - TcpClient tcpClient = listener.AcceptTcpClient(); - - Logger.LogInfo("Connection open with Ide Client"); - - lock (ConnectionLock) - { - Connection = new GodotIdeConnectionServer(tcpClient, HandleMessage); - Connection.Logger = Logger; - } - - Connected += () => clientConnectedAwaiter.SetResult(true); - - Connection.Start(); - } - } - catch (Exception e) - { - if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted)) - throw; - } - } - - public async void WriteMessage(Message message) - { - async Task LaunchIde() - { - if (IsConnected) - return; - - launchIdeAction(); - await Task.WhenAny(Task.Delay(10000), AwaitClientConnected()); - } - - await LaunchIde(); - - if (!IsConnected) - { - Logger.LogError("Cannot write message: Godot Ide Server not connected"); - return; - } - - Connection.WriteMessage(message); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - listener?.Stop(); - - metaFile?.Dispose(); - - File.Delete(MetaFilePath); - } - } - - protected virtual bool HandleMessage(Message message) - { - if (messageHandlers.TryGetValue(message.Id, out var action)) - { - action(message.Arguments); - return true; - } - - return false; - } - - private readonly Dictionary<string, Action<string[]>> messageHandlers; - - private Dictionary<string, Action<string[]>> InitializeMessageHandlers() - { - return new Dictionary<string, Action<string[]>> - { - ["Play"] = args => - { - switch (args.Length) - { - case 0: - Play(); - return; - case 2: - Play(debuggerHost: args[0], debuggerPort: int.Parse(args[1])); - return; - default: - throw new ArgumentException(); - } - }, - ["ReloadScripts"] = args => ReloadScripts() - }; - } - - private void DispatchToMainThread(Action action) - { - var d = new SendOrPostCallback(state => action()); - Godot.Dispatcher.SynchronizationContext.Post(d, null); - } - - private void Play() - { - DispatchToMainThread(() => - { - CurrentPlayRequest = new PlayRequest(); - Internal.EditorRunPlay(); - CurrentPlayRequest = null; - }); - } - - private void Play(string debuggerHost, int debuggerPort) - { - DispatchToMainThread(() => - { - CurrentPlayRequest = new PlayRequest(debuggerHost, debuggerPort); - Internal.EditorRunPlay(); - CurrentPlayRequest = null; - }); - } - - private void ReloadScripts() - { - DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts); - } - - public PlayRequest? CurrentPlayRequest { get; private set; } - - public struct PlayRequest - { - public bool HasDebugger { get; } - public string DebuggerHost { get; } - public int DebuggerPort { get; } - - public PlayRequest(string debuggerHost, int debuggerPort) - { - HasDebugger = true; - DebuggerHost = debuggerHost; - DebuggerPort = debuggerPort; - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs new file mode 100644 index 0000000000..17f3339560 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -0,0 +1,377 @@ +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 { get; } + + 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(() => + { + GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings(); + Internal.EditorRunPlay(); + GodotSharpEditor.Instance.CurrentPlaySettings = null; + }); + return Task.FromResult<Response>(new PlayResponse()); + } + + private static Task<Response> HandleDebugPlay(DebugPlayRequest request) + { + DispatchToMainThread(() => + { + GodotSharpEditor.Instance.CurrentPlaySettings = + new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true); + Internal.EditorRunPlay(); + GodotSharpEditor.Instance.CurrentPlaySettings = null; + }); + 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; + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index 6026c109ad..d6fa2eeba7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -7,14 +7,16 @@ using GodotTools.Utils; namespace GodotTools.Ides.MonoDevelop { - public class Instance + 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() { @@ -59,6 +61,8 @@ namespace GodotTools.Ides.MonoDevelop if (command == null) throw new FileNotFoundException(); + LaunchTime = DateTime.Now; + if (newWindow) { process = Process.Start(new ProcessStartInfo @@ -88,6 +92,12 @@ namespace GodotTools.Ides.MonoDevelop this.editorId = editorId; } + public void Dispose() + { + IsDisposed = true; + process?.Dispose(); + } + private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames; private static readonly IReadOnlyDictionary<EditorId, string> BundleIds; @@ -118,7 +128,7 @@ namespace GodotTools.Ides.MonoDevelop {EditorId.MonoDevelop, "MonoDevelop.exe"} }; } - else if (OS.IsUnixLike()) + else if (OS.IsUnixLike) { ExecutableNames = new Dictionary<EditorId, string> { diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs index e3a4fa7b45..e22e9af919 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs @@ -36,7 +36,7 @@ namespace GodotTools.Ides.Rider { return CollectRiderInfosMac(); } - if (OS.IsUnixLike()) + if (OS.IsUnixLike) { return CollectAllRiderPathsLinux(); } @@ -141,16 +141,16 @@ namespace GodotTools.Ides.Rider if (OS.IsOSX) { var home = Environment.GetEnvironmentVariable("HOME"); - if (string.IsNullOrEmpty(home)) + if (string.IsNullOrEmpty(home)) return string.Empty; var localAppData = Path.Combine(home, @"Library/Application Support"); return GetToolboxRiderRootPath(localAppData); } - if (OS.IsUnixLike()) + if (OS.IsUnixLike) { var home = Environment.GetEnvironmentVariable("HOME"); - if (string.IsNullOrEmpty(home)) + if (string.IsNullOrEmpty(home)) return string.Empty; var localAppData = Path.Combine(home, @".local/share"); return GetToolboxRiderRootPath(localAppData); @@ -209,7 +209,7 @@ namespace GodotTools.Ides.Rider private static string GetRelativePathToBuildTxt() { - if (OS.IsWindows || OS.IsUnixLike()) + if (OS.IsWindows || OS.IsUnixLike) return "../../build.txt"; if (OS.IsOSX) return "Contents/Resources/build.txt"; @@ -322,7 +322,7 @@ namespace GodotTools.Ides.Rider class SettingsJson { public string install_location; - + [CanBeNull] public static string GetInstallLocationFromJson(string json) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 026a7db89c..7e5049e4b7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -2,6 +2,7 @@ using System; using System.Runtime.CompilerServices; using Godot; using Godot.Collections; +using GodotTools.IdeMessaging.Requests; namespace GodotTools.Internals { @@ -52,6 +53,9 @@ namespace GodotTools.Internals 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)] @@ -111,6 +115,9 @@ namespace GodotTools.Internals [MethodImpl(MethodImplOptions.InternalCall)] private static extern void internal_ScriptEditorDebugger_ReloadScripts(); + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile); + #endregion } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs index 7fb087467f..c72a84c513 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs @@ -13,9 +13,13 @@ namespace GodotTools.Internals public string Name { get; } public string Namespace { get; } public bool Nested { get; } - public int BaseCount { get; } + public long BaseCount { get; } - public ClassDecl(string name, string @namespace, bool nested, int baseCount) + public string SearchName => Nested ? + Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : + Name; + + public ClassDecl(string name, string @namespace, bool nested, long baseCount) { Name = name; Namespace = @namespace; @@ -45,7 +49,7 @@ namespace GodotTools.Internals (string)classDeclDict["name"], (string)classDeclDict["namespace"], (bool)classDeclDict["nested"], - (int)classDeclDict["base_count"] + (long)classDeclDict["base_count"] )); } diff --git a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs new file mode 100644 index 0000000000..820d0c0b83 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs @@ -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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs deleted file mode 100644 index f5fe85c722..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotTools")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Godot Engine contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs new file mode 100644 index 0000000000..c6724ccaf7 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Godot; +using GodotTools.Core; +using JetBrains.Annotations; + +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); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index b057ac12c6..6c05891f2c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -22,7 +22,10 @@ namespace GodotTools.Utils { public const string Windows = "Windows"; public const string OSX = "OSX"; - public const string X11 = "X11"; + public const string Linux = "Linux"; + public const string FreeBSD = "FreeBSD"; + public const string NetBSD = "NetBSD"; + public const string BSD = "BSD"; public const string Server = "Server"; public const string UWP = "UWP"; public const string Haiku = "Haiku"; @@ -35,7 +38,7 @@ namespace GodotTools.Utils { public const string Windows = "windows"; public const string OSX = "osx"; - public const string X11 = "linuxbsd"; + public const string LinuxBSD = "linuxbsd"; public const string Server = "server"; public const string UWP = "uwp"; public const string Haiku = "haiku"; @@ -48,7 +51,10 @@ namespace GodotTools.Utils { [Names.Windows] = Platforms.Windows, [Names.OSX] = Platforms.OSX, - [Names.X11] = Platforms.X11, + [Names.Linux] = Platforms.LinuxBSD, + [Names.FreeBSD] = Platforms.LinuxBSD, + [Names.NetBSD] = Platforms.LinuxBSD, + [Names.BSD] = Platforms.LinuxBSD, [Names.Server] = Platforms.Server, [Names.UWP] = Platforms.UWP, [Names.Haiku] = Platforms.Haiku, @@ -62,38 +68,39 @@ namespace GodotTools.Utils 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 IEnumerable<string> LinuxBSDPlatforms = + new[] {Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD}; + + private static readonly IEnumerable<string> UnixLikePlatforms = + new[] {Names.OSX, Names.Server, Names.Haiku, Names.Android, Names.iOS} + .Concat(LinuxBSDPlatforms).ToArray(); + 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> _isLinuxBSD = new Lazy<bool>(() => IsAnyOS(LinuxBSDPlatforms)); 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 IsLinuxBSD => _isLinuxBSD.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 bool? _isUnixCache; - private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS }; - - public static bool IsUnixLike() - { - if (_isUnixCache.HasValue) - return _isUnixCache.Value; - - string osName = GetPlatformName(); - _isUnixCache = UnixLikePlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase)); - return _isUnixCache.Value; - } + public static bool IsUnixLike => _isUnixLike.Value; public static char PathSep => IsWindows ? ';' : ':'; @@ -121,10 +128,10 @@ namespace GodotTools.Utils return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists); return (from dir in searchDirs - select Path.Combine(dir, name) + select Path.Combine(dir, name) into path - from ext in windowsExts - select path + ext).FirstOrDefault(File.Exists); + from ext in windowsExts + select path + ext).FirstOrDefault(File.Exists); } private static string PathWhichUnix([NotNull] string name) @@ -189,7 +196,7 @@ namespace GodotTools.Utils startInfo.UseShellExecute = false; - using (var process = new Process { StartInfo = startInfo }) + using (var process = new Process {StartInfo = startInfo}) { process.Start(); process.WaitForExit(); diff --git a/modules/mono/editor/GodotTools/GodotTools/packages.config b/modules/mono/editor/GodotTools/GodotTools/packages.config deleted file mode 100644 index dd3de2865a..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/packages.config +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> - <package id="JetBrains.Annotations" version="2019.1.3" targetFramework="net45" /> - <package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" /> -</packages> diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index ee92a6234a..a17c371117 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -45,7 +45,6 @@ #include "../mono_gd/gd_mono_marshal.h" #include "../utils/path_utils.h" #include "../utils/string_utils.h" -#include "csharp_project.h" #define CS_INDENT " " // 4 whitespaces @@ -62,10 +61,8 @@ #define OPEN_BLOCK_L2 INDENT2 OPEN_BLOCK INDENT3 #define OPEN_BLOCK_L3 INDENT3 OPEN_BLOCK INDENT4 -#define OPEN_BLOCK_L4 INDENT4 OPEN_BLOCK INDENT5 #define CLOSE_BLOCK_L2 INDENT2 CLOSE_BLOCK #define CLOSE_BLOCK_L3 INDENT3 CLOSE_BLOCK -#define CLOSE_BLOCK_L4 INDENT4 CLOSE_BLOCK #define CS_FIELD_MEMORYOWN "memoryOwn" #define CS_PARAM_METHODBIND "method" @@ -105,7 +102,6 @@ const char *BindingsGenerator::TypeInterface::DEFAULT_VARARG_C_IN("\t%0 %1_in = %1;\n"); static String fix_doc_description(const String &p_bbcode) { - // This seems to be the correct way to do this. It's the same EditorHelp does. return p_bbcode.dedent() @@ -115,7 +111,6 @@ static String fix_doc_description(const String &p_bbcode) { } static String snake_to_pascal_case(const String &p_identifier, bool p_input_is_upper = false) { - String ret; Vector<String> parts = p_identifier.split("_", true); @@ -125,8 +120,9 @@ static String snake_to_pascal_case(const String &p_identifier, bool p_input_is_u if (part.length()) { part[0] = _find_upper(part[0]); if (p_input_is_upper) { - for (int j = 1; j < part.length(); j++) + for (int j = 1; j < part.length(); j++) { part[j] = _find_lower(part[j]); + } } ret += part; } else { @@ -148,7 +144,6 @@ static String snake_to_pascal_case(const String &p_identifier, bool p_input_is_u } static String snake_to_camel_case(const String &p_identifier, bool p_input_is_upper = false) { - String ret; Vector<String> parts = p_identifier.split("_", true); @@ -160,8 +155,9 @@ static String snake_to_camel_case(const String &p_identifier, bool p_input_is_up part[0] = _find_upper(part[0]); } if (p_input_is_upper) { - for (int j = i != 0 ? 1 : 0; j < part.length(); j++) + for (int j = i != 0 ? 1 : 0; j < part.length(); j++) { part[j] = _find_lower(part[j]); + } } ret += part; } else { @@ -183,11 +179,11 @@ static String snake_to_camel_case(const String &p_identifier, bool p_input_is_up } String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterface *p_itype) { - // Based on the version in EditorHelp - if (p_bbcode.empty()) + if (p_bbcode.empty()) { return String(); + } DocData *doc = EditorHelp::get_doc_data(); @@ -204,8 +200,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf while (pos < bbcode.length()) { int brk_pos = bbcode.find("[", pos); - if (brk_pos < 0) + if (brk_pos < 0) { brk_pos = bbcode.length(); + } if (brk_pos > pos) { String text = bbcode.substr(pos, brk_pos - pos); @@ -214,19 +211,22 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf } else { Vector<String> lines = text.split("\n"); for (int i = 0; i < lines.size(); i++) { - if (i != 0) + if (i != 0) { xml_output.append("<para>"); + } xml_output.append(lines[i].xml_escape()); - if (i != lines.size() - 1) + if (i != lines.size() - 1) { xml_output.append("</para>\n"); + } } } } - if (brk_pos == bbcode.length()) + if (brk_pos == bbcode.length()) { break; // nothing else to add + } int brk_end = bbcode.find("]", brk_pos + 1); @@ -237,13 +237,15 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf } else { Vector<String> lines = text.split("\n"); for (int i = 0; i < lines.size(); i++) { - if (i != 0) + if (i != 0) { xml_output.append("<para>"); + } xml_output.append(lines[i].xml_escape()); - if (i != lines.size() - 1) + if (i != lines.size() - 1) { xml_output.append("</para>\n"); + } } } @@ -416,8 +418,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf for (const List<EnumInterface>::Element *E = global_enums.front(); E; E = E->next()) { target_ienum = &E->get(); target_iconst = find_constant_by_name(target_name, target_ienum->constants); - if (target_iconst) + if (target_iconst) { break; + } } if (target_iconst) { @@ -454,8 +457,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf for (const List<EnumInterface>::Element *E = target_itype->enums.front(); E; E = E->next()) { target_ienum = &E->get(); target_iconst = find_constant_by_name(target_name, target_ienum->constants); - if (target_iconst) + if (target_iconst) { break; + } } if (target_iconst) { @@ -566,8 +570,12 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf code_tag = true; pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "kbd") { + // keyboard combinations are not supported in xml comments + pos = brk_end + 1; + tag_stack.push_front(tag); } else if (tag == "center") { - // center is alignment not supported in xml comments + // center alignment is not supported in xml comments pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "br") { @@ -583,8 +591,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf tag_stack.push_front(tag); } else if (tag == "url") { int end = bbcode.find("[", brk_end); - if (end == -1) + if (end == -1) { end = bbcode.length(); + } String url = bbcode.substr(brk_end + 1, end - brk_end - 1); xml_output.append("<a href=\""); xml_output.append(url); @@ -603,8 +612,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf tag_stack.push_front("url"); } else if (tag == "img") { int end = bbcode.find("[", brk_end); - if (end == -1) + if (end == -1) { end = bbcode.length(); + } String image = bbcode.substr(brk_end + 1, end - brk_end - 1); // Not supported. Just append the bbcode. @@ -634,15 +644,15 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf } int BindingsGenerator::_determine_enum_prefix(const EnumInterface &p_ienum) { - CRASH_COND(p_ienum.constants.empty()); const ConstantInterface &front_iconstant = p_ienum.constants.front()->get(); Vector<String> front_parts = front_iconstant.name.split("_", /* p_allow_empty: */ true); int candidate_len = front_parts.size() - 1; - if (candidate_len == 0) + if (candidate_len == 0) { return 0; + } for (const List<ConstantInterface>::Element *E = p_ienum.constants.front()->next(); E; E = E->next()) { const ConstantInterface &iconstant = E->get(); @@ -654,21 +664,22 @@ int BindingsGenerator::_determine_enum_prefix(const EnumInterface &p_ienum) { if (front_parts[i] != parts[i]) { // HARDCODED: Some Flag enums have the prefix 'FLAG_' for everything except 'FLAGS_DEFAULT' (same for 'METHOD_FLAG_' and'METHOD_FLAGS_DEFAULT'). bool hardcoded_exc = (i == candidate_len - 1 && ((front_parts[i] == "FLAGS" && parts[i] == "FLAG") || (front_parts[i] == "FLAG" && parts[i] == "FLAGS"))); - if (!hardcoded_exc) + if (!hardcoded_exc) { break; + } } } candidate_len = i; - if (candidate_len == 0) + if (candidate_len == 0) { return 0; + } } return candidate_len; } void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumInterface &p_ienum, int p_prefix_length) { - if (p_prefix_length > 0) { for (List<ConstantInterface>::Element *E = p_ienum.constants.front(); E; E = E->next()) { int curr_prefix_length = p_prefix_length; @@ -679,22 +690,25 @@ void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumI Vector<String> parts = constant_name.split("_", /* p_allow_empty: */ true); - if (parts.size() <= curr_prefix_length) + if (parts.size() <= curr_prefix_length) { continue; + } if (parts[curr_prefix_length][0] >= '0' && parts[curr_prefix_length][0] <= '9') { // The name of enum constants may begin with a numeric digit when strip from the enum prefix, // so we make the prefix for this constant one word shorter in those cases. for (curr_prefix_length = curr_prefix_length - 1; curr_prefix_length > 0; curr_prefix_length--) { - if (parts[curr_prefix_length][0] < '0' || parts[curr_prefix_length][0] > '9') + if (parts[curr_prefix_length][0] < '0' || parts[curr_prefix_length][0] > '9') { break; + } } } constant_name = ""; for (int i = curr_prefix_length; i < parts.size(); i++) { - if (i > curr_prefix_length) + if (i > curr_prefix_length) { constant_name += "_"; + } constant_name += parts[i]; } @@ -704,12 +718,12 @@ void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumI } void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) { - for (const List<MethodInterface>::Element *E = p_itype.methods.front(); E; E = E->next()) { const MethodInterface &imethod = E->get(); - if (imethod.is_virtual) + if (imethod.is_virtual) { continue; + } const TypeInterface *return_type = _get_type_or_placeholder(imethod.return_type); @@ -758,8 +772,9 @@ void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) { List<InternalCall>::Element *match = method_icalls.find(im_icall); if (match) { - if (p_itype.api_type != ClassDB::API_EDITOR) + if (p_itype.api_type != ClassDB::API_EDITOR) { match->get().editor_only = false; + } method_icalls_map.insert(&E->get(), &match->get()); } else { List<InternalCall>::Element *added = method_icalls.push_back(im_icall); @@ -769,7 +784,6 @@ void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) { } void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { - // Constants (in partial GD class) p_output.append("\n#pragma warning disable CS1591 // Disable warning: " @@ -805,8 +819,9 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { p_output.append(";"); } - if (!global_constants.empty()) + if (!global_constants.empty()) { p_output.append("\n"); + } p_output.append(INDENT1 CLOSE_BLOCK); // end of GD class @@ -868,8 +883,9 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { p_output.append(INDENT1 CLOSE_BLOCK); - if (enum_in_static_class) + if (enum_in_static_class) { p_output.append(INDENT1 CLOSE_BLOCK); + } } p_output.append(CLOSE_BLOCK); // end of namespace @@ -878,7 +894,6 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { } Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { - ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED); DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); @@ -904,8 +919,9 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { _generate_global_constants(constants_source); String output_file = path::join(base_gen_dir, BINDINGS_GLOBAL_SCOPE_CLASS "_constants.cs"); Error save_err = _save_file(output_file, constants_source); - if (save_err != OK) + if (save_err != OK) { return save_err; + } compile_items.push_back(output_file); } @@ -913,17 +929,20 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) { const TypeInterface &itype = E.get(); - if (itype.api_type == ClassDB::API_EDITOR) + if (itype.api_type == ClassDB::API_EDITOR) { continue; + } String output_file = path::join(godot_objects_gen_dir, itype.proxy_name + ".cs"); Error err = _generate_cs_type(itype, output_file); - if (err == ERR_SKIP) + if (err == ERR_SKIP) { continue; + } - if (err != OK) + if (err != OK) { return err; + } compile_items.push_back(output_file); } @@ -954,10 +973,12 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { cs_icalls_content.append(m_icall.im_sig + ");\n"); \ } - for (const List<InternalCall>::Element *E = core_custom_icalls.front(); E; E = E->next()) + for (const List<InternalCall>::Element *E = core_custom_icalls.front(); E; E = E->next()) { ADD_INTERNAL_CALL(E->get()); - for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next()) + } + for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next()) { ADD_INTERNAL_CALL(E->get()); + } #undef ADD_INTERNAL_CALL @@ -966,8 +987,9 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS ".cs"); Error err = _save_file(internal_methods_file, cs_icalls_content); - if (err != OK) + if (err != OK) { return err; + } compile_items.push_back(internal_methods_file); @@ -986,14 +1008,14 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) { String includes_props_file = path::join(base_gen_dir, "GeneratedIncludes.props"); err = _save_file(includes_props_file, includes_props_content); - if (err != OK) + if (err != OK) { return err; + } return OK; } Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { - ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED); DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); @@ -1016,17 +1038,20 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) { const TypeInterface &itype = E.get(); - if (itype.api_type != ClassDB::API_EDITOR) + if (itype.api_type != ClassDB::API_EDITOR) { continue; + } String output_file = path::join(godot_objects_gen_dir, itype.proxy_name + ".cs"); Error err = _generate_cs_type(itype, output_file); - if (err == ERR_SKIP) + if (err == ERR_SKIP) { continue; + } - if (err != OK) + if (err != OK) { return err; + } compile_items.push_back(output_file); } @@ -1056,10 +1081,12 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { cs_icalls_content.append(m_icall.im_sig + ");\n"); \ } - for (const List<InternalCall>::Element *E = editor_custom_icalls.front(); E; E = E->next()) + for (const List<InternalCall>::Element *E = editor_custom_icalls.front(); E; E = E->next()) { ADD_INTERNAL_CALL(E->get()); - for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next()) + } + for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next()) { ADD_INTERNAL_CALL(E->get()); + } #undef ADD_INTERNAL_CALL @@ -1068,8 +1095,9 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS_EDITOR ".cs"); Error err = _save_file(internal_methods_file, cs_icalls_content); - if (err != OK) + if (err != OK) { return err; + } compile_items.push_back(internal_methods_file); @@ -1088,14 +1116,14 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) { String includes_props_file = path::join(base_gen_dir, "GeneratedIncludes.props"); err = _save_file(includes_props_file, includes_props_content); - if (err != OK) + if (err != OK) { return err; + } return OK; } Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { - ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED); String output_dir = path::abspath(path::realpath(p_output_dir)); @@ -1143,7 +1171,6 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { // - Csc warning e.g.: // ObjectType/LineEdit.cs(140,38): warning CS0108: 'LineEdit.FocusMode' hides inherited member 'Control.FocusMode'. Use the new keyword if hiding was intended. Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const String &p_output_file) { - CRASH_COND(!itype.is_object_type); bool is_derived_type = itype.base_name != StringName(); @@ -1218,93 +1245,89 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output.append(INDENT1 "{"); - if (class_doc) { + // Add constants - // Add constants - - for (const List<ConstantInterface>::Element *E = itype.constants.front(); E; E = E->next()) { - const ConstantInterface &iconstant = E->get(); - - if (iconstant.const_doc && iconstant.const_doc->description.size()) { - String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype); - Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); + for (const List<ConstantInterface>::Element *E = itype.constants.front(); E; E = E->next()) { + const ConstantInterface &iconstant = E->get(); - if (summary_lines.size()) { - output.append(MEMBER_BEGIN "/// <summary>\n"); + if (iconstant.const_doc && iconstant.const_doc->description.size()) { + String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype); + Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); - for (int i = 0; i < summary_lines.size(); i++) { - output.append(INDENT2 "/// "); - output.append(summary_lines[i]); - output.append("\n"); - } + if (summary_lines.size()) { + output.append(MEMBER_BEGIN "/// <summary>\n"); - output.append(INDENT2 "/// </summary>"); + for (int i = 0; i < summary_lines.size(); i++) { + output.append(INDENT2 "/// "); + output.append(summary_lines[i]); + output.append("\n"); } - } - output.append(MEMBER_BEGIN "public const int "); - output.append(iconstant.proxy_name); - output.append(" = "); - output.append(itos(iconstant.value)); - output.append(";"); + output.append(INDENT2 "/// </summary>"); + } } - if (itype.constants.size()) - output.append("\n"); + output.append(MEMBER_BEGIN "public const int "); + output.append(iconstant.proxy_name); + output.append(" = "); + output.append(itos(iconstant.value)); + output.append(";"); + } - // Add enums + if (itype.constants.size()) { + output.append("\n"); + } - for (const List<EnumInterface>::Element *E = itype.enums.front(); E; E = E->next()) { - const EnumInterface &ienum = E->get(); + // Add enums - ERR_FAIL_COND_V(ienum.constants.empty(), ERR_BUG); + for (const List<EnumInterface>::Element *E = itype.enums.front(); E; E = E->next()) { + const EnumInterface &ienum = E->get(); - output.append(MEMBER_BEGIN "public enum "); - output.append(ienum.cname.operator String()); - output.append(MEMBER_BEGIN OPEN_BLOCK); + ERR_FAIL_COND_V(ienum.constants.empty(), ERR_BUG); - for (const List<ConstantInterface>::Element *F = ienum.constants.front(); F; F = F->next()) { - const ConstantInterface &iconstant = F->get(); + output.append(MEMBER_BEGIN "public enum "); + output.append(ienum.cname.operator String()); + output.append(MEMBER_BEGIN OPEN_BLOCK); - if (iconstant.const_doc && iconstant.const_doc->description.size()) { - String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype); - Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); + for (const List<ConstantInterface>::Element *F = ienum.constants.front(); F; F = F->next()) { + const ConstantInterface &iconstant = F->get(); - if (summary_lines.size()) { - output.append(INDENT3 "/// <summary>\n"); + if (iconstant.const_doc && iconstant.const_doc->description.size()) { + String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype); + Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); - for (int i = 0; i < summary_lines.size(); i++) { - output.append(INDENT3 "/// "); - output.append(summary_lines[i]); - output.append("\n"); - } + if (summary_lines.size()) { + output.append(INDENT3 "/// <summary>\n"); - output.append(INDENT3 "/// </summary>\n"); + for (int i = 0; i < summary_lines.size(); i++) { + output.append(INDENT3 "/// "); + output.append(summary_lines[i]); + output.append("\n"); } - } - output.append(INDENT3); - output.append(iconstant.proxy_name); - output.append(" = "); - output.append(itos(iconstant.value)); - output.append(F != ienum.constants.back() ? ",\n" : "\n"); + output.append(INDENT3 "/// </summary>\n"); + } } - output.append(INDENT2 CLOSE_BLOCK); + output.append(INDENT3); + output.append(iconstant.proxy_name); + output.append(" = "); + output.append(itos(iconstant.value)); + output.append(F != ienum.constants.back() ? ",\n" : "\n"); } - // Add properties - - for (const List<PropertyInterface>::Element *E = itype.properties.front(); E; E = E->next()) { - const PropertyInterface &iprop = E->get(); - Error prop_err = _generate_cs_property(itype, iprop, output); - ERR_FAIL_COND_V_MSG(prop_err != OK, prop_err, - "Failed to generate property '" + iprop.cname.operator String() + - "' for class '" + itype.name + "'."); - } + output.append(INDENT2 CLOSE_BLOCK); } - // TODO: BINDINGS_NATIVE_NAME_FIELD should be StringName, once we support it in C# + // Add properties + + for (const List<PropertyInterface>::Element *E = itype.properties.front(); E; E = E->next()) { + const PropertyInterface &iprop = E->get(); + Error prop_err = _generate_cs_property(itype, iprop, output); + ERR_FAIL_COND_V_MSG(prop_err != OK, prop_err, + "Failed to generate property '" + iprop.cname.operator String() + + "' for class '" + itype.name + "'."); + } if (itype.is_singleton) { // Add the type name and the singleton pointer as static fields @@ -1377,15 +1400,17 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str if (itype.is_singleton) { InternalCall singleton_icall = InternalCall(itype.api_type, ICALL_PREFIX + itype.name + SINGLETON_ICALL_SUFFIX, "IntPtr"); - if (!find_icall_by_name(singleton_icall.name, custom_icalls)) + if (!find_icall_by_name(singleton_icall.name, custom_icalls)) { custom_icalls.push_back(singleton_icall); + } } if (is_derived_type && itype.is_instantiable) { InternalCall ctor_icall = InternalCall(itype.api_type, ctor_method, "IntPtr", itype.proxy_name + " obj"); - if (!find_icall_by_name(ctor_icall.name, custom_icalls)) + if (!find_icall_by_name(ctor_icall.name, custom_icalls)) { custom_icalls.push_back(ctor_icall); + } } output.append(INDENT1 CLOSE_BLOCK /* class */ @@ -1399,7 +1424,6 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str } Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInterface &p_itype, const PropertyInterface &p_iprop, StringBuilder &p_output) { - const MethodInterface *setter = p_itype.find_method_by_name(p_iprop.setter); // Search it in base types too @@ -1452,6 +1476,9 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte const TypeInterface *prop_itype = _get_type_or_null(proptype_name); ERR_FAIL_NULL_V(prop_itype, ERR_BUG); // Property type not found + ERR_FAIL_COND_V_MSG(prop_itype->is_singleton, ERR_BUG, + "Property type is a singleton: '" + p_itype.name + "." + String(p_iprop.cname) + "'."); + if (p_iprop.prop_doc && p_iprop.prop_doc->description.size()) { String xml_summary = bbcode_to_xml(fix_doc_description(p_iprop.prop_doc->description), &p_itype); Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); @@ -1471,8 +1498,9 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte p_output.append(MEMBER_BEGIN "public "); - if (p_itype.is_singleton) + if (p_itype.is_singleton) { p_output.append("static "); + } p_output.append(prop_itype->cs_type); p_output.append(" "); @@ -1542,9 +1570,11 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte } Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, int &p_method_bind_count, StringBuilder &p_output) { - const TypeInterface *return_type = _get_type_or_placeholder(p_imethod.return_type); + ERR_FAIL_COND_V_MSG(return_type->is_singleton, ERR_BUG, + "Method return type is a singleton: '" + p_itype.name + "." + p_imethod.name + "'."); + String method_bind_field = "__method_bind_" + itos(p_method_bind_count); String arguments_sig; @@ -1560,29 +1590,41 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf const ArgumentInterface &iarg = F->get(); const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type); + ERR_FAIL_COND_V_MSG(arg_type->is_singleton, ERR_BUG, + "Argument type is a singleton: '" + iarg.name + "' of method '" + p_itype.name + "." + p_imethod.name + "'."); + + if (iarg.default_argument.size()) { + CRASH_COND_MSG(!_arg_default_value_is_assignable_to_type(iarg.def_param_value, *arg_type), + "Invalid default value for parameter '" + iarg.name + "' of method '" + p_itype.name + "." + p_imethod.name + "'."); + } + // Add the current arguments to the signature // If the argument has a default value which is not a constant, we will make it Nullable { - if (F != p_imethod.arguments.front()) + if (F != p_imethod.arguments.front()) { arguments_sig += ", "; + } - if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) + if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { arguments_sig += "Nullable<"; + } arguments_sig += arg_type->cs_type; - if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) + if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { arguments_sig += "> "; - else + } else { arguments_sig += " "; + } arguments_sig += iarg.name; if (iarg.default_argument.size()) { - if (iarg.def_param_mode != ArgumentInterface::CONSTANT) + if (iarg.def_param_mode != ArgumentInterface::CONSTANT) { arguments_sig += " = null"; - else + } else { arguments_sig += " = " + sformat(iarg.default_argument, arg_type->cs_type); + } } } @@ -1600,17 +1642,19 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf cs_in_statements += " = "; cs_in_statements += iarg.name; - if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) + if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { cs_in_statements += ".HasValue ? "; - else + } else { cs_in_statements += " != null ? "; + } cs_in_statements += iarg.name; - if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) + if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { cs_in_statements += ".Value : "; - else + } else { cs_in_statements += " : "; + } String def_arg = sformat(iarg.default_argument, arg_type->cs_type); @@ -1660,14 +1704,19 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf } if (!p_imethod.is_internal) { + // TODO: This alone adds ~0.2 MB of bloat to the core API assembly. It would be + // better to generate a table in the C++ glue instead. That way the strings wouldn't + // add that much extra bloat as they're already used in engine code. Also, it would + // probably be much faster than looking up the attributes when fetching methods. p_output.append(MEMBER_BEGIN "[GodotMethod(\""); p_output.append(p_imethod.name); p_output.append("\")]"); } if (p_imethod.is_deprecated) { - if (p_imethod.deprecation_message.empty()) + if (p_imethod.deprecation_message.empty()) { WARN_PRINT("An empty deprecation message is discouraged. Method: '" + p_imethod.proxy_name + "'."); + } p_output.append(MEMBER_BEGIN "[Obsolete(\""); p_output.append(p_imethod.deprecation_message); @@ -1727,8 +1776,9 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf im_call += "."; im_call += im_icall->name; - if (p_imethod.arguments.size()) + if (p_imethod.arguments.size()) { p_output.append(cs_in_statements); + } if (return_type->cname == name_cache.type_void) { p_output.append(im_call + "(" + icall_params + ");\n"); @@ -1755,10 +1805,14 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf const ArgumentInterface &iarg = F->get(); const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type); + ERR_FAIL_COND_V_MSG(arg_type->is_singleton, ERR_BUG, + "Argument type is a singleton: '" + iarg.name + "' of signal" + p_itype.name + "." + p_isignal.name + "'."); + // Add the current arguments to the signature - if (F != p_isignal.arguments.front()) + if (F != p_isignal.arguments.front()) { arguments_sig += ", "; + } arguments_sig += arg_type->cs_type; arguments_sig += " "; @@ -1785,8 +1839,9 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf } if (p_isignal.is_deprecated) { - if (p_isignal.deprecation_message.empty()) + if (p_isignal.deprecation_message.empty()) { WARN_PRINT("An empty deprecation message is discouraged. Signal: '" + p_isignal.proxy_name + "'."); + } p_output.append(MEMBER_BEGIN "[Obsolete(\""); p_output.append(p_isignal.deprecation_message); @@ -1817,8 +1872,9 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf // Generate event p_output.append(MEMBER_BEGIN "[Signal]" MEMBER_BEGIN "public "); - if (p_itype.is_singleton) + if (p_itype.is_singleton) { p_output.append("static "); + } p_output.append("event "); p_output.append(delegate_name); @@ -1826,18 +1882,20 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(p_isignal.proxy_name); p_output.append("\n" OPEN_BLOCK_L2); - if (p_itype.is_singleton) + if (p_itype.is_singleton) { p_output.append("add => Singleton.Connect(__signal_name_"); - else + } else { p_output.append("add => Connect(__signal_name_"); + } p_output.append(p_isignal.name); p_output.append(", new Callable(value));\n"); - if (p_itype.is_singleton) + if (p_itype.is_singleton) { p_output.append(INDENT3 "remove => Singleton.Disconnect(__signal_name_"); - else + } else { p_output.append(INDENT3 "remove => Disconnect(__signal_name_"); + } p_output.append(p_isignal.name); p_output.append(", new Callable(value));\n"); @@ -1848,7 +1906,6 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf } Error BindingsGenerator::generate_glue(const String &p_output_dir) { - ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED); bool dir_exists = DirAccess::exists(p_output_dir); @@ -1872,7 +1929,6 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) { CRASH_COND(itype.cname != name_cache.type_Object); CRASH_COND(!itype.is_instantiable); CRASH_COND(itype.api_type != ClassDB::API_CORE); - CRASH_COND(itype.is_reference); CRASH_COND(itype.is_singleton); } @@ -1893,8 +1949,9 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) { String singleton_icall_name = ICALL_PREFIX + itype.name + SINGLETON_ICALL_SUFFIX; InternalCall singleton_icall = InternalCall(itype.api_type, singleton_icall_name, "IntPtr"); - if (!find_icall_by_name(singleton_icall.name, custom_icalls)) + if (!find_icall_by_name(singleton_icall.name, custom_icalls)) { custom_icalls.push_back(singleton_icall); + } output.append("Object* "); output.append(singleton_icall_name); @@ -1906,8 +1963,9 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) { if (is_derived_type && itype.is_instantiable) { InternalCall ctor_icall = InternalCall(itype.api_type, ctor_method, "IntPtr", itype.proxy_name + " obj"); - if (!find_icall_by_name(ctor_icall.name, custom_icalls)) + if (!find_icall_by_name(ctor_icall.name, custom_icalls)) { custom_icalls.push_back(ctor_icall); + } output.append("Object* "); output.append(ctor_method); @@ -1953,7 +2011,6 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) { bool tools_sequence = false; for (const List<InternalCall>::Element *E = core_custom_icalls.front(); E; E = E->next()) { - if (tools_sequence) { if (!E->get().editor_only) { tools_sequence = false; @@ -2007,8 +2064,9 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) { output.append("\n#endif // MONO_GLUE_ENABLED\n"); Error save_err = _save_file(path::join(p_output_dir, "mono_glue.gen.cpp"), output); - if (save_err != OK) + if (save_err != OK) { return save_err; + } OS::get_singleton()->print("Mono glue generated successfully\n"); @@ -2020,7 +2078,6 @@ uint32_t BindingsGenerator::get_version() { } Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p_content) { - FileAccessRef file = FileAccess::open(p_path, FileAccess::WRITE); ERR_FAIL_COND_V_MSG(!file, ERR_FILE_CANT_WRITE, "Cannot open file: '" + p_path + "'."); @@ -2032,9 +2089,9 @@ Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p } Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, StringBuilder &p_output) { - - if (p_imethod.is_virtual) + if (p_imethod.is_virtual) { return OK; // Ignore + } bool ret_void = p_imethod.return_type.cname == name_cache.type_void; @@ -2062,10 +2119,12 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte c_in_statements += sformat(", &%s_in);\n", c_param_name); } } else { - if (i > 0) + if (i > 0) { c_args_var_content += ", "; - if (arg_type->c_in.size()) + } + if (arg_type->c_in.size()) { c_in_statements += sformat(arg_type->c_in, arg_type->c_type, c_param_name); + } c_args_var_content += sformat(arg_type->c_arg_in, c_param_name); } @@ -2095,8 +2154,9 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte if (!generated_icall_funcs.find(im_icall)) { generated_icall_funcs.push_back(im_icall); - if (im_icall->editor_only) + if (im_icall->editor_only) { p_output.append("#ifdef TOOLS_ENABLED\n"); + } // Generate icall function @@ -2135,7 +2195,7 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte if (return_type->ret_as_byref_arg) { p_output.append("\tif (" CS_PARAM_INSTANCE " == nullptr) { *arg_ret = "); p_output.append(fail_ret); - p_output.append("; ERR_FAIL_MSG(\"Parameter ' arg_ret ' is null.\"); }\n"); + p_output.append("; ERR_FAIL_MSG(\"Parameter ' " CS_PARAM_INSTANCE " ' is null.\"); }\n"); } else { p_output.append("\tERR_FAIL_NULL_V(" CS_PARAM_INSTANCE ", "); p_output.append(fail_ret); @@ -2214,30 +2274,33 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte p_output.append(CLOSE_BLOCK "\n"); - if (im_icall->editor_only) + if (im_icall->editor_only) { p_output.append("#endif // TOOLS_ENABLED\n"); + } } return OK; } const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_null(const TypeReference &p_typeref) { - const Map<StringName, TypeInterface>::Element *builtin_type_match = builtin_types.find(p_typeref.cname); - if (builtin_type_match) + if (builtin_type_match) { return &builtin_type_match->get(); + } const OrderedHashMap<StringName, TypeInterface>::Element obj_type_match = obj_types.find(p_typeref.cname); - if (obj_type_match) + if (obj_type_match) { return &obj_type_match.get(); + } if (p_typeref.is_enum) { const Map<StringName, TypeInterface>::Element *enum_match = enum_types.find(p_typeref.cname); - if (enum_match) + if (enum_match) { return &enum_match->get(); + } // Enum not found. Most likely because none of its constants were bound, so it's empty. That's fine. Use int instead. const Map<StringName, TypeInterface>::Element *int_match = builtin_types.find(name_cache.type_int); @@ -2249,18 +2312,19 @@ const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_null(con } const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_placeholder(const TypeReference &p_typeref) { - const TypeInterface *found = _get_type_or_null(p_typeref); - if (found) + if (found) { return found; + } ERR_PRINT(String() + "Type not found. Creating placeholder: '" + p_typeref.cname.operator String() + "'."); const Map<StringName, TypeInterface>::Element *match = placeholder_types.find(p_typeref.cname); - if (match) + if (match) { return &match->get(); + } TypeInterface placeholder; TypeInterface::create_placeholder_type(placeholder, p_typeref.cname); @@ -2269,7 +2333,6 @@ const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_placehol } StringName BindingsGenerator::_get_int_type_name_from_meta(GodotTypeInfo::Metadata p_meta) { - switch (p_meta) { case GodotTypeInfo::METADATA_INT_IS_INT8: return "sbyte"; @@ -2302,7 +2365,6 @@ StringName BindingsGenerator::_get_int_type_name_from_meta(GodotTypeInfo::Metada } StringName BindingsGenerator::_get_float_type_name_from_meta(GodotTypeInfo::Metadata p_meta) { - switch (p_meta) { case GodotTypeInfo::METADATA_REAL_IS_FLOAT: return "float"; @@ -2320,8 +2382,85 @@ StringName BindingsGenerator::_get_float_type_name_from_meta(GodotTypeInfo::Meta } } -bool BindingsGenerator::_populate_object_type_interfaces() { +bool BindingsGenerator::_arg_default_value_is_assignable_to_type(const Variant &p_val, const TypeInterface &p_arg_type) { + if (p_arg_type.name == name_cache.type_Variant) { + // Variant can take anything + return true; + } + + switch (p_val.get_type()) { + case Variant::NIL: + return p_arg_type.is_object_type || + name_cache.is_nullable_type(p_arg_type.name); + case Variant::BOOL: + return p_arg_type.name == name_cache.type_bool; + case Variant::INT: + return p_arg_type.name == name_cache.type_sbyte || + p_arg_type.name == name_cache.type_short || + p_arg_type.name == name_cache.type_int || + p_arg_type.name == name_cache.type_byte || + p_arg_type.name == name_cache.type_ushort || + p_arg_type.name == name_cache.type_uint || + p_arg_type.name == name_cache.type_long || + p_arg_type.name == name_cache.type_ulong || + p_arg_type.name == name_cache.type_float || + p_arg_type.name == name_cache.type_double || + p_arg_type.is_enum; + case Variant::FLOAT: + return p_arg_type.name == name_cache.type_float || + p_arg_type.name == name_cache.type_double; + case Variant::STRING: + case Variant::STRING_NAME: + return p_arg_type.name == name_cache.type_String || + p_arg_type.name == name_cache.type_StringName || + p_arg_type.name == name_cache.type_NodePath; + case Variant::NODE_PATH: + return p_arg_type.name == name_cache.type_NodePath; + case Variant::TRANSFORM: + case Variant::TRANSFORM2D: + case Variant::BASIS: + case Variant::QUAT: + case Variant::PLANE: + case Variant::AABB: + case Variant::COLOR: + case Variant::VECTOR2: + case Variant::RECT2: + case Variant::VECTOR3: + case Variant::_RID: + case Variant::ARRAY: + case Variant::DICTIONARY: + case Variant::PACKED_BYTE_ARRAY: + case Variant::PACKED_INT32_ARRAY: + case Variant::PACKED_INT64_ARRAY: + case Variant::PACKED_FLOAT32_ARRAY: + case Variant::PACKED_FLOAT64_ARRAY: + case Variant::PACKED_STRING_ARRAY: + case Variant::PACKED_VECTOR2_ARRAY: + case Variant::PACKED_VECTOR3_ARRAY: + case Variant::PACKED_COLOR_ARRAY: + case Variant::CALLABLE: + case Variant::SIGNAL: + return p_arg_type.name == Variant::get_type_name(p_val.get_type()); + case Variant::OBJECT: + return p_arg_type.is_object_type; + case Variant::VECTOR2I: + return p_arg_type.name == name_cache.type_Vector2 || + p_arg_type.name == Variant::get_type_name(p_val.get_type()); + case Variant::RECT2I: + return p_arg_type.name == name_cache.type_Rect2 || + p_arg_type.name == Variant::get_type_name(p_val.get_type()); + case Variant::VECTOR3I: + return p_arg_type.name == name_cache.type_Vector3 || + p_arg_type.name == Variant::get_type_name(p_val.get_type()); + default: + CRASH_NOW_MSG("Unexpected Variant type: " + itos(p_val.get_type())); + break; + } + return false; +} + +bool BindingsGenerator::_populate_object_type_interfaces() { obj_types.clear(); List<StringName> class_list; @@ -2383,22 +2522,30 @@ bool BindingsGenerator::_populate_object_type_interfaces() { for (const List<PropertyInfo>::Element *E = property_list.front(); E; E = E->next()) { const PropertyInfo &property = E->get(); - if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY) + if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY) { continue; + } + + if (property.name.find("/") >= 0) { + // Ignore properties with '/' (slash) in the name. These are only meant for use in the inspector. + continue; + } PropertyInterface iprop; iprop.cname = property.name; iprop.setter = ClassDB::get_property_setter(type_cname, iprop.cname); iprop.getter = ClassDB::get_property_getter(type_cname, iprop.cname); - if (iprop.setter != StringName()) + if (iprop.setter != StringName()) { accessor_methods[iprop.setter] = iprop.cname; - if (iprop.getter != StringName()) + } + if (iprop.getter != StringName()) { accessor_methods[iprop.getter] = iprop.cname; + } bool valid = false; iprop.index = ClassDB::get_property_index(type_cname, iprop.cname, &valid); - ERR_FAIL_COND_V(!valid, false); + ERR_FAIL_COND_V_MSG(!valid, false, "Invalid property: '" + itype.name + "." + String(iprop.cname) + "'."); iprop.proxy_name = escape_csharp_keyword(snake_to_pascal_case(iprop.cname)); @@ -2410,8 +2557,6 @@ bool BindingsGenerator::_populate_object_type_interfaces() { iprop.proxy_name += "_"; } - iprop.proxy_name = iprop.proxy_name.replace("/", "__"); // Some members have a slash... - iprop.prop_doc = nullptr; for (int i = 0; i < itype.class_doc->properties.size(); i++) { @@ -2440,20 +2585,23 @@ bool BindingsGenerator::_populate_object_type_interfaces() { int argc = method_info.arguments.size(); - if (method_info.name.empty()) + if (method_info.name.empty()) { continue; + } String cname = method_info.name; - if (blacklisted_methods.find(itype.cname) && blacklisted_methods[itype.cname].find(cname)) + if (blacklisted_methods.find(itype.cname) && blacklisted_methods[itype.cname].find(cname)) { continue; + } MethodInterface imethod; imethod.name = method_info.name; imethod.cname = cname; - if (method_info.flags & METHOD_FLAG_VIRTUAL) + if (method_info.flags & METHOD_FLAG_VIRTUAL) { imethod.is_virtual = true; + } PropertyInfo return_info = method_info.return_val; @@ -2475,9 +2623,8 @@ bool BindingsGenerator::_populate_object_type_interfaces() { // We assume the return type is void. imethod.return_type.cname = name_cache.type_void; - // Actually, more methods like this may be added in the future, - // which could actually will return something different. - // Let's put this to notify us if that ever happens. + // Actually, more methods like this may be added in the future, which could return + // something different. Let's put this check to notify us if that ever happens. if (itype.cname != name_cache.type_Object || imethod.name != "free") { WARN_PRINT("Notification: New unexpected virtual non-overridable method found." " We only expected Object.free, but found '" + @@ -2488,13 +2635,12 @@ bool BindingsGenerator::_populate_object_type_interfaces() { imethod.return_type.is_enum = true; } else if (return_info.class_name != StringName()) { imethod.return_type.cname = return_info.class_name; - if (!imethod.is_virtual && ClassDB::is_parent_class(return_info.class_name, name_cache.type_Reference) && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE) { - /* clang-format off */ - ERR_PRINT("Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'." - " Are you returning a reference type by pointer? Method: '" + itype.name + "." + imethod.name + "'."); - /* clang-format on */ - ERR_FAIL_V(false); - } + + bool bad_reference_hint = !imethod.is_virtual && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE && + ClassDB::is_parent_class(return_info.class_name, name_cache.type_Reference); + ERR_FAIL_COND_V_MSG(bad_reference_hint, false, + String() + "Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'." + + " Are you returning a reference type by pointer? Method: '" + itype.name + "." + imethod.name + "'."); } else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) { imethod.return_type.cname = return_info.hint_string; } else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) { @@ -2585,6 +2731,10 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } } + ERR_FAIL_COND_V_MSG(itype.find_property_by_name(imethod.cname), false, + "Method name conflicts with property: '" + itype.name + "." + imethod.name + "'."); + + // Classes starting with an underscore are ignored unless they're used as a property setter or getter if (!imethod.is_virtual && imethod.name[0] == '_') { for (const List<PropertyInterface>::Element *F = itype.properties.front(); F; F = F->next()) { const PropertyInterface &iprop = F->get(); @@ -2764,8 +2914,8 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, ArgumentInterface &r_iarg) { - - r_iarg.default_argument = p_val; + r_iarg.def_param_value = p_val; + r_iarg.default_argument = p_val.operator String(); switch (p_val.get_type()) { case Variant::NIL: @@ -2798,8 +2948,9 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar } break; case Variant::TRANSFORM: - if (p_val.operator Transform() == Transform()) + if (p_val.operator Transform() == Transform()) { r_iarg.default_argument.clear(); + } r_iarg.default_argument = "new %s(" + r_iarg.default_argument + ")"; r_iarg.def_param_mode = ArgumentInterface::NULLABLE_VAL; break; @@ -2865,14 +3016,14 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar break; } - if (r_iarg.def_param_mode == ArgumentInterface::CONSTANT && r_iarg.type.cname == name_cache.type_Variant && r_iarg.default_argument != "null") + if (r_iarg.def_param_mode == ArgumentInterface::CONSTANT && r_iarg.type.cname == name_cache.type_Variant && r_iarg.default_argument != "null") { r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF; + } return true; } void BindingsGenerator::_populate_builtin_type_interfaces() { - builtin_types.clear(); TypeInterface itype; @@ -3248,7 +3399,6 @@ void BindingsGenerator::_populate_builtin_type_interfaces() { } void BindingsGenerator::_populate_global_constants() { - int global_constants_count = GlobalConstants::get_global_constant_count(); if (global_constants_count > 0) { @@ -3259,7 +3409,6 @@ void BindingsGenerator::_populate_global_constants() { const DocData::ClassDoc &global_scope_doc = match->value(); for (int i = 0; i < global_constants_count; i++) { - String constant_name = GlobalConstants::get_global_constant_name(i); const DocData::ConstantDoc *const_doc = nullptr; @@ -3338,14 +3487,12 @@ void BindingsGenerator::_populate_global_constants() { } void BindingsGenerator::_initialize_blacklisted_methods() { - blacklisted_methods["Object"].push_back("to_string"); // there is already ToString blacklisted_methods["Object"].push_back("_to_string"); // override ToString instead blacklisted_methods["Object"].push_back("_init"); // never called in C# (TODO: implement it) } void BindingsGenerator::_log(const char *p_format, ...) { - if (log_print_enabled) { va_list list; @@ -3356,7 +3503,6 @@ void BindingsGenerator::_log(const char *p_format, ...) { } void BindingsGenerator::_initialize() { - initialized = false; EditorHelp::generate_doc(); @@ -3377,14 +3523,14 @@ void BindingsGenerator::_initialize() { core_custom_icalls.clear(); editor_custom_icalls.clear(); - for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) + for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) { _generate_method_icalls(E.get()); + } initialized = true; } void BindingsGenerator::handle_cmdline_args(const List<String> &p_cmdline_args) { - const int NUM_OPTIONS = 2; String generate_all_glue_option = "--generate-mono-glue"; String generate_cs_glue_option = "--generate-mono-cs-glue"; @@ -3447,21 +3593,25 @@ void BindingsGenerator::handle_cmdline_args(const List<String> &p_cmdline_args) } if (glue_dir_path.length()) { - if (bindings_generator.generate_glue(glue_dir_path) != OK) + if (bindings_generator.generate_glue(glue_dir_path) != OK) { ERR_PRINT(generate_all_glue_option + ": Failed to generate the C++ glue."); + } - if (bindings_generator.generate_cs_api(glue_dir_path.plus_file(API_SOLUTION_NAME)) != OK) + if (bindings_generator.generate_cs_api(glue_dir_path.plus_file(API_SOLUTION_NAME)) != OK) { ERR_PRINT(generate_all_glue_option + ": Failed to generate the C# API."); + } } if (cs_dir_path.length()) { - if (bindings_generator.generate_cs_api(cs_dir_path) != OK) + if (bindings_generator.generate_cs_api(cs_dir_path) != OK) { ERR_PRINT(generate_cs_glue_option + ": Failed to generate the C# API."); + } } if (cpp_dir_path.length()) { - if (bindings_generator.generate_glue(cpp_dir_path) != OK) + if (bindings_generator.generate_glue(cpp_dir_path) != OK) { ERR_PRINT(generate_cpp_glue_option + ": Failed to generate the C++ glue."); + } } // Exit once done diff --git a/modules/mono/editor/bindings_generator.h b/modules/mono/editor/bindings_generator.h index 7c87c688db..90c1c9f3ee 100644 --- a/modules/mono/editor/bindings_generator.h +++ b/modules/mono/editor/bindings_generator.h @@ -41,7 +41,6 @@ #include "core/ustring.h" class BindingsGenerator { - struct ConstantInterface { String name; String proxy_name; @@ -85,16 +84,12 @@ class BindingsGenerator { struct TypeReference { StringName cname; - bool is_enum; + bool is_enum = false; - TypeReference() : - is_enum(false) { - } + TypeReference() {} TypeReference(const StringName &p_cname) : - cname(p_cname), - is_enum(false) { - } + cname(p_cname) {} }; struct ArgumentInterface { @@ -107,7 +102,9 @@ class BindingsGenerator { TypeReference type; String name; - DefaultParamMode def_param_mode; + + Variant def_param_value; + DefaultParamMode def_param_mode = CONSTANT; /** * Determines the expression for the parameter default value. @@ -116,9 +113,7 @@ class BindingsGenerator { */ String default_argument; - ArgumentInterface() { - def_param_mode = CONSTANT; - } + ArgumentInterface() {} }; struct MethodInterface { @@ -138,19 +133,19 @@ class BindingsGenerator { /** * Determines if the method has a variable number of arguments (VarArg) */ - bool is_vararg; + bool is_vararg = false; /** * Virtual methods ("virtual" as defined by the Godot API) are methods that by default do nothing, * but can be overridden by the user to add custom functionality. * e.g.: _ready, _process, etc. */ - bool is_virtual; + bool is_virtual = false; /** * Determines if the call should fallback to Godot's object.Call(string, params) in C#. */ - bool requires_object_call; + bool requires_object_call = false; /** * Determines if the method visibility is 'internal' (visible only to files in the same assembly). @@ -158,27 +153,20 @@ class BindingsGenerator { * but are required by properties as getters or setters. * Methods that are not meant to be exposed are those that begin with underscore and are not virtual. */ - bool is_internal; + bool is_internal = false; List<ArgumentInterface> arguments; - const DocData::MethodDoc *method_doc; + const DocData::MethodDoc *method_doc = nullptr; - bool is_deprecated; + bool is_deprecated = false; String deprecation_message; void add_argument(const ArgumentInterface &argument) { arguments.push_back(argument); } - MethodInterface() { - is_vararg = false; - is_virtual = false; - requires_object_call = false; - is_internal = false; - method_doc = nullptr; - is_deprecated = false; - } + MethodInterface() {} }; struct SignalInterface { @@ -192,19 +180,16 @@ class BindingsGenerator { List<ArgumentInterface> arguments; - const DocData::MethodDoc *method_doc; + const DocData::MethodDoc *method_doc = nullptr; - bool is_deprecated; + bool is_deprecated = false; String deprecation_message; void add_argument(const ArgumentInterface &argument) { arguments.push_back(argument); } - SignalInterface() { - method_doc = nullptr; - is_deprecated = false; - } + SignalInterface() {} }; struct TypeInterface { @@ -225,26 +210,26 @@ class BindingsGenerator { */ String proxy_name; - ClassDB::APIType api_type; + ClassDB::APIType api_type = ClassDB::API_NONE; - bool is_enum; - bool is_object_type; - bool is_singleton; - bool is_reference; + bool is_enum = false; + bool is_object_type = false; + bool is_singleton = false; + bool is_reference = false; /** * Used only by Object-derived types. * Determines if this type is not abstract (incomplete). * e.g.: CanvasItem cannot be instantiated. */ - bool is_instantiable; + bool is_instantiable = false; /** * Used only by Object-derived types. * Determines if the C# class owns the native handle and must free it somehow when disposed. * e.g.: Reference types must notify when the C# instance is disposed, for proper refcounting. */ - bool memory_own; + bool memory_own = false; /** * This must be set to true for any struct bigger than 32-bits. Those cannot be passed/returned by value @@ -252,7 +237,7 @@ class BindingsGenerator { * In this case, [c_out] and [cs_out] must have a different format, explained below. * The Mono IL interpreter icall trampolines don't support passing structs bigger than 32-bits by value (at least not on WASM). */ - bool ret_as_byref_arg; + bool ret_as_byref_arg = false; // !! The comments of the following fields make reference to other fields via square brackets, e.g.: [field_name] // !! When renaming those fields, make sure to rename their references in the comments @@ -279,7 +264,7 @@ class BindingsGenerator { * Formatting elements: * %0 or %s: name of the parameter */ - String c_arg_in; + String c_arg_in = "%s"; /** * One or more statements that determine how a variable of this type is returned from a function. @@ -362,7 +347,7 @@ class BindingsGenerator { */ String im_type_out; - const DocData::ClassDoc *class_doc; + const DocData::ClassDoc *class_doc = nullptr; List<ConstantInterface> constants; List<EnumInterface> enums; @@ -372,8 +357,9 @@ class BindingsGenerator { const MethodInterface *find_method_by_name(const StringName &p_cname) const { for (const List<MethodInterface>::Element *E = methods.front(); E; E = E->next()) { - if (E->get().cname == p_cname) + if (E->get().cname == p_cname) { return &E->get(); + } } return nullptr; @@ -381,8 +367,9 @@ class BindingsGenerator { const PropertyInterface *find_property_by_name(const StringName &p_cname) const { for (const List<PropertyInterface>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().cname == p_cname) + if (E->get().cname == p_cname) { return &E->get(); + } } return nullptr; @@ -390,8 +377,9 @@ class BindingsGenerator { const PropertyInterface *find_property_by_proxy_name(const String &p_proxy_name) const { for (const List<PropertyInterface>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().proxy_name == p_proxy_name) + if (E->get().proxy_name == p_proxy_name) { return &E->get(); + } } return nullptr; @@ -399,8 +387,9 @@ class BindingsGenerator { const MethodInterface *find_method_by_proxy_name(const String &p_proxy_name) const { for (const List<MethodInterface>::Element *E = methods.front(); E; E = E->next()) { - if (E->get().proxy_name == p_proxy_name) + if (E->get().proxy_name == p_proxy_name) { return &E->get(); + } } return nullptr; @@ -482,24 +471,7 @@ class BindingsGenerator { r_enum_itype.class_doc = &EditorHelp::get_doc_data()->class_list[r_enum_itype.proxy_name]; } - TypeInterface() { - - api_type = ClassDB::API_NONE; - - is_enum = false; - is_object_type = false; - is_singleton = false; - is_reference = false; - is_instantiable = false; - - memory_own = false; - - ret_as_byref_arg = false; - - c_arg_in = "%s"; - - class_doc = nullptr; - } + TypeInterface() {} }; struct InternalCall { @@ -532,8 +504,8 @@ class BindingsGenerator { } }; - bool log_print_enabled; - bool initialized; + bool log_print_enabled = true; + bool initialized = false; OrderedHashMap<StringName, TypeInterface> obj_types; @@ -557,58 +529,70 @@ class BindingsGenerator { void _initialize_blacklisted_methods(); struct NameCache { - StringName type_void; - StringName type_Array; - StringName type_Dictionary; - StringName type_Variant; - StringName type_VarArg; - StringName type_Object; - StringName type_Reference; - StringName type_RID; - StringName type_String; - StringName type_StringName; - StringName type_NodePath; - StringName type_at_GlobalScope; - StringName enum_Error; - - StringName type_sbyte; - StringName type_short; - StringName type_int; - StringName type_long; - StringName type_byte; - StringName type_ushort; - StringName type_uint; - StringName type_ulong; - StringName type_float; - StringName type_double; - - NameCache() { - type_void = StaticCString::create("void"); - type_Array = StaticCString::create("Array"); - type_Dictionary = StaticCString::create("Dictionary"); - type_Variant = StaticCString::create("Variant"); - type_VarArg = StaticCString::create("VarArg"); - type_Object = StaticCString::create("Object"); - type_Reference = StaticCString::create("Reference"); - type_RID = StaticCString::create("RID"); - type_String = StaticCString::create("String"); - type_StringName = StaticCString::create("StringName"); - type_NodePath = StaticCString::create("NodePath"); - type_at_GlobalScope = StaticCString::create("@GlobalScope"); - enum_Error = StaticCString::create("Error"); - - type_sbyte = StaticCString::create("sbyte"); - type_short = StaticCString::create("short"); - type_int = StaticCString::create("int"); - type_long = StaticCString::create("long"); - type_byte = StaticCString::create("byte"); - type_ushort = StaticCString::create("ushort"); - type_uint = StaticCString::create("uint"); - type_ulong = StaticCString::create("ulong"); - type_float = StaticCString::create("float"); - type_double = StaticCString::create("double"); + StringName type_void = StaticCString::create("void"); + StringName type_Variant = StaticCString::create("Variant"); + StringName type_VarArg = StaticCString::create("VarArg"); + StringName type_Object = StaticCString::create("Object"); + StringName type_Reference = StaticCString::create("Reference"); + StringName type_RID = StaticCString::create("RID"); + StringName type_String = StaticCString::create("String"); + StringName type_StringName = StaticCString::create("StringName"); + StringName type_NodePath = StaticCString::create("NodePath"); + StringName type_at_GlobalScope = StaticCString::create("@GlobalScope"); + StringName enum_Error = StaticCString::create("Error"); + + StringName type_sbyte = StaticCString::create("sbyte"); + StringName type_short = StaticCString::create("short"); + StringName type_int = StaticCString::create("int"); + StringName type_byte = StaticCString::create("byte"); + StringName type_ushort = StaticCString::create("ushort"); + StringName type_uint = StaticCString::create("uint"); + StringName type_long = StaticCString::create("long"); + StringName type_ulong = StaticCString::create("ulong"); + + StringName type_bool = StaticCString::create("bool"); + StringName type_float = StaticCString::create("float"); + StringName type_double = StaticCString::create("double"); + + StringName type_Vector2 = StaticCString::create("Vector2"); + StringName type_Rect2 = StaticCString::create("Rect2"); + StringName type_Vector3 = StaticCString::create("Vector3"); + + // Object not included as it must be checked for all derived classes + static constexpr int nullable_types_count = 17; + StringName nullable_types[nullable_types_count] = { + type_String, + type_StringName, + type_NodePath, + + StaticCString::create(_STR(Array)), + StaticCString::create(_STR(Dictionary)), + StaticCString::create(_STR(Callable)), + StaticCString::create(_STR(Signal)), + + StaticCString::create(_STR(PackedByteArray)), + StaticCString::create(_STR(PackedInt32Array)), + StaticCString::create(_STR(PackedInt64rray)), + StaticCString::create(_STR(PackedFloat32Array)), + StaticCString::create(_STR(PackedFloat64Array)), + StaticCString::create(_STR(PackedStringArray)), + StaticCString::create(_STR(PackedVector2Array)), + StaticCString::create(_STR(PackedVector3Array)), + StaticCString::create(_STR(PackedColorArray)), + }; + + bool is_nullable_type(const StringName &p_type) const { + for (int i = 0; i < nullable_types_count; i++) { + if (p_type == nullable_types[i]) { + return true; + } + } + + return false; } + NameCache() {} + private: NameCache(const NameCache &); NameCache &operator=(const NameCache &); @@ -619,7 +603,9 @@ class BindingsGenerator { const List<InternalCall>::Element *find_icall_by_name(const String &p_name, const List<InternalCall> &p_list) { const List<InternalCall>::Element *it = p_list.front(); while (it) { - if (it->get().name == p_name) return it; + if (it->get().name == p_name) { + return it; + } it = it->next(); } return nullptr; @@ -627,20 +613,22 @@ class BindingsGenerator { const ConstantInterface *find_constant_by_name(const String &p_name, const List<ConstantInterface> &p_constants) const { for (const List<ConstantInterface>::Element *E = p_constants.front(); E; E = E->next()) { - if (E->get().name == p_name) + if (E->get().name == p_name) { return &E->get(); + } } return nullptr; } inline String get_unique_sig(const TypeInterface &p_type) { - if (p_type.is_reference) + if (p_type.is_reference) { return "Ref"; - else if (p_type.is_object_type) + } else if (p_type.is_object_type) { return "Obj"; - else if (p_type.is_enum) + } else if (p_type.is_enum) { return "int"; + } return p_type.name; } @@ -659,6 +647,7 @@ class BindingsGenerator { StringName _get_float_type_name_from_meta(GodotTypeInfo::Metadata p_meta); bool _arg_default_value_from_variant(const Variant &p_val, ArgumentInterface &r_iarg); + bool _arg_default_value_is_assignable_to_type(const Variant &p_val, const TypeInterface &p_arg_type); bool _populate_object_type_interfaces(); void _populate_builtin_type_interfaces(); @@ -696,9 +685,7 @@ public: static void handle_cmdline_args(const List<String> &p_cmdline_args); - BindingsGenerator() : - log_print_enabled(true), - initialized(false) { + BindingsGenerator() { _initialize(); } }; diff --git a/modules/mono/editor/code_completion.cpp b/modules/mono/editor/code_completion.cpp new file mode 100644 index 0000000000..942c6d26a6 --- /dev/null +++ b/modules/mono/editor/code_completion.cpp @@ -0,0 +1,250 @@ +/*************************************************************************/ +/* code_completion.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "code_completion.h" + +#include "core/project_settings.h" +#include "editor/editor_file_system.h" +#include "editor/editor_settings.h" +#include "scene/gui/control.h" +#include "scene/main/node.h" + +namespace gdmono { + +// Almost everything here is taken from functions used by GDScript for code completion, adapted for C#. + +_FORCE_INLINE_ String quoted(const String &p_str) { + return "\"" + p_str + "\""; +} + +void _add_nodes_suggestions(const Node *p_base, const Node *p_node, PackedStringArray &r_suggestions) { + if (p_node != p_base && !p_node->get_owner()) { + return; + } + + String path_relative_to_orig = p_base->get_path_to(p_node); + + r_suggestions.push_back(quoted(path_relative_to_orig)); + + for (int i = 0; i < p_node->get_child_count(); i++) { + _add_nodes_suggestions(p_base, p_node->get_child(i), r_suggestions); + } +} + +Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) { + if (p_current->get_owner() != p_base && p_base != p_current) { + return nullptr; + } + + Ref<Script> c = p_current->get_script(); + + if (c == p_script) { + return p_current; + } + + for (int i = 0; i < p_current->get_child_count(); i++) { + Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script); + if (found) { + return found; + } + } + + return nullptr; +} + +void _get_directory_contents(EditorFileSystemDirectory *p_dir, PackedStringArray &r_suggestions) { + for (int i = 0; i < p_dir->get_file_count(); i++) { + r_suggestions.push_back(quoted(p_dir->get_file_path(i))); + } + + for (int i = 0; i < p_dir->get_subdir_count(); i++) { + _get_directory_contents(p_dir->get_subdir(i), r_suggestions); + } +} + +Node *_try_find_owner_node_in_tree(const Ref<Script> p_script) { + SceneTree *tree = SceneTree::get_singleton(); + if (!tree) { + return nullptr; + } + Node *base = tree->get_edited_scene_root(); + if (base) { + base = _find_node_for_script(base, base, p_script); + } + return base; +} + +PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file) { + PackedStringArray suggestions; + + switch (p_kind) { + case CompletionKind::INPUT_ACTIONS: { + List<PropertyInfo> project_props; + ProjectSettings::get_singleton()->get_property_list(&project_props); + + for (List<PropertyInfo>::Element *E = project_props.front(); E; E = E->next()) { + const PropertyInfo &prop = E->get(); + + if (!prop.name.begins_with("input/")) { + continue; + } + + String name = prop.name.substr(prop.name.find("/") + 1, prop.name.length()); + suggestions.push_back(quoted(name)); + } + } break; + case CompletionKind::NODE_PATHS: { + { + // AutoLoads + Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list(); + + for (Map<StringName, ProjectSettings::AutoloadInfo>::Element *E = autoloads.front(); E; E = E->next()) { + const ProjectSettings::AutoloadInfo &info = E->value(); + suggestions.push_back(quoted("/root/" + String(info.name))); + } + } + + { + // Current edited scene tree + Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path()); + Node *base = _try_find_owner_node_in_tree(script); + if (base) { + _add_nodes_suggestions(base, base, suggestions); + } + } + } break; + case CompletionKind::RESOURCE_PATHS: { + if (bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) { + _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), suggestions); + } + } break; + case CompletionKind::SCENE_PATHS: { + DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES); + List<String> directories; + directories.push_back(dir_access->get_current_dir()); + + while (!directories.empty()) { + dir_access->change_dir(directories.back()->get()); + directories.pop_back(); + + dir_access->list_dir_begin(); + String filename = dir_access->get_next(); + + while (filename != "") { + if (filename == "." || filename == "..") { + filename = dir_access->get_next(); + continue; + } + + if (dir_access->dir_exists(filename)) { + directories.push_back(dir_access->get_current_dir().plus_file(filename)); + } else if (filename.ends_with(".tscn") || filename.ends_with(".scn")) { + suggestions.push_back(quoted(dir_access->get_current_dir().plus_file(filename))); + } + + filename = dir_access->get_next(); + } + } + } break; + case CompletionKind::SHADER_PARAMS: { + print_verbose("Shared params completion for C# not implemented."); + } break; + case CompletionKind::SIGNALS: { + Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path()); + + List<MethodInfo> signals; + script->get_script_signal_list(&signals); + + StringName native = script->get_instance_base_type(); + if (native != StringName()) { + ClassDB::get_signal_list(native, &signals, /* p_no_inheritance: */ false); + } + + for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) { + const String &signal = E->get().name; + suggestions.push_back(quoted(signal)); + } + } break; + case CompletionKind::THEME_COLORS: { + Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path()); + Node *base = _try_find_owner_node_in_tree(script); + if (base && Object::cast_to<Control>(base)) { + List<StringName> sn; + Theme::get_default()->get_color_list(base->get_class(), &sn); + + for (List<StringName>::Element *E = sn.front(); E; E = E->next()) { + suggestions.push_back(quoted(E->get())); + } + } + } break; + case CompletionKind::THEME_CONSTANTS: { + Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path()); + Node *base = _try_find_owner_node_in_tree(script); + if (base && Object::cast_to<Control>(base)) { + List<StringName> sn; + Theme::get_default()->get_constant_list(base->get_class(), &sn); + + for (List<StringName>::Element *E = sn.front(); E; E = E->next()) { + suggestions.push_back(quoted(E->get())); + } + } + } break; + case CompletionKind::THEME_FONTS: { + Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path()); + Node *base = _try_find_owner_node_in_tree(script); + if (base && Object::cast_to<Control>(base)) { + List<StringName> sn; + Theme::get_default()->get_font_list(base->get_class(), &sn); + + for (List<StringName>::Element *E = sn.front(); E; E = E->next()) { + suggestions.push_back(quoted(E->get())); + } + } + } break; + case CompletionKind::THEME_STYLES: { + Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path()); + Node *base = _try_find_owner_node_in_tree(script); + if (base && Object::cast_to<Control>(base)) { + List<StringName> sn; + Theme::get_default()->get_stylebox_list(base->get_class(), &sn); + + for (List<StringName>::Element *E = sn.front(); E; E = E->next()) { + suggestions.push_back(quoted(E->get())); + } + } + } break; + default: + ERR_FAIL_V_MSG(suggestions, "Invalid completion kind."); + } + + return suggestions; +} + +} // namespace gdmono diff --git a/modules/mono/editor/csharp_project.h b/modules/mono/editor/code_completion.h index 515b8d3d62..77673b766f 100644 --- a/modules/mono/editor/csharp_project.h +++ b/modules/mono/editor/code_completion.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* csharp_project.h */ +/* code_completion.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,15 +28,29 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef CSHARP_PROJECT_H -#define CSHARP_PROJECT_H +#ifndef CODE_COMPLETION_H +#define CODE_COMPLETION_H #include "core/ustring.h" +#include "core/variant.h" -namespace CSharpProject { +namespace gdmono { -void add_item(const String &p_project_path, const String &p_item_type, const String &p_include); +enum class CompletionKind { + INPUT_ACTIONS = 0, + NODE_PATHS, + RESOURCE_PATHS, + SCENE_PATHS, + SHADER_PARAMS, + SIGNALS, + THEME_COLORS, + THEME_CONSTANTS, + THEME_FONTS, + THEME_STYLES +}; -} // namespace CSharpProject +PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file); -#endif // CSHARP_PROJECT_H +} // namespace gdmono + +#endif // CODE_COMPLETION_H diff --git a/modules/mono/editor/csharp_project.cpp b/modules/mono/editor/csharp_project.cpp deleted file mode 100644 index e5c2d023d3..0000000000 --- a/modules/mono/editor/csharp_project.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/*************************************************************************/ -/* csharp_project.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 "csharp_project.h" - -#include "core/io/json.h" -#include "core/os/dir_access.h" -#include "core/os/file_access.h" -#include "core/os/os.h" -#include "core/project_settings.h" - -#include "../csharp_script.h" -#include "../mono_gd/gd_mono_class.h" -#include "../mono_gd/gd_mono_marshal.h" -#include "../utils/string_utils.h" -#include "script_class_parser.h" - -namespace CSharpProject { - -void add_item(const String &p_project_path, const String &p_item_type, const String &p_include) { - - if (!GLOBAL_DEF("mono/project/auto_update_project", true)) - return; - - GDMonoAssembly *tools_project_editor_assembly = GDMono::get_singleton()->get_tools_project_editor_assembly(); - - GDMonoClass *klass = tools_project_editor_assembly->get_class("GodotTools.ProjectEditor", "ProjectUtils"); - - Variant project_path = p_project_path; - Variant item_type = p_item_type; - Variant include = p_include; - const Variant *args[3] = { &project_path, &item_type, &include }; - MonoException *exc = nullptr; - klass->get_method("AddItemToProjectChecked", 3)->invoke(nullptr, args, &exc); - - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - ERR_FAIL(); - } -} - -} // namespace CSharpProject diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index c3e7e67ae9..68fc372959 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -48,6 +48,7 @@ #include "../mono_gd/gd_mono_marshal.h" #include "../utils/osx_utils.h" #include "bindings_generator.h" +#include "code_completion.h" #include "godotsharp_export.h" #include "script_class_parser.h" @@ -323,7 +324,7 @@ MonoObject *godot_icall_Internal_GetScriptsMetadataOrNothing(MonoReflectionType MonoType *dict_type = mono_reflection_type_get_type(p_dict_reftype); - uint32_t type_encoding = mono_type_get_type(dict_type); + int type_encoding = mono_type_get_type(dict_type); MonoClass *type_class_raw = mono_class_from_mono_type(dict_type); GDMonoClass *type_class = GDMono::get_singleton()->get_class(type_class_raw); @@ -354,6 +355,12 @@ void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() { } } +MonoArray *godot_icall_Internal_CodeCompletionRequest(int32_t p_kind, MonoString *p_script_file) { + String script_file = GDMonoMarshal::mono_string_to_godot(p_script_file); + PackedStringArray suggestions = gdmono::get_code_completion((gdmono::CompletionKind)p_kind, script_file); + return GDMonoMarshal::PackedStringArray_to_mono_array(suggestions); +} + float godot_icall_Globals_EditorScale() { return EDSCALE; } @@ -392,7 +399,6 @@ MonoBoolean godot_icall_Utils_OS_UnixFileHasExecutableAccess(MonoString *p_file_ } void register_editor_internal_calls() { - // GodotSharpDirs mono_add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResDataDir", (void *)godot_icall_GodotSharpDirs_ResDataDir); mono_add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResMetadataDir", (void *)godot_icall_GodotSharpDirs_ResMetadataDir); @@ -454,6 +460,7 @@ void register_editor_internal_calls() { mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", (void *)godot_icall_Internal_EditorRunPlay); mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", (void *)godot_icall_Internal_EditorRunStop); mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts); + mono_add_internal_call("GodotTools.Internals.Internal::internal_CodeCompletionRequest", (void *)godot_icall_Internal_CodeCompletionRequest); // Globals mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale); diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp index 4126da16be..b15e9b060a 100644 --- a/modules/mono/editor/godotsharp_export.cpp +++ b/modules/mono/editor/godotsharp_export.cpp @@ -32,68 +32,71 @@ #include <mono/metadata/image.h> +#include "core/io/file_access_pack.h" #include "core/os/os.h" +#include "core/project_settings.h" #include "../mono_gd/gd_mono.h" #include "../mono_gd/gd_mono_assembly.h" #include "../mono_gd/gd_mono_cache.h" +#include "../utils/macros.h" namespace GodotSharpExport { -String get_assemblyref_name(MonoImage *p_image, int index) { +struct AssemblyRefInfo { + String name; + uint16_t major; + uint16_t minor; + uint16_t build; + uint16_t revision; +}; + +AssemblyRefInfo get_assemblyref_name(MonoImage *p_image, int index) { const MonoTableInfo *table_info = mono_image_get_table_info(p_image, MONO_TABLE_ASSEMBLYREF); uint32_t cols[MONO_ASSEMBLYREF_SIZE]; mono_metadata_decode_row(table_info, index, cols, MONO_ASSEMBLYREF_SIZE); - return String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME])); + return { + String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME])), + (uint16_t)cols[MONO_ASSEMBLYREF_MAJOR_VERSION], + (uint16_t)cols[MONO_ASSEMBLYREF_MINOR_VERSION], + (uint16_t)cols[MONO_ASSEMBLYREF_BUILD_NUMBER], + (uint16_t)cols[MONO_ASSEMBLYREF_REV_NUMBER] + }; } Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_assembly_dependencies) { MonoImage *image = p_assembly->get_image(); for (int i = 0; i < mono_image_get_table_rows(image, MONO_TABLE_ASSEMBLYREF); i++) { - String ref_name = get_assemblyref_name(image, i); + AssemblyRefInfo ref_info = get_assemblyref_name(image, i); + + const String &ref_name = ref_info.name; - if (r_assembly_dependencies.has(ref_name)) + if (r_assembly_dependencies.has(ref_name)) { continue; + } GDMonoAssembly *ref_assembly = nullptr; - String path; - bool has_extension = ref_name.ends_with(".dll") || ref_name.ends_with(".exe"); - - for (int j = 0; j < p_search_dirs.size(); j++) { - const String &search_dir = p_search_dirs[j]; - - if (has_extension) { - path = search_dir.plus_file(ref_name); - if (FileAccess::exists(path)) { - GDMono::get_singleton()->load_assembly_from(ref_name.get_basename(), path, &ref_assembly, true); - if (ref_assembly != nullptr) - break; - } - } else { - path = search_dir.plus_file(ref_name + ".dll"); - if (FileAccess::exists(path)) { - GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true); - if (ref_assembly != nullptr) - break; - } - - path = search_dir.plus_file(ref_name + ".exe"); - if (FileAccess::exists(path)) { - GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true); - if (ref_assembly != nullptr) - break; - } - } - } - ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'."); + { + MonoAssemblyName *ref_aname = mono_assembly_name_new("A"); // We can't allocate an empty MonoAssemblyName, hence "A" + CRASH_COND(ref_aname == nullptr); + SCOPE_EXIT { + mono_assembly_name_free(ref_aname); + mono_free(ref_aname); + }; + + mono_assembly_get_assemblyref(image, i, ref_aname); - // Use the path we got from the search. Don't try to get the path from the loaded assembly as we can't trust it will be from the selected BCL dir. - r_assembly_dependencies[ref_name] = path; + if (!GDMono::get_singleton()->load_assembly(ref_name, ref_aname, &ref_assembly, /* refonly: */ true, p_search_dirs)) { + ERR_FAIL_V_MSG(ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'."); + } + + r_assembly_dependencies[ref_name] = ref_assembly->get_path(); + } Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_assembly_dependencies); ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'."); @@ -113,6 +116,11 @@ Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies, Vector<String> search_dirs; GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_bcl_dir); + if (p_custom_bcl_dir.length()) { + // Only one mscorlib can be loaded. We need this workaround to make sure we get it from the right BCL directory. + r_assembly_dependencies["mscorlib"] = p_custom_bcl_dir.plus_file("mscorlib.dll").simplify_path(); + } + for (const Variant *key = p_initial_assemblies.next(); key; key = p_initial_assemblies.next(key)) { String assembly_name = *key; String assembly_path = p_initial_assemblies[*key]; @@ -123,8 +131,9 @@ Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies, ERR_FAIL_COND_V_MSG(!load_success, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + assembly_name + "'."); Error err = get_assembly_dependencies(assembly, search_dirs, r_assembly_dependencies); - if (err != OK) + if (err != OK) { return err; + } } return OK; diff --git a/modules/mono/editor/script_class_parser.cpp b/modules/mono/editor/script_class_parser.cpp index bece23c9a6..430c82953e 100644 --- a/modules/mono/editor/script_class_parser.cpp +++ b/modules/mono/editor/script_class_parser.cpp @@ -54,13 +54,11 @@ const char *ScriptClassParser::token_names[ScriptClassParser::TK_MAX] = { }; String ScriptClassParser::get_token_name(ScriptClassParser::Token p_token) { - ERR_FAIL_INDEX_V(p_token, TK_MAX, "<error>"); return token_names[p_token]; } ScriptClassParser::Token ScriptClassParser::get_token() { - while (true) { switch (code[idx]) { case '\n': { @@ -181,14 +179,24 @@ ScriptClassParser::Token ScriptClassParser::get_token() { CharType res = 0; switch (next) { - case 'b': res = 8; break; - case 't': res = 9; break; - case 'n': res = 10; break; - case 'f': res = 12; break; + case 'b': + res = 8; + break; + case 't': + res = 9; + break; + case 'n': + res = 10; + break; + case 'f': + res = 12; + break; case 'r': res = 13; break; - case '\"': res = '\"'; break; + case '\"': + res = '\"'; + break; case '\\': res = '\\'; break; @@ -200,8 +208,9 @@ ScriptClassParser::Token ScriptClassParser::get_token() { tk_string += res; } else { - if (code[idx] == '\n') + if (code[idx] == '\n') { line++; + } tk_string += code[idx]; } idx++; @@ -226,7 +235,7 @@ ScriptClassParser::Token ScriptClassParser::get_token() { if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) { //a number const CharType *rptr; - double number = String::to_double(&code[idx], &rptr); + double number = String::to_float(&code[idx], &rptr); idx += (rptr - &code[idx]); value = number; return TK_NUMBER; @@ -258,7 +267,6 @@ ScriptClassParser::Token ScriptClassParser::get_token() { } Error ScriptClassParser::_skip_generic_type_params() { - Token tk; while (true) { @@ -293,15 +301,17 @@ Error ScriptClassParser::_skip_generic_type_params() { tk = get_token(); - if (tk != TK_PERIOD) + if (tk != TK_PERIOD) { break; + } } } if (tk == TK_OP_LESS) { Error err = _skip_generic_type_params(); - if (err) + if (err) { return err; + } tk = get_token(); } @@ -327,7 +337,6 @@ Error ScriptClassParser::_skip_generic_type_params() { } Error ScriptClassParser::_parse_type_full_name(String &r_full_name) { - Token tk = get_token(); if (tk != TK_IDENTIFIER) { @@ -343,12 +352,14 @@ Error ScriptClassParser::_parse_type_full_name(String &r_full_name) { // We don't mind if the base is generic, but we skip it any ways since this information is not needed Error err = _skip_generic_type_params(); - if (err) + if (err) { return err; + } } - if (code[idx] != '.') // We only want to take the next token if it's a period + if (code[idx] != '.') { // We only want to take the next token if it's a period return OK; + } tk = get_token(); @@ -360,19 +371,20 @@ Error ScriptClassParser::_parse_type_full_name(String &r_full_name) { } Error ScriptClassParser::_parse_class_base(Vector<String> &r_base) { - String name; Error err = _parse_type_full_name(name); - if (err) + if (err) { return err; + } Token tk = get_token(); if (tk == TK_COMMA) { err = _parse_class_base(r_base); - if (err) + if (err) { return err; + } } else if (tk == TK_IDENTIFIER && String(value) == "where") { err = _parse_type_constraints(); if (err) { @@ -428,8 +440,9 @@ Error ScriptClassParser::_parse_type_constraints() { tk = get_token(); - if (tk != TK_PERIOD) + if (tk != TK_PERIOD) { break; + } } } } @@ -447,8 +460,9 @@ Error ScriptClassParser::_parse_type_constraints() { } } else if (tk == TK_OP_LESS) { Error err = _skip_generic_type_params(); - if (err) + if (err) { return err; + } } else if (tk == TK_CURLY_BRACKET_OPEN) { return OK; } else { @@ -460,7 +474,6 @@ Error ScriptClassParser::_parse_type_constraints() { } Error ScriptClassParser::_parse_namespace_name(String &r_name, int &r_curly_stack) { - Token tk = get_token(); if (tk == TK_IDENTIFIER) { @@ -487,7 +500,6 @@ Error ScriptClassParser::_parse_namespace_name(String &r_name, int &r_curly_stac } Error ScriptClassParser::parse(const String &p_code) { - code = p_code; idx = 0; line = 0; @@ -519,8 +531,9 @@ Error ScriptClassParser::parse(const String &p_code) { const NameDecl &name_decl = E->value(); if (name_decl.type == NameDecl::NAMESPACE_DECL) { - if (E != name_stack.front()) + if (E != name_stack.front()) { class_decl.namespace_ += "."; + } class_decl.namespace_ += name_decl.name; } else { class_decl.name += name_decl.name + "."; @@ -537,8 +550,9 @@ Error ScriptClassParser::parse(const String &p_code) { if (tk == TK_COLON) { Error err = _parse_class_base(class_decl.base); - if (err) + if (err) { return err; + } curly_stack++; type_curly_stack++; @@ -552,8 +566,9 @@ Error ScriptClassParser::parse(const String &p_code) { generic = true; Error err = _skip_generic_type_params(); - if (err) + if (err) { return err; + } } else if (tk == TK_IDENTIFIER && String(value) == "where") { Error err = _parse_type_constraints(); if (err) { @@ -581,8 +596,9 @@ Error ScriptClassParser::parse(const String &p_code) { classes.push_back(class_decl); } else if (OS::get_singleton()->is_stdout_verbose()) { String full_name = class_decl.namespace_; - if (full_name.length()) + if (full_name.length()) { full_name += "."; + } full_name += class_decl.name; OS::get_singleton()->print("Ignoring generic class declaration: %s\n", full_name.utf8().get_data()); } @@ -599,8 +615,9 @@ Error ScriptClassParser::parse(const String &p_code) { int at_level = curly_stack; Error err = _parse_namespace_name(name, curly_stack); - if (err) + if (err) { return err; + } NameDecl name_decl; name_decl.name = name; @@ -611,8 +628,9 @@ Error ScriptClassParser::parse(const String &p_code) { } else if (tk == TK_CURLY_BRACKET_CLOSE) { curly_stack--; if (name_stack.has(curly_stack)) { - if (name_stack[curly_stack].type != NameDecl::NAMESPACE_DECL) + if (name_stack[curly_stack].type != NameDecl::NAMESPACE_DECL) { type_curly_stack--; + } name_stack.erase(curly_stack); } } @@ -625,8 +643,9 @@ Error ScriptClassParser::parse(const String &p_code) { error = true; } - if (error) + if (error) { return ERR_PARSE_ERROR; + } return OK; } @@ -643,7 +662,6 @@ static String get_preprocessor_directive(const String &p_line, int p_from) { } static void run_dummy_preprocessor(String &r_source, const String &p_filepath) { - Vector<String> lines = r_source.split("\n", /* p_allow_empty: */ true); bool *include_lines = memnew_arr(bool, lines.size()); @@ -700,8 +718,9 @@ static void run_dummy_preprocessor(String &r_source, const String &p_filepath) { // Custom join ignoring lines removed by the preprocessor for (int i = 0; i < lines.size(); i++) { - if (i > 0 && include_lines[i - 1]) + if (i > 0 && include_lines[i - 1]) { r_source += '\n'; + } if (include_lines[i]) { r_source += lines[i]; @@ -710,7 +729,6 @@ static void run_dummy_preprocessor(String &r_source, const String &p_filepath) { } Error ScriptClassParser::parse_file(const String &p_filepath) { - String source; Error ferr = read_all_file_utf8(p_filepath, source); diff --git a/modules/mono/editor/script_class_parser.h b/modules/mono/editor/script_class_parser.h index a76a3a50a9..d611e8fb74 100644 --- a/modules/mono/editor/script_class_parser.h +++ b/modules/mono/editor/script_class_parser.h @@ -36,7 +36,6 @@ #include "core/vector.h" class ScriptClassParser { - public: struct NameDecl { enum Type { @@ -54,7 +53,6 @@ public: String namespace_; Vector<String> base; bool nested; - bool has_script_attr; }; private: diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs index 6a4f785551..3aecce50f5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs @@ -14,6 +14,10 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// Axis-Aligned Bounding Box. AABB consists of a position, a size, and + /// several utility functions. It is typically used for fast overlap tests. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct AABB : IEquatable<AABB> @@ -21,24 +25,55 @@ namespace Godot private Vector3 _position; private Vector3 _size; + /// <summary> + /// Beginning corner. Typically has values lower than End. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector3 Position { get { return _position; } set { _position = value; } } + /// <summary> + /// Size from Position to End. Typically all components are positive. + /// If the size is negative, you can use <see cref="Abs"/> to fix it. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector3 Size { get { return _size; } set { _size = value; } } + /// <summary> + /// Ending corner. This is calculated as <see cref="Position"/> plus + /// <see cref="Size"/>. Setting this value will change the size. + /// </summary> + /// <value>Getting is equivalent to `value = Position + Size`, setting is equivalent to `Size = value - Position`.</value> public Vector3 End { get { return _position + _size; } set { _size = value - _position; } } + /// <summary> + /// Returns an AABB with equivalent position and size, modified so that + /// the most-negative corner is the origin and the size is positive. + /// </summary> + /// <returns>The modified AABB.</returns> + public AABB Abs() + { + Vector3 end = End; + Vector3 topLeft = new Vector3(Mathf.Min(_position.x, end.x), Mathf.Min(_position.y, end.y), Mathf.Min(_position.z, end.z)); + return new AABB(topLeft, _size.Abs()); + } + + /// <summary> + /// Returns true if this AABB completely encloses another one. + /// </summary> + /// <param name="with">The other AABB that may be enclosed.</param> + /// <returns>A bool for whether or not this AABB encloses `b`.</returns> public bool Encloses(AABB with) { Vector3 src_min = _position; @@ -54,33 +89,59 @@ namespace Godot src_max.z > dst_max.z; } + /// <summary> + /// Returns this AABB expanded to include a given point. + /// </summary> + /// <param name="point">The point to include.</param> + /// <returns>The expanded AABB.</returns> public AABB Expand(Vector3 point) { Vector3 begin = _position; Vector3 end = _position + _size; if (point.x < begin.x) + { begin.x = point.x; + } if (point.y < begin.y) + { begin.y = point.y; + } if (point.z < begin.z) + { begin.z = point.z; + } if (point.x > end.x) + { end.x = point.x; + } if (point.y > end.y) + { end.y = point.y; + } if (point.z > end.z) + { end.z = point.z; + } return new AABB(begin, end - begin); } + /// <summary> + /// Returns the area of the AABB. + /// </summary> + /// <returns>The area.</returns> public real_t GetArea() { return _size.x * _size.y * _size.z; } + /// <summary> + /// Gets the position of one of the 8 endpoints of the AABB. + /// </summary> + /// <param name="idx">Which endpoint to get.</param> + /// <returns>An endpoint of the AABB.</returns> public Vector3 GetEndpoint(int idx) { switch (idx) @@ -106,6 +167,10 @@ namespace Godot } } + /// <summary> + /// Returns the normalized longest axis of the AABB. + /// </summary> + /// <returns>A vector representing the normalized longest axis of the AABB.</returns> public Vector3 GetLongestAxis() { var axis = new Vector3(1f, 0f, 0f); @@ -125,6 +190,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the <see cref="Vector3.Axis"/> index of the longest axis of the AABB. + /// </summary> + /// <returns>A <see cref="Vector3.Axis"/> index for which axis is longest.</returns> public Vector3.Axis GetLongestAxisIndex() { var axis = Vector3.Axis.X; @@ -144,6 +213,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the scalar length of the longest axis of the AABB. + /// </summary> + /// <returns>The scalar length of the longest axis of the AABB.</returns> public real_t GetLongestAxisSize() { real_t max_size = _size.x; @@ -157,6 +230,10 @@ namespace Godot return max_size; } + /// <summary> + /// Returns the normalized shortest axis of the AABB. + /// </summary> + /// <returns>A vector representing the normalized shortest axis of the AABB.</returns> public Vector3 GetShortestAxis() { var axis = new Vector3(1f, 0f, 0f); @@ -176,6 +253,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the <see cref="Vector3.Axis"/> index of the shortest axis of the AABB. + /// </summary> + /// <returns>A <see cref="Vector3.Axis"/> index for which axis is shortest.</returns> public Vector3.Axis GetShortestAxisIndex() { var axis = Vector3.Axis.X; @@ -195,6 +276,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the scalar length of the shortest axis of the AABB. + /// </summary> + /// <returns>The scalar length of the shortest axis of the AABB.</returns> public real_t GetShortestAxisSize() { real_t max_size = _size.x; @@ -208,6 +293,12 @@ namespace Godot return max_size; } + /// <summary> + /// Returns the support point in a given direction. + /// This is useful for collision detection algorithms. + /// </summary> + /// <param name="dir">The direction to find support for.</param> + /// <returns>A vector representing the support.</returns> public Vector3 GetSupport(Vector3 dir) { Vector3 half_extents = _size * 0.5f; @@ -219,6 +310,11 @@ namespace Godot dir.z > 0f ? -half_extents.z : half_extents.z); } + /// <summary> + /// Returns a copy of the AABB grown a given amount of units towards all the sides. + /// </summary> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown AABB.</returns> public AABB Grow(real_t by) { var res = this; @@ -233,16 +329,29 @@ namespace Godot return res; } + /// <summary> + /// Returns true if the AABB is flat or empty, or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the AABB has area.</returns> public bool HasNoArea() { return _size.x <= 0f || _size.y <= 0f || _size.z <= 0f; } + /// <summary> + /// Returns true if the AABB has no surface (no size), or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the AABB has area.</returns> public bool HasNoSurface() { return _size.x <= 0f && _size.y <= 0f && _size.z <= 0f; } + /// <summary> + /// Returns true if the AABB contains a point, or false otherwise. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the AABB contains `point`.</returns> public bool HasPoint(Vector3 point) { if (point.x < _position.x) @@ -261,6 +370,11 @@ namespace Godot return true; } + /// <summary> + /// Returns the intersection of this AABB and `b`. + /// </summary> + /// <param name="with">The other AABB.</param> + /// <returns>The clipped AABB.</returns> public AABB Intersection(AABB with) { Vector3 src_min = _position; @@ -297,24 +411,57 @@ namespace Godot return new AABB(min, max - min); } - public bool Intersects(AABB with) + /// <summary> + /// Returns true if the AABB overlaps with `b` + /// (i.e. they have at least one point in common). + /// + /// If `includeBorders` is true, they will also be considered overlapping + /// if their borders touch, even without intersection. + /// </summary> + /// <param name="with">The other AABB to check for intersections with.</param> + /// <param name="includeBorders">Whether or not to consider borders.</param> + /// <returns>A bool for whether or not they are intersecting.</returns> + public bool Intersects(AABB with, bool includeBorders = false) { - if (_position.x >= with._position.x + with._size.x) - return false; - if (_position.x + _size.x <= with._position.x) - return false; - if (_position.y >= with._position.y + with._size.y) - return false; - if (_position.y + _size.y <= with._position.y) - return false; - if (_position.z >= with._position.z + with._size.z) - return false; - if (_position.z + _size.z <= with._position.z) - return false; + if (includeBorders) + { + if (_position.x > with._position.x + with._size.x) + return false; + if (_position.x + _size.x < with._position.x) + return false; + if (_position.y > with._position.y + with._size.y) + return false; + if (_position.y + _size.y < with._position.y) + return false; + if (_position.z > with._position.z + with._size.z) + return false; + if (_position.z + _size.z < with._position.z) + return false; + } + else + { + if (_position.x >= with._position.x + with._size.x) + return false; + if (_position.x + _size.x <= with._position.x) + return false; + if (_position.y >= with._position.y + with._size.y) + return false; + if (_position.y + _size.y <= with._position.y) + return false; + if (_position.z >= with._position.z + with._size.z) + return false; + if (_position.z + _size.z <= with._position.z) + return false; + } return true; } + /// <summary> + /// Returns true if the AABB is on both sides of `plane`. + /// </summary> + /// <param name="plane">The plane to check for intersection.</param> + /// <returns>A bool for whether or not the AABB intersects the plane.</returns> public bool IntersectsPlane(Plane plane) { Vector3[] points = @@ -335,14 +482,24 @@ namespace Godot for (int i = 0; i < 8; i++) { if (plane.DistanceTo(points[i]) > 0) + { over = true; + } else + { under = true; + } } return under && over; } + /// <summary> + /// Returns true if the AABB intersects the line segment between `from` and `to`. + /// </summary> + /// <param name="from">The start of the line segment.</param> + /// <param name="to">The end of the line segment.</param> + /// <returns>A bool for whether or not the AABB intersects the line segment.</returns> public bool IntersectsSegment(Vector3 from, Vector3 to) { real_t min = 0f; @@ -359,7 +516,9 @@ namespace Godot if (segFrom < segTo) { if (segFrom > boxEnd || segTo < boxBegin) + { return false; + } real_t length = segTo - segFrom; cmin = segFrom < boxBegin ? (boxBegin - segFrom) / length : 0f; @@ -368,7 +527,9 @@ namespace Godot else { if (segTo > boxEnd || segFrom < boxBegin) + { return false; + } real_t length = segTo - segFrom; cmin = segFrom > boxEnd ? (boxEnd - segFrom) / length : 0f; @@ -381,14 +542,23 @@ namespace Godot } if (cmax < max) + { max = cmax; + } if (max < min) + { return false; + } } return true; } + /// <summary> + /// Returns a larger AABB that contains this AABB and `b`. + /// </summary> + /// <param name="with">The other AABB.</param> + /// <returns>The merged AABB.</returns> public AABB Merge(AABB with) { Vector3 beg1 = _position; @@ -411,22 +581,52 @@ namespace Godot return new AABB(min, max - min); } - // Constructors + /// <summary> + /// Constructs an AABB from a position and size. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="size">The size, typically positive.</param> public AABB(Vector3 position, Vector3 size) { _position = position; _size = size; } + + /// <summary> + /// Constructs an AABB from a position, width, height, and depth. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="width">The width, typically positive.</param> + /// <param name="height">The height, typically positive.</param> + /// <param name="depth">The depth, typically positive.</param> public AABB(Vector3 position, real_t width, real_t height, real_t depth) { _position = position; _size = new Vector3(width, height, depth); } + + /// <summary> + /// Constructs an AABB from x, y, z, and size. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="z">The position's Z coordinate.</param> + /// <param name="size">The size, typically positive.</param> public AABB(real_t x, real_t y, real_t z, Vector3 size) { _position = new Vector3(x, y, z); _size = size; } + + /// <summary> + /// Constructs an AABB from x, y, z, width, height, and depth. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="z">The position's Z coordinate.</param> + /// <param name="width">The width, typically positive.</param> + /// <param name="height">The height, typically positive.</param> + /// <param name="depth">The depth, typically positive.</param> public AABB(real_t x, real_t y, real_t z, real_t width, real_t height, real_t depth) { _position = new Vector3(x, y, z); @@ -458,6 +658,12 @@ namespace Godot return _position == other._position && _size == other._size; } + /// <summary> + /// Returns true if this AABB and `other` are approximately equal, by running + /// <see cref="Vector3.IsEqualApprox(Vector3)"/> on each component. + /// </summary> + /// <param name="other">The other AABB to compare.</param> + /// <returns>Whether or not the AABBs are approximately equal.</returns> public bool IsEqualApprox(AABB other) { return _position.IsEqualApprox(other._position) && _size.IsEqualApprox(other._size); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs index baf470a0cc..5aba31c622 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs @@ -8,6 +8,20 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 3×3 matrix used for 3D rotation and scale. + /// Almost always used as an orthogonal basis for a Transform. + /// + /// Contains 3 vector fields X, Y and Z as its columns, which are typically + /// interpreted as the local basis vectors of a 3D transformation. For such use, + /// it is composed of a scaling and a rotation matrix, in that order (M = R.S). + /// + /// Can also be accessed as array of 3D vectors. These vectors are normally + /// orthogonal to each other, but are not necessarily normalized (due to scaling). + /// + /// For more information, read this documentation article: + /// https://docs.godotengine.org/en/latest/tutorials/math/matrices_and_transforms.html + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Basis : IEquatable<Basis> @@ -15,9 +29,9 @@ namespace Godot // NOTE: x, y and z are public-only. Use Column0, Column1 and Column2 internally. /// <summary> - /// Returns the basis matrix’s x vector. - /// This is equivalent to <see cref="Column0"/>. + /// The basis matrix's X vector (column 0). /// </summary> + /// <value>Equivalent to <see cref="Column0"/> and array index `[0]`.</value> public Vector3 x { get => Column0; @@ -25,9 +39,9 @@ namespace Godot } /// <summary> - /// Returns the basis matrix’s y vector. - /// This is equivalent to <see cref="Column1"/>. + /// The basis matrix's Y vector (column 1). /// </summary> + /// <value>Equivalent to <see cref="Column1"/> and array index `[1]`.</value> public Vector3 y { get => Column1; @@ -35,19 +49,40 @@ namespace Godot } /// <summary> - /// Returns the basis matrix’s z vector. - /// This is equivalent to <see cref="Column2"/>. + /// The basis matrix's Z vector (column 2). /// </summary> + /// <value>Equivalent to <see cref="Column2"/> and array index `[2]`.</value> public Vector3 z { get => Column2; set => Column2 = value; } + /// <summary> + /// Row 0 of the basis matrix. Shows which vectors contribute + /// to the X direction. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> public Vector3 Row0; + + /// <summary> + /// Row 1 of the basis matrix. Shows which vectors contribute + /// to the Y direction. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> public Vector3 Row1; + + /// <summary> + /// Row 2 of the basis matrix. Shows which vectors contribute + /// to the Z direction. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> public Vector3 Row2; + /// <summary> + /// Column 0 of the basis matrix (the X vector). + /// </summary> + /// <value>Equivalent to <see cref="x"/> and array index `[0]`.</value> public Vector3 Column0 { get => new Vector3(Row0.x, Row1.x, Row2.x); @@ -58,6 +93,11 @@ namespace Godot this.Row2.x = value.z; } } + + /// <summary> + /// Column 1 of the basis matrix (the Y vector). + /// </summary> + /// <value>Equivalent to <see cref="y"/> and array index `[1]`.</value> public Vector3 Column1 { get => new Vector3(Row0.y, Row1.y, Row2.y); @@ -68,6 +108,11 @@ namespace Godot this.Row2.y = value.z; } } + + /// <summary> + /// Column 2 of the basis matrix (the Z vector). + /// </summary> + /// <value>Equivalent to <see cref="z"/> and array index `[2]`.</value> public Vector3 Column2 { get => new Vector3(Row0.z, Row1.z, Row2.z); @@ -79,6 +124,10 @@ namespace Godot } } + /// <summary> + /// The scale of this basis. + /// </summary> + /// <value>Equivalent to the lengths of each column vector, but negative if the determinant is negative.</value> public Vector3 Scale { get @@ -86,11 +135,18 @@ namespace Godot real_t detSign = Mathf.Sign(Determinant()); return detSign * new Vector3 ( - new Vector3(this.Row0[0], this.Row1[0], this.Row2[0]).Length(), - new Vector3(this.Row0[1], this.Row1[1], this.Row2[1]).Length(), - new Vector3(this.Row0[2], this.Row1[2], this.Row2[2]).Length() + Column0.Length(), + Column1.Length(), + Column2.Length() ); } + set + { + value /= Scale; // Value becomes what's called "delta_scale" in core. + Column0 *= value.x; + Column1 *= value.y; + Column2 *= value.z; + } } /// <summary> @@ -157,8 +213,9 @@ namespace Godot real_t det = orthonormalizedBasis.Determinant(); if (det < 0) { - // Ensure that the determinant is 1, such that result is a proper rotation matrix which can be represented by Euler angles. - orthonormalizedBasis = orthonormalizedBasis.Scaled(Vector3.NegOne); + // Ensure that the determinant is 1, such that result is a proper + // rotation matrix which can be represented by Euler angles. + orthonormalizedBasis = orthonormalizedBasis.Scaled(-Vector3.One); } return orthonormalizedBasis.Quat(); @@ -182,6 +239,15 @@ namespace Godot Row2 = new Vector3(0, 0, diagonal.z); } + /// <summary> + /// Returns the determinant of the basis matrix. If the basis is + /// uniformly scaled, its determinant is the square of the scale. + /// + /// A negative determinant means the basis has a negative scale. + /// A zero determinant means the basis isn't invertible, + /// and is usually considered invalid. + /// </summary> + /// <returns>The determinant of the basis matrix.</returns> public real_t Determinant() { real_t cofac00 = Row1[1] * Row2[2] - Row1[2] * Row2[1]; @@ -191,6 +257,16 @@ namespace Godot return Row0[0] * cofac00 + Row0[1] * cofac10 + Row0[2] * cofac20; } + /// <summary> + /// Returns the basis's rotation in the form of Euler angles + /// (in the YXZ convention: when *decomposing*, first Z, then X, and Y last). + /// The returned vector contains the rotation angles in + /// the format (X angle, Y angle, Z angle). + /// + /// Consider using the <see cref="Basis.Quat()"/> method instead, which + /// returns a <see cref="Godot.Quat"/> quaternion instead of Euler angles. + /// </summary> + /// <returns>A Vector3 representing the basis rotation in Euler angles.</returns> public Vector3 GetEuler() { Basis m = Orthonormalized(); @@ -223,6 +299,12 @@ namespace Godot return euler; } + /// <summary> + /// Get rows by index. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> + /// <param name="index">Which row.</param> + /// <returns>One of `Row0`, `Row1`, or `Row2`.</returns> public Vector3 GetRow(int index) { switch (index) @@ -238,6 +320,12 @@ namespace Godot } } + /// <summary> + /// Sets rows by index. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> + /// <param name="index">Which row.</param> + /// <param name="value">The vector to set the row to.</param> public void SetRow(int index, Vector3 value) { switch (index) @@ -256,16 +344,16 @@ namespace Godot } } - public Vector3 GetColumn(int index) - { - return this[index]; - } - - public void SetColumn(int index, Vector3 value) - { - this[index] = value; - } - + /// <summary> + /// This function considers a discretization of rotations into + /// 24 points on unit sphere, lying along the vectors (x, y, z) with + /// each component being either -1, 0, or 1, and returns the index + /// of the point best representing the orientation of the object. + /// It is mainly used by the <see cref="GridMap"/> editor. + /// + /// For further details, refer to the Godot source code. + /// </summary> + /// <returns>The orthogonal index.</returns> public int GetOrthogonalIndex() { var orth = this; @@ -279,11 +367,17 @@ namespace Godot real_t v = row[j]; if (v > 0.5f) + { v = 1.0f; + } else if (v < -0.5f) + { v = -1.0f; + } else + { v = 0f; + } row[j] = v; @@ -294,12 +388,18 @@ namespace Godot for (int i = 0; i < 24; i++) { if (orth == _orthoBases[i]) + { return i; + } } return 0; } + /// <summary> + /// Returns the inverse of the matrix. + /// </summary> + /// <returns>The inverse matrix.</returns> public Basis Inverse() { real_t cofac00 = Row1[1] * Row2[2] - Row1[2] * Row2[1]; @@ -309,7 +409,9 @@ namespace Godot real_t det = Row0[0] * cofac00 + Row0[1] * cofac10 + Row0[2] * cofac20; if (det == 0) + { throw new InvalidOperationException("Matrix determinant is zero and cannot be inverted."); + } real_t detInv = 1.0f / det; @@ -328,11 +430,17 @@ namespace Godot ); } + /// <summary> + /// Returns the orthonormalized version of the basis matrix (useful to + /// call occasionally to avoid rounding errors for orthogonal matrices). + /// This performs a Gram-Schmidt orthonormalization on the basis of the matrix. + /// </summary> + /// <returns>An orthonormalized basis matrix.</returns> public Basis Orthonormalized() { - Vector3 column0 = GetColumn(0); - Vector3 column1 = GetColumn(1); - Vector3 column2 = GetColumn(2); + Vector3 column0 = this[0]; + Vector3 column1 = this[1]; + Vector3 column2 = this[2]; column0.Normalize(); column1 = column1 - column0 * column0.Dot(column1); @@ -343,48 +451,86 @@ namespace Godot return new Basis(column0, column1, column2); } + /// <summary> + /// Introduce an additional rotation around the given `axis` + /// by `phi` (in radians). The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate, in radians.</param> + /// <returns>The rotated basis matrix.</returns> public Basis Rotated(Vector3 axis, real_t phi) { return new Basis(axis, phi) * this; } + /// <summary> + /// Introduce an additional scaling specified by the given 3D scaling factor. + /// </summary> + /// <param name="scale">The scale to introduce.</param> + /// <returns>The scaled basis matrix.</returns> public Basis Scaled(Vector3 scale) { - var b = this; + Basis b = this; b.Row0 *= scale.x; b.Row1 *= scale.y; b.Row2 *= scale.z; return b; } - public Basis Slerp(Basis target, real_t t) + /// <summary> + /// Assuming that the matrix is a proper rotation matrix, slerp performs + /// a spherical-linear interpolation with another rotation matrix. + /// </summary> + /// <param name="target">The destination basis for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting basis matrix of the interpolation.</returns> + public Basis Slerp(Basis target, real_t weight) { - var from = new Quat(this); - var to = new Quat(target); + Quat from = new Quat(this); + Quat to = new Quat(target); - var b = new Basis(from.Slerp(to, t)); - b.Row0 *= Mathf.Lerp(Row0.Length(), target.Row0.Length(), t); - b.Row1 *= Mathf.Lerp(Row1.Length(), target.Row1.Length(), t); - b.Row2 *= Mathf.Lerp(Row2.Length(), target.Row2.Length(), t); + Basis b = new Basis(from.Slerp(to, weight)); + b.Row0 *= Mathf.Lerp(Row0.Length(), target.Row0.Length(), weight); + b.Row1 *= Mathf.Lerp(Row1.Length(), target.Row1.Length(), weight); + b.Row2 *= Mathf.Lerp(Row2.Length(), target.Row2.Length(), weight); return b; } + /// <summary> + /// Transposed dot product with the X axis of the matrix. + /// </summary> + /// <param name="with">A vector to calculate the dot product with.</param> + /// <returns>The resulting dot product.</returns> public real_t Tdotx(Vector3 with) { return this.Row0[0] * with[0] + this.Row1[0] * with[1] + this.Row2[0] * with[2]; } + /// <summary> + /// Transposed dot product with the Y axis of the matrix. + /// </summary> + /// <param name="with">A vector to calculate the dot product with.</param> + /// <returns>The resulting dot product.</returns> public real_t Tdoty(Vector3 with) { return this.Row0[1] * with[0] + this.Row1[1] * with[1] + this.Row2[1] * with[2]; } + /// <summary> + /// Transposed dot product with the Z axis of the matrix. + /// </summary> + /// <param name="with">A vector to calculate the dot product with.</param> + /// <returns>The resulting dot product.</returns> public real_t Tdotz(Vector3 with) { return this.Row0[2] * with[0] + this.Row1[2] * with[1] + this.Row2[2] * with[2]; } + /// <summary> + /// Returns the transposed version of the basis matrix. + /// </summary> + /// <returns>The transposed basis matrix.</returns> public Basis Transposed() { var tr = this; @@ -404,6 +550,11 @@ namespace Godot return tr; } + /// <summary> + /// Returns a vector transformed (multiplied) by the basis matrix. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector3 Xform(Vector3 v) { return new Vector3 @@ -414,6 +565,14 @@ namespace Godot ); } + /// <summary> + /// Returns a vector transformed (multiplied) by the transposed basis matrix. + /// + /// Note: This results in a multiplication by the inverse of the + /// basis matrix only if it represents a rotation-reflection. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector3 XformInv(Vector3 v) { return new Vector3 @@ -424,6 +583,12 @@ namespace Godot ); } + /// <summary> + /// Returns the basis's rotation in the form of a quaternion. + /// See <see cref="GetEuler()"/> if you need Euler angles, but keep in + /// mind that quaternions should generally be preferred to Euler angles. + /// </summary> + /// <returns>A <see cref="Godot.Quat"/> representing the basis's rotation.</returns> public Quat Quat() { real_t trace = Row0[0] + Row1[1] + Row2[2]; @@ -508,11 +673,33 @@ namespace Godot private static readonly Basis _flipY = new Basis(1, 0, 0, 0, -1, 0, 0, 0, 1); private static readonly Basis _flipZ = new Basis(1, 0, 0, 0, 1, 0, 0, 0, -1); + /// <summary> + /// The identity basis, with no rotation or scaling applied. + /// This is used as a replacement for `Basis()` in GDScript. + /// Do not use `new Basis()` with no arguments in C#, because it sets all values to zero. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Right, Vector3.Up, Vector3.Back)`.</value> public static Basis Identity { get { return _identity; } } + /// <summary> + /// The basis that will flip something along the X axis when used in a transformation. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Left, Vector3.Up, Vector3.Back)`.</value> public static Basis FlipX { get { return _flipX; } } + /// <summary> + /// The basis that will flip something along the Y axis when used in a transformation. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Right, Vector3.Down, Vector3.Back)`.</value> public static Basis FlipY { get { return _flipY; } } + /// <summary> + /// The basis that will flip something along the Z axis when used in a transformation. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Right, Vector3.Up, Vector3.Forward)`.</value> public static Basis FlipZ { get { return _flipZ; } } + /// <summary> + /// Constructs a pure rotation basis matrix from the given quaternion. + /// </summary> + /// <param name="quat">The quaternion to create the basis from.</param> public Basis(Quat quat) { real_t s = 2.0f / quat.LengthSquared; @@ -535,26 +722,41 @@ namespace Godot Row2 = new Vector3(xz - wy, yz + wx, 1.0f - (xx + yy)); } - public Basis(Vector3 euler) + /// <summary> + /// Constructs a pure rotation basis matrix from the given Euler angles + /// (in the YXZ convention: when *composing*, first Y, then X, and Z last), + /// given in the vector format as (X angle, Y angle, Z angle). + /// + /// Consider using the <see cref="Basis(Quat)"/> constructor instead, which + /// uses a <see cref="Godot.Quat"/> quaternion instead of Euler angles. + /// </summary> + /// <param name="eulerYXZ">The Euler angles to create the basis from.</param> + public Basis(Vector3 eulerYXZ) { real_t c; real_t s; - c = Mathf.Cos(euler.x); - s = Mathf.Sin(euler.x); + c = Mathf.Cos(eulerYXZ.x); + s = Mathf.Sin(eulerYXZ.x); var xmat = new Basis(1, 0, 0, 0, c, -s, 0, s, c); - c = Mathf.Cos(euler.y); - s = Mathf.Sin(euler.y); + c = Mathf.Cos(eulerYXZ.y); + s = Mathf.Sin(eulerYXZ.y); var ymat = new Basis(c, 0, s, 0, 1, 0, -s, 0, c); - c = Mathf.Cos(euler.z); - s = Mathf.Sin(euler.z); + c = Mathf.Cos(eulerYXZ.z); + s = Mathf.Sin(eulerYXZ.z); var zmat = new Basis(c, -s, 0, s, c, 0, 0, 0, 1); this = ymat * xmat * zmat; } + /// <summary> + /// Constructs a pure rotation basis matrix, rotated around the given `axis` + /// by `phi` (in radians). The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate, in radians.</param> public Basis(Vector3 axis, real_t phi) { Vector3 axisSq = new Vector3(axis.x * axis.x, axis.y * axis.y, axis.z * axis.z); @@ -582,6 +784,12 @@ namespace Godot Row2.y = xyzt + zyxs; } + /// <summary> + /// Constructs a basis matrix from 3 axis vectors (matrix columns). + /// </summary> + /// <param name="column0">The X vector, or Column0.</param> + /// <param name="column1">The Y vector, or Column1.</param> + /// <param name="column2">The Z vector, or Column2.</param> public Basis(Vector3 column0, Vector3 column1, Vector3 column2) { Row0 = new Vector3(column0.x, column1.x, column2.x); @@ -637,6 +845,12 @@ namespace Godot return Row0.Equals(other.Row0) && Row1.Equals(other.Row1) && Row2.Equals(other.Row2); } + /// <summary> + /// Returns true if this basis and `other` are approximately equal, by running + /// <see cref="Vector3.IsEqualApprox(Vector3)"/> on each component. + /// </summary> + /// <param name="other">The other basis to compare.</param> + /// <returns>Whether or not the matrices are approximately equal.</returns> public bool IsEqualApprox(Basis other) { return Row0.IsEqualApprox(other.Row0) && Row1.IsEqualApprox(other.Row1) && Row2.IsEqualApprox(other.Row2); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs index 1d1a49945f..d851abc6d3 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs @@ -3,15 +3,44 @@ using System.Runtime.InteropServices; namespace Godot { + /// <summary> + /// A color represented by red, green, blue, and alpha (RGBA) components. + /// The alpha component is often used for transparency. + /// Values are in floating-point and usually range from 0 to 1. + /// Some properties (such as CanvasItem.modulate) may accept values + /// greater than 1 (overbright or HDR colors). + /// + /// If you want to supply values in a range of 0 to 255, you should use + /// <see cref="Color8"/> and the `r8`/`g8`/`b8`/`a8` properties. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Color : IEquatable<Color> { + /// <summary> + /// The color's red component, typically on the range of 0 to 1. + /// </summary> public float r; + + /// <summary> + /// The color's green component, typically on the range of 0 to 1. + /// </summary> public float g; + + /// <summary> + /// The color's blue component, typically on the range of 0 to 1. + /// </summary> public float b; + + /// <summary> + /// The color's alpha (transparency) component, typically on the range of 0 to 1. + /// </summary> public float a; + /// <summary> + /// Wrapper for <see cref="r"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int r8 { get @@ -24,6 +53,10 @@ namespace Godot } } + /// <summary> + /// Wrapper for <see cref="g"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int g8 { get @@ -36,6 +69,10 @@ namespace Godot } } + /// <summary> + /// Wrapper for <see cref="b"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int b8 { get @@ -48,6 +85,10 @@ namespace Godot } } + /// <summary> + /// Wrapper for <see cref="a"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int a8 { get @@ -60,6 +101,10 @@ namespace Godot } } + /// <summary> + /// The HSV hue of this color, on the range 0 to 1. + /// </summary> + /// <value>Getting is a long process, refer to the source code for details. Setting uses <see cref="FromHsv"/>.</value> public float h { get @@ -70,21 +115,31 @@ namespace Godot float delta = max - min; if (delta == 0) + { return 0; + } float h; if (r == max) + { h = (g - b) / delta; // Between yellow & magenta + } else if (g == max) + { h = 2 + (b - r) / delta; // Between cyan & yellow + } else + { h = 4 + (r - g) / delta; // Between magenta & cyan + } h /= 6.0f; if (h < 0) + { h += 1.0f; + } return h; } @@ -94,6 +149,10 @@ namespace Godot } } + /// <summary> + /// The HSV saturation of this color, on the range 0 to 1. + /// </summary> + /// <value>Getting is equivalent to the ratio between the min and max RGB value. Setting uses <see cref="FromHsv"/>.</value> public float s { get @@ -103,7 +162,7 @@ namespace Godot float delta = max - min; - return max != 0 ? delta / max : 0; + return max == 0 ? 0 : delta / max; } set { @@ -111,6 +170,10 @@ namespace Godot } } + /// <summary> + /// The HSV value (brightness) of this color, on the range 0 to 1. + /// </summary> + /// <value>Getting is equivalent to using `Max()` on the RGB components. Setting uses <see cref="FromHsv"/>.</value> public float v { get @@ -123,25 +186,10 @@ namespace Godot } } - public static Color ColorN(string name, float alpha = 1f) - { - name = name.Replace(" ", String.Empty); - name = name.Replace("-", String.Empty); - name = name.Replace("_", String.Empty); - name = name.Replace("'", String.Empty); - name = name.Replace(".", String.Empty); - name = name.ToLower(); - - if (!Colors.namedColors.ContainsKey(name)) - { - throw new ArgumentOutOfRangeException($"Invalid Color Name: {name}"); - } - - Color color = Colors.namedColors[name]; - color.a = alpha; - return color; - } - + /// <summary> + /// Access color components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.r`, `[1]` is equivalent to `.g`, `[2]` is equivalent to `.b`, `[3]` is equivalent to `.a`.</value> public float this[int index] { get @@ -182,73 +230,13 @@ namespace Godot } } - public void ToHsv(out float hue, out float saturation, out float value) - { - float max = (float)Mathf.Max(r, Mathf.Max(g, b)); - float min = (float)Mathf.Min(r, Mathf.Min(g, b)); - - float delta = max - min; - - if (delta == 0) - { - hue = 0; - } - else - { - if (r == max) - hue = (g - b) / delta; // Between yellow & magenta - else if (g == max) - hue = 2 + (b - r) / delta; // Between cyan & yellow - else - hue = 4 + (r - g) / delta; // Between magenta & cyan - - hue /= 6.0f; - - if (hue < 0) - hue += 1.0f; - } - - saturation = max == 0 ? 0 : 1f - 1f * min / max; - value = max; - } - - public static Color FromHsv(float hue, float saturation, float value, float alpha = 1.0f) - { - if (saturation == 0) - { - // acp_hromatic (grey) - return new Color(value, value, value, alpha); - } - - int i; - float f, p, q, t; - - hue *= 6.0f; - hue %= 6f; - i = (int)hue; - - f = hue - i; - p = value * (1 - saturation); - q = value * (1 - saturation * f); - t = value * (1 - saturation * (1 - f)); - - switch (i) - { - case 0: // Red is the dominant color - return new Color(value, t, p, alpha); - case 1: // Green is the dominant color - return new Color(q, value, p, alpha); - case 2: - return new Color(p, value, t, alpha); - case 3: // Blue is the dominant color - return new Color(p, q, value, alpha); - case 4: - return new Color(t, p, value, alpha); - default: // (5) Red is the dominant color - return new Color(value, p, q, alpha); - } - } - + /// <summary> + /// Returns a new color resulting from blending this color over another. + /// If the color is opaque, the result is also opaque. + /// The second color may have a range of alpha values. + /// </summary> + /// <param name="over">The color to blend over.</param> + /// <returns>This color blended over `over`.</returns> public Color Blend(Color over) { Color res; @@ -268,6 +256,10 @@ namespace Godot return res; } + /// <summary> + /// Returns the most contrasting color. + /// </summary> + /// <returns>The most contrasting color</returns> public Color Contrasted() { return new Color( @@ -278,6 +270,12 @@ namespace Godot ); } + /// <summary> + /// Returns a new color resulting from making this color darker + /// by the specified ratio (on the range of 0 to 1). + /// </summary> + /// <param name="amount">The ratio to darken by.</param> + /// <returns>The darkened color.</returns> public Color Darkened(float amount) { Color res = this; @@ -287,6 +285,10 @@ namespace Godot return res; } + /// <summary> + /// Returns the inverted color: `(1 - r, 1 - g, 1 - b, a)`. + /// </summary> + /// <returns>The inverted color.</returns> public Color Inverted() { return new Color( @@ -297,6 +299,12 @@ namespace Godot ); } + /// <summary> + /// Returns a new color resulting from making this color lighter + /// by the specified ratio (on the range of 0 to 1). + /// </summary> + /// <param name="amount">The ratio to lighten by.</param> + /// <returns>The darkened color.</returns> public Color Lightened(float amount) { Color res = this; @@ -306,18 +314,48 @@ namespace Godot return res; } - public Color LinearInterpolate(Color c, float t) + /// <summary> + /// Returns the result of the linear interpolation between + /// this color and `to` by amount `weight`. + /// </summary> + /// <param name="to">The destination color for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting color of the interpolation.</returns> + public Color Lerp(Color to, float weight) { - var res = this; - - res.r += t * (c.r - r); - res.g += t * (c.g - g); - res.b += t * (c.b - b); - res.a += t * (c.a - a); + return new Color + ( + Mathf.Lerp(r, to.r, weight), + Mathf.Lerp(g, to.g, weight), + Mathf.Lerp(b, to.b, weight), + Mathf.Lerp(a, to.a, weight) + ); + } - return res; + /// <summary> + /// Returns the result of the linear interpolation between + /// this color and `to` by color amount `weight`. + /// </summary> + /// <param name="to">The destination color for interpolation.</param> + /// <param name="weight">A color with components on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting color of the interpolation.</returns> + public Color Lerp(Color to, Color weight) + { + return new Color + ( + Mathf.Lerp(r, to.r, weight.r), + Mathf.Lerp(g, to.g, weight.g), + Mathf.Lerp(b, to.b, weight.b), + Mathf.Lerp(a, to.a, weight.a) + ); } + /// <summary> + /// Returns the color's 32-bit integer in ABGR format + /// (each byte represents a component of the ABGR profile). + /// ABGR is the reversed version of the default format. + /// </summary> + /// <returns>A uint representing this color in ABGR32 format.</returns> public uint ToAbgr32() { uint c = (byte)Math.Round(a * 255); @@ -331,6 +369,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 64-bit integer in ABGR format + /// (each word represents a component of the ABGR profile). + /// ABGR is the reversed version of the default format. + /// </summary> + /// <returns>A ulong representing this color in ABGR64 format.</returns> public ulong ToAbgr64() { ulong c = (ushort)Math.Round(a * 65535); @@ -344,6 +388,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 32-bit integer in ARGB format + /// (each byte represents a component of the ARGB profile). + /// ARGB is more compatible with DirectX, but not used much in Godot. + /// </summary> + /// <returns>A uint representing this color in ARGB32 format.</returns> public uint ToArgb32() { uint c = (byte)Math.Round(a * 255); @@ -357,6 +407,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 64-bit integer in ARGB format + /// (each word represents a component of the ARGB profile). + /// ARGB is more compatible with DirectX, but not used much in Godot. + /// </summary> + /// <returns>A ulong representing this color in ARGB64 format.</returns> public ulong ToArgb64() { ulong c = (ushort)Math.Round(a * 65535); @@ -370,6 +426,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 32-bit integer in RGBA format + /// (each byte represents a component of the RGBA profile). + /// RGBA is Godot's default and recommended format. + /// </summary> + /// <returns>A uint representing this color in RGBA32 format.</returns> public uint ToRgba32() { uint c = (byte)Math.Round(r * 255); @@ -383,6 +445,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 64-bit integer in RGBA format + /// (each word represents a component of the RGBA profile). + /// RGBA is Godot's default and recommended format. + /// </summary> + /// <returns>A ulong representing this color in RGBA64 format.</returns> public ulong ToRgba64() { ulong c = (ushort)Math.Round(r * 65535); @@ -396,6 +464,11 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's HTML hexadecimal color string in RGBA format. + /// </summary> + /// <param name="includeAlpha">Whether or not to include alpha. If false, the color is RGB instead of RGBA.</param> + /// <returns>A string for the HTML hexadecimal representation of this color.</returns> public string ToHtml(bool includeAlpha = true) { var txt = string.Empty; @@ -405,12 +478,20 @@ namespace Godot txt += ToHex32(b); if (includeAlpha) - txt = ToHex32(a) + txt; + { + txt += ToHex32(a); + } return txt; } - // Constructors + /// <summary> + /// Constructs a color from RGBA values on the range of 0 to 1. + /// </summary> + /// <param name="r">The color's red component, typically on the range of 0 to 1.</param> + /// <param name="g">The color's green component, typically on the range of 0 to 1.</param> + /// <param name="b">The color's blue component, typically on the range of 0 to 1.</param> + /// <param name="a">The color's alpha (transparency) value, typically on the range of 0 to 1. Default: 1.</param> public Color(float r, float g, float b, float a = 1.0f) { this.r = r; @@ -419,6 +500,24 @@ namespace Godot this.a = a; } + /// <summary> + /// Constructs a color from an existing color and an alpha value. + /// </summary> + /// <param name="c">The color to construct from. Only its RGB values are used.</param> + /// <param name="a">The color's alpha (transparency) value, typically on the range of 0 to 1. Default: 1.</param> + public Color(Color c, float a = 1.0f) + { + r = c.r; + g = c.g; + b = c.b; + this.a = a; + } + + /// <summary> + /// Constructs a color from a 32-bit integer + /// (each byte represents a component of the RGBA profile). + /// </summary> + /// <param name="rgba">The uint representing the color.</param> public Color(uint rgba) { a = (rgba & 0xFF) / 255.0f; @@ -430,6 +529,11 @@ namespace Godot r = (rgba & 0xFF) / 255.0f; } + /// <summary> + /// Constructs a color from a 64-bit integer + /// (each word represents a component of the RGBA profile). + /// </summary> + /// <param name="rgba">The ulong representing the color.</param> public Color(ulong rgba) { a = (rgba & 0xFFFF) / 65535.0f; @@ -441,6 +545,212 @@ namespace Godot r = (rgba & 0xFFFF) / 65535.0f; } + /// <summary> + /// Constructs a color from the HTML hexadecimal color string in RGBA format. + /// </summary> + /// <param name="rgba">A string for the HTML hexadecimal representation of this color.</param> + public Color(string rgba) + { + if (rgba.Length == 0) + { + r = 0f; + g = 0f; + b = 0f; + a = 1.0f; + return; + } + + if (rgba[0] == '#') + { + rgba = rgba.Substring(1); + } + + bool alpha; + + if (rgba.Length == 8) + { + alpha = true; + } + else if (rgba.Length == 6) + { + alpha = false; + } + else + { + throw new ArgumentOutOfRangeException("Invalid color code. Length is " + rgba.Length + " but a length of 6 or 8 is expected: " + rgba); + } + + if (alpha) + { + a = ParseCol8(rgba, 6) / 255f; + + if (a < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Alpha part is not valid hexadecimal: " + rgba); + } + } + else + { + a = 1.0f; + } + + int from = alpha ? 2 : 0; + + r = ParseCol8(rgba, 0) / 255f; + + if (r < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Red part is not valid hexadecimal: " + rgba); + } + + g = ParseCol8(rgba, 2) / 255f; + + if (g < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Green part is not valid hexadecimal: " + rgba); + } + + b = ParseCol8(rgba, 4) / 255f; + + if (b < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Blue part is not valid hexadecimal: " + rgba); + } + } + + /// <summary> + /// Returns a color constructed from integer red, green, blue, and alpha channels. + /// Each channel should have 8 bits of information ranging from 0 to 255. + /// </summary> + /// <param name="r8">The red component represented on the range of 0 to 255.</param> + /// <param name="g8">The green component represented on the range of 0 to 255.</param> + /// <param name="b8">The blue component represented on the range of 0 to 255.</param> + /// <param name="a8">The alpha (transparency) component represented on the range of 0 to 255.</param> + /// <returns>The constructed color.</returns> + public static Color Color8(byte r8, byte g8, byte b8, byte a8 = 255) + { + return new Color(r8 / 255f, g8 / 255f, b8 / 255f, a8 / 255f); + } + + /// <summary> + /// Returns a color according to the standardized name, with the + /// specified alpha value. Supported color names are the same as + /// the constants defined in <see cref="Colors"/>. + /// </summary> + /// <param name="name">The name of the color.</param> + /// <param name="alpha">The alpha (transparency) component represented on the range of 0 to 1. Default: 1.</param> + /// <returns>The constructed color.</returns> + public static Color ColorN(string name, float alpha = 1f) + { + name = name.Replace(" ", String.Empty); + name = name.Replace("-", String.Empty); + name = name.Replace("_", String.Empty); + name = name.Replace("'", String.Empty); + name = name.Replace(".", String.Empty); + name = name.ToLower(); + + if (!Colors.namedColors.ContainsKey(name)) + { + throw new ArgumentOutOfRangeException($"Invalid Color Name: {name}"); + } + + Color color = Colors.namedColors[name]; + color.a = alpha; + return color; + } + + /// <summary> + /// Constructs a color from an HSV profile, with values on the + /// range of 0 to 1. This is equivalent to using each of + /// the `h`/`s`/`v` properties, but much more efficient. + /// </summary> + /// <param name="hue">The HSV hue, typically on the range of 0 to 1.</param> + /// <param name="saturation">The HSV saturation, typically on the range of 0 to 1.</param> + /// <param name="value">The HSV value (brightness), typically on the range of 0 to 1.</param> + /// <param name="alpha">The alpha (transparency) value, typically on the range of 0 to 1.</param> + /// <returns>The constructed color.</returns> + public static Color FromHsv(float hue, float saturation, float value, float alpha = 1.0f) + { + if (saturation == 0) + { + // acp_hromatic (grey) + return new Color(value, value, value, alpha); + } + + int i; + float f, p, q, t; + + hue *= 6.0f; + hue %= 6f; + i = (int)hue; + + f = hue - i; + p = value * (1 - saturation); + q = value * (1 - saturation * f); + t = value * (1 - saturation * (1 - f)); + + switch (i) + { + case 0: // Red is the dominant color + return new Color(value, t, p, alpha); + case 1: // Green is the dominant color + return new Color(q, value, p, alpha); + case 2: + return new Color(p, value, t, alpha); + case 3: // Blue is the dominant color + return new Color(p, q, value, alpha); + case 4: + return new Color(t, p, value, alpha); + default: // (5) Red is the dominant color + return new Color(value, p, q, alpha); + } + } + + /// <summary> + /// Converts a color to HSV values. This is equivalent to using each of + /// the `h`/`s`/`v` properties, but much more efficient. + /// </summary> + /// <param name="hue">Output parameter for the HSV hue.</param> + /// <param name="saturation">Output parameter for the HSV saturation.</param> + /// <param name="value">Output parameter for the HSV value.</param> + public void ToHsv(out float hue, out float saturation, out float value) + { + float max = (float)Mathf.Max(r, Mathf.Max(g, b)); + float min = (float)Mathf.Min(r, Mathf.Min(g, b)); + + float delta = max - min; + + if (delta == 0) + { + hue = 0; + } + else + { + if (r == max) + { + hue = (g - b) / delta; // Between yellow & magenta + } + else if (g == max) + { + hue = 2 + (b - r) / delta; // Between cyan & yellow + } + else + { + hue = 4 + (r - g) / delta; // Between magenta & cyan + } + + hue /= 6.0f; + + if (hue < 0) + { + hue += 1.0f; + } + } + + saturation = max == 0 ? 0 : 1f - 1f * min / max; + value = max; + } + private static int ParseCol8(string str, int ofs) { int ig = 0; @@ -470,9 +780,13 @@ namespace Godot } if (i == 0) + { ig += v * 16; + } else + { ig += v; + } } return ig; @@ -490,9 +804,13 @@ namespace Godot int lv = v & 0xF; if (lv < 10) + { c = (char)('0' + lv); + } else + { c = (char)('a' + lv - 10); + } v >>= 4; ret = c + ret; @@ -504,10 +822,14 @@ namespace Godot internal static bool HtmlIsValid(string color) { if (color.Length == 0) + { return false; + } if (color[0] == '#') + { color = color.Substring(1, color.Length - 1); + } bool alpha; @@ -526,83 +848,27 @@ namespace Godot if (alpha) { if (ParseCol8(color, 0) < 0) + { return false; + } } int from = alpha ? 2 : 0; if (ParseCol8(color, from + 0) < 0) - return false; - if (ParseCol8(color, from + 2) < 0) - return false; - if (ParseCol8(color, from + 4) < 0) - return false; - - return true; - } - - public static Color Color8(byte r8, byte g8, byte b8, byte a8 = 255) - { - return new Color(r8 / 255f, g8 / 255f, b8 / 255f, a8 / 255f); - } - - public Color(string rgba) - { - if (rgba.Length == 0) { - r = 0f; - g = 0f; - b = 0f; - a = 1.0f; - return; - } - - if (rgba[0] == '#') - rgba = rgba.Substring(1); - - bool alpha; - - if (rgba.Length == 8) - { - alpha = true; - } - else if (rgba.Length == 6) - { - alpha = false; - } - else - { - throw new ArgumentOutOfRangeException("Invalid color code. Length is " + rgba.Length + " but a length of 6 or 8 is expected: " + rgba); + return false; } - - if (alpha) + if (ParseCol8(color, from + 2) < 0) { - a = ParseCol8(rgba, 0) / 255f; - - if (a < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Alpha part is not valid hexadecimal: " + rgba); + return false; } - else + if (ParseCol8(color, from + 4) < 0) { - a = 1.0f; + return false; } - int from = alpha ? 2 : 0; - - r = ParseCol8(rgba, from + 0) / 255f; - - if (r < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Red part is not valid hexadecimal: " + rgba); - - g = ParseCol8(rgba, from + 2) / 255f; - - if (g < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Green part is not valid hexadecimal: " + rgba); - - b = ParseCol8(rgba, from + 4) / 255f; - - if (b < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Blue part is not valid hexadecimal: " + rgba); + return true; } public static Color operator +(Color left, Color right) @@ -690,13 +956,13 @@ namespace Godot if (Mathf.IsEqualApprox(left.g, right.g)) { if (Mathf.IsEqualApprox(left.b, right.b)) + { return left.a < right.a; + } return left.b < right.b; } - return left.g < right.g; } - return left.r < right.r; } @@ -707,13 +973,13 @@ namespace Godot if (Mathf.IsEqualApprox(left.g, right.g)) { if (Mathf.IsEqualApprox(left.b, right.b)) + { return left.a > right.a; + } return left.b > right.b; } - return left.g > right.g; } - return left.r > right.r; } @@ -732,6 +998,12 @@ namespace Godot return r == other.r && g == other.g && b == other.b && a == other.a; } + /// <summary> + /// Returns true if this color and `other` are approximately equal, by running + /// <see cref="Godot.Mathf.IsEqualApprox(float, float)"/> on each component. + /// </summary> + /// <param name="other">The other color to compare.</param> + /// <returns>Whether or not the colors are approximately equal.</returns> public bool IsEqualApprox(Color other) { return Mathf.IsEqualApprox(r, other.r) && Mathf.IsEqualApprox(g, other.g) && Mathf.IsEqualApprox(b, other.b) && Mathf.IsEqualApprox(a, other.a); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs index f41f5e9fc8..d05a0414aa 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs @@ -3,6 +3,10 @@ using System.Collections.Generic; namespace Godot { + /// <summary> + /// This class contains color constants created from standardized color names. + /// The standardized color set is based on the X11 and .NET color names. + /// </summary> public static class Colors { // Color names and values are derived from core/color_names.inc diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/SceneTreeExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/SceneTreeExtensions.cs new file mode 100644 index 0000000000..20b11a48dd --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/SceneTreeExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.CompilerServices; +using Godot.Collections; + +namespace Godot +{ + public partial class SceneTree + { + public Array<T> GetNodesInGroup<T>(StringName group) where T : class + { + return new Array<T>(godot_icall_SceneTree_get_nodes_in_group_Generic(Object.GetPtr(this), StringName.GetPtr(group), typeof(T))); + } + + [MethodImpl(MethodImplOptions.InternalCall)] + internal extern static IntPtr godot_icall_SceneTree_get_nodes_in_group_Generic(IntPtr obj, IntPtr group, Type elemType); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/GD.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/GD.cs index 9384da0e48..e050d1fdd1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/GD.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/GD.cs @@ -84,7 +84,7 @@ namespace Godot public static void Print(params object[] what) { - godot_icall_GD_print(Array.ConvertAll(what, x => x?.ToString())); + godot_icall_GD_print(Array.ConvertAll(what ?? new object[]{"null"}, x => x != null ? x.ToString() : "null")); } public static void PrintStack() @@ -94,22 +94,22 @@ namespace Godot public static void PrintErr(params object[] what) { - godot_icall_GD_printerr(Array.ConvertAll(what, x => x?.ToString())); + godot_icall_GD_printerr(Array.ConvertAll(what ?? new object[]{"null"}, x => x != null ? x.ToString() : "null")); } public static void PrintRaw(params object[] what) { - godot_icall_GD_printraw(Array.ConvertAll(what, x => x?.ToString())); + godot_icall_GD_printraw(Array.ConvertAll(what ?? new object[]{"null"}, x => x != null ? x.ToString() : "null")); } public static void PrintS(params object[] what) { - godot_icall_GD_prints(Array.ConvertAll(what, x => x?.ToString())); + godot_icall_GD_prints(Array.ConvertAll(what ?? new object[]{"null"}, x => x != null ? x.ToString() : "null")); } public static void PrintT(params object[] what) { - godot_icall_GD_printt(Array.ConvertAll(what, x => x?.ToString())); + godot_icall_GD_printt(Array.ConvertAll(what ?? new object[]{"null"}, x => x != null ? x.ToString() : "null")); } public static float Randf() diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs index 4f7aa99df8..6eecc262d6 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs @@ -11,79 +11,185 @@ namespace Godot { // Define constants with Decimal precision and cast down to double or float. + /// <summary> + /// The circle constant, the circumference of the unit circle in radians. + /// </summary> public const real_t Tau = (real_t) 6.2831853071795864769252867666M; // 6.2831855f and 6.28318530717959 + + /// <summary> + /// Constant that represents how many times the diameter of a circle + /// fits around its perimeter. This is equivalent to `Mathf.Tau / 2`. + /// </summary> public const real_t Pi = (real_t) 3.1415926535897932384626433833M; // 3.1415927f and 3.14159265358979 + + /// <summary> + /// Positive infinity. For negative infinity, use `-Mathf.Inf`. + /// </summary> public const real_t Inf = real_t.PositiveInfinity; + + /// <summary> + /// "Not a Number", an invalid value. `NaN` has special properties, including + /// that it is not equal to itself. It is output by some invalid operations, + /// such as dividing zero by zero. + /// </summary> public const real_t NaN = real_t.NaN; private const real_t Deg2RadConst = (real_t) 0.0174532925199432957692369077M; // 0.0174532924f and 0.0174532925199433 private const real_t Rad2DegConst = (real_t) 57.295779513082320876798154814M; // 57.29578f and 57.2957795130823 + /// <summary> + /// Returns the absolute value of `s` (i.e. positive value). + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>The absolute value of `s`.</returns> public static int Abs(int s) { return Math.Abs(s); } + /// <summary> + /// Returns the absolute value of `s` (i.e. positive value). + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>The absolute value of `s`.</returns> public static real_t Abs(real_t s) { return Math.Abs(s); } + /// <summary> + /// Returns the arc cosine of `s` in radians. Use to get the angle of cosine s. + /// </summary> + /// <param name="s">The input cosine value. Must be on the range of -1.0 to 1.0.</param> + /// <returns>An angle that would result in the given cosine value. On the range `0` to `Tau/2`.</returns> public static real_t Acos(real_t s) { return (real_t)Math.Acos(s); } + /// <summary> + /// Returns the arc sine of `s` in radians. Use to get the angle of sine s. + /// </summary> + /// <param name="s">The input sine value. Must be on the range of -1.0 to 1.0.</param> + /// <returns>An angle that would result in the given sine value. On the range `-Tau/4` to `Tau/4`.</returns> public static real_t Asin(real_t s) { return (real_t)Math.Asin(s); } + /// <summary> + /// Returns the arc tangent of `s` in radians. Use to get the angle of tangent s. + /// + /// The method cannot know in which quadrant the angle should fall. + /// See <see cref="Atan2(real_t, real_t)"/> if you have both `y` and `x`. + /// </summary> + /// <param name="s">The input tangent value.</param> + /// <returns>An angle that would result in the given tangent value. On the range `-Tau/4` to `Tau/4`.</returns> public static real_t Atan(real_t s) { return (real_t)Math.Atan(s); } + /// <summary> + /// Returns the arc tangent of `y` and `x` in radians. Use to get the angle + /// of the tangent of `y/x`. To compute the value, the method takes into + /// account the sign of both arguments in order to determine the quadrant. + /// + /// Important note: The Y coordinate comes first, by convention. + /// </summary> + /// <param name="y">The Y coordinate of the point to find the angle to.</param> + /// <param name="x">The X coordinate of the point to find the angle to.</param> + /// <returns>An angle that would result in the given tangent value. On the range `-Tau/2` to `Tau/2`.</returns> public static real_t Atan2(real_t y, real_t x) { return (real_t)Math.Atan2(y, x); } + /// <summary> + /// Converts a 2D point expressed in the cartesian coordinate + /// system (X and Y axis) to the polar coordinate system + /// (a distance from the origin and an angle). + /// </summary> + /// <param name="x">The input X coordinate.</param> + /// <param name="y">The input Y coordinate.</param> + /// <returns>A <see cref="Vector2"/> with X representing the distance and Y representing the angle.</returns> public static Vector2 Cartesian2Polar(real_t x, real_t y) { return new Vector2(Sqrt(x * x + y * y), Atan2(y, x)); } + /// <summary> + /// Rounds `s` upward (towards positive infinity). + /// </summary> + /// <param name="s">The number to ceil.</param> + /// <returns>The smallest whole number that is not less than `s`.</returns> public static real_t Ceil(real_t s) { return (real_t)Math.Ceiling(s); } + /// <summary> + /// Clamps a `value` so that it is not less than `min` and not more than `max`. + /// </summary> + /// <param name="value">The value to clamp.</param> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The clamped value.</returns> public static int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value; } + /// <summary> + /// Clamps a `value` so that it is not less than `min` and not more than `max`. + /// </summary> + /// <param name="value">The value to clamp.</param> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The clamped value.</returns> public static real_t Clamp(real_t value, real_t min, real_t max) { return value < min ? min : value > max ? max : value; } + /// <summary> + /// Returns the cosine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The cosine of that angle.</returns> public static real_t Cos(real_t s) { return (real_t)Math.Cos(s); } + /// <summary> + /// Returns the hyperbolic cosine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The hyperbolic cosine of that angle.</returns> public static real_t Cosh(real_t s) { return (real_t)Math.Cosh(s); } + /// <summary> + /// Converts an angle expressed in degrees to radians. + /// </summary> + /// <param name="deg">An angle expressed in degrees.</param> + /// <returns>The same angle expressed in radians.</returns> public static real_t Deg2Rad(real_t deg) { return deg * Deg2RadConst; } + /// <summary> + /// Easing function, based on exponent. The curve values are: + /// `0` is constant, `1` is linear, `0` to `1` is ease-in, `1` or more is ease-out. + /// Negative values are in-out/out-in. + /// </summary> + /// <param name="s">The value to ease.</param> + /// <param name="curve">`0` is constant, `1` is linear, `0` to `1` is ease-in, `1` or more is ease-out.</param> + /// <returns>The eased value.</returns> public static real_t Ease(real_t s, real_t curve) { if (s < 0f) @@ -118,21 +224,47 @@ namespace Godot return 0f; } + /// <summary> + /// The natural exponential function. It raises the mathematical + /// constant `e` to the power of `s` and returns it. + /// </summary> + /// <param name="s">The exponent to raise `e` to.</param> + /// <returns>`e` raised to the power of `s`.</returns> public static real_t Exp(real_t s) { return (real_t)Math.Exp(s); } + /// <summary> + /// Rounds `s` downward (towards negative infinity). + /// </summary> + /// <param name="s">The number to floor.</param> + /// <returns>The largest whole number that is not more than `s`.</returns> public static real_t Floor(real_t s) { return (real_t)Math.Floor(s); } + /// <summary> + /// Returns a normalized value considering the given range. + /// This is the opposite of <see cref="Lerp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="from">The interpolated value.</param> + /// <param name="to">The destination value for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting value of the inverse interpolation.</returns> public static real_t InverseLerp(real_t from, real_t to, real_t weight) { return (weight - from) / (to - from); } + /// <summary> + /// Returns true if `a` and `b` are approximately equal to each other. + /// The comparison is done using a tolerance calculation with <see cref="Epsilon"/>. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>A bool for whether or not the two values are approximately equal.</returns> public static bool IsEqualApprox(real_t a, real_t b) { // Check for exact equality first, required to handle "infinity" values. @@ -149,26 +281,62 @@ namespace Godot return Abs(a - b) < tolerance; } + /// <summary> + /// Returns whether `s` is an infinity value (either positive infinity or negative infinity). + /// </summary> + /// <param name="s">The value to check.</param> + /// <returns>A bool for whether or not the value is an infinity value.</returns> public static bool IsInf(real_t s) { return real_t.IsInfinity(s); } + /// <summary> + /// Returns whether `s` is a `NaN` ("Not a Number" or invalid) value. + /// </summary> + /// <param name="s">The value to check.</param> + /// <returns>A bool for whether or not the value is a `NaN` value.</returns> public static bool IsNaN(real_t s) { return real_t.IsNaN(s); } + /// <summary> + /// Returns true if `s` is approximately zero. + /// The comparison is done using a tolerance calculation with <see cref="Epsilon"/>. + /// + /// This method is faster than using <see cref="IsEqualApprox(real_t, real_t)"/> with one value as zero. + /// </summary> + /// <param name="s">The value to check.</param> + /// <returns>A bool for whether or not the value is nearly zero.</returns> public static bool IsZeroApprox(real_t s) { return Abs(s) < Epsilon; } + /// <summary> + /// Linearly interpolates between two values by a normalized value. + /// This is the opposite <see cref="InverseLerp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="from">The start value for interpolation.</param> + /// <param name="to">The destination value for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting value of the interpolation.</returns> public static real_t Lerp(real_t from, real_t to, real_t weight) { return from + (to - from) * weight; } + /// <summary> + /// Linearly interpolates between two angles (in radians) by a normalized value. + /// + /// Similar to <see cref="Lerp(real_t, real_t, real_t)"/>, + /// but interpolates correctly when the angles wrap around <see cref="Tau"/>. + /// </summary> + /// <param name="from">The start angle for interpolation.</param> + /// <param name="to">The destination angle for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting angle of the interpolation.</returns> public static real_t LerpAngle(real_t from, real_t to, real_t weight) { real_t difference = (to - from) % Mathf.Tau; @@ -176,36 +344,81 @@ namespace Godot return from + distance * weight; } + /// <summary> + /// Natural logarithm. The amount of time needed to reach a certain level of continuous growth. + /// + /// Note: This is not the same as the "log" function on most calculators, which uses a base 10 logarithm. + /// </summary> + /// <param name="s">The input value.</param> + /// <returns>The natural log of `s`.</returns> public static real_t Log(real_t s) { return (real_t)Math.Log(s); } + /// <summary> + /// Returns the maximum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is higher.</returns> public static int Max(int a, int b) { return a > b ? a : b; } + /// <summary> + /// Returns the maximum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is higher.</returns> public static real_t Max(real_t a, real_t b) { return a > b ? a : b; } + /// <summary> + /// Returns the minimum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is lower.</returns> public static int Min(int a, int b) { return a < b ? a : b; } + /// <summary> + /// Returns the minimum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is lower.</returns> public static real_t Min(real_t a, real_t b) { return a < b ? a : b; } + /// <summary> + /// Moves `from` toward `to` by the `delta` value. + /// + /// Use a negative delta value to move away. + /// </summary> + /// <param name="from">The start value.</param> + /// <param name="to">The value to move towards.</param> + /// <param name="delta">The amount to move by.</param> + /// <returns>The value after moving.</returns> public static real_t MoveToward(real_t from, real_t to, real_t delta) { return Abs(to - from) <= delta ? to : from + Sign(to - from) * delta; } + /// <summary> + /// Returns the nearest larger power of 2 for the integer `value`. + /// </summary> + /// <param name="value">The input value.</param> + /// <returns>The nearest larger power of 2.</returns> public static int NearestPo2(int value) { value--; @@ -218,14 +431,25 @@ namespace Godot return value; } + /// <summary> + /// Converts a 2D point expressed in the polar coordinate + /// system (a distance from the origin `r` and an angle `th`) + /// to the cartesian coordinate system (X and Y axis). + /// </summary> + /// <param name="r">The distance from the origin.</param> + /// <param name="th">The angle of the point.</param> + /// <returns>A <see cref="Vector2"/> representing the cartesian coordinate.</returns> public static Vector2 Polar2Cartesian(real_t r, real_t th) { return new Vector2(r * Cos(th), r * Sin(th)); } /// <summary> - /// Performs a canonical Modulus operation, where the output is on the range [0, b). + /// Performs a canonical Modulus operation, where the output is on the range `[0, b)`. /// </summary> + /// <param name="a">The dividend, the primary input.</param> + /// <param name="b">The divisor. The output is on the range `[0, b)`.</param> + /// <returns>The resulting output.</returns> public static int PosMod(int a, int b) { int c = a % b; @@ -237,8 +461,11 @@ namespace Godot } /// <summary> - /// Performs a canonical Modulus operation, where the output is on the range [0, b). + /// Performs a canonical Modulus operation, where the output is on the range `[0, b)`. /// </summary> + /// <param name="a">The dividend, the primary input.</param> + /// <param name="b">The divisor. The output is on the range `[0, b)`.</param> + /// <returns>The resulting output.</returns> public static real_t PosMod(real_t a, real_t b) { real_t c = a % b; @@ -249,43 +476,89 @@ namespace Godot return c; } + /// <summary> + /// Returns the result of `x` raised to the power of `y`. + /// </summary> + /// <param name="x">The base.</param> + /// <param name="y">The exponent.</param> + /// <returns>`x` raised to the power of `y`.</returns> public static real_t Pow(real_t x, real_t y) { return (real_t)Math.Pow(x, y); } + /// <summary> + /// Converts an angle expressed in radians to degrees. + /// </summary> + /// <param name="rad">An angle expressed in radians.</param> + /// <returns>The same angle expressed in degrees.</returns> public static real_t Rad2Deg(real_t rad) { return rad * Rad2DegConst; } + /// <summary> + /// Rounds `s` to the nearest whole number, + /// with halfway cases rounded towards the nearest multiple of two. + /// </summary> + /// <param name="s">The number to round.</param> + /// <returns>The rounded number.</returns> public static real_t Round(real_t s) { return (real_t)Math.Round(s); } + /// <summary> + /// Returns the sign of `s`: `-1` or `1`. Returns `0` if `s` is `0`. + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>One of three possible values: `1`, `-1`, or `0`.</returns> public static int Sign(int s) { if (s == 0) return 0; return s < 0 ? -1 : 1; } + /// <summary> + /// Returns the sign of `s`: `-1` or `1`. Returns `0` if `s` is `0`. + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>One of three possible values: `1`, `-1`, or `0`.</returns> public static int Sign(real_t s) { if (s == 0) return 0; return s < 0 ? -1 : 1; } + /// <summary> + /// Returns the sine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The sine of that angle.</returns> public static real_t Sin(real_t s) { return (real_t)Math.Sin(s); } + /// <summary> + /// Returns the hyperbolic sine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The hyperbolic sine of that angle.</returns> public static real_t Sinh(real_t s) { return (real_t)Math.Sinh(s); } + /// <summary> + /// Returns a number smoothly interpolated between `from` and `to`, + /// based on the `weight`. Similar to <see cref="Lerp(real_t, real_t, real_t)"/>, + /// but interpolates faster at the beginning and slower at the end. + /// </summary> + /// <param name="from">The start value for interpolation.</param> + /// <param name="to">The destination value for interpolation.</param> + /// <param name="weight">A value representing the amount of interpolation.</param> + /// <returns>The resulting value of the interpolation.</returns> public static real_t SmoothStep(real_t from, real_t to, real_t weight) { if (IsEqualApprox(from, to)) @@ -296,11 +569,25 @@ namespace Godot return x * x * (3 - 2 * x); } + /// <summary> + /// Returns the square root of `s`, where `s` is a non-negative number. + /// + /// If you need negative inputs, use `System.Numerics.Complex`. + /// </summary> + /// <param name="s">The input number. Must not be negative.</param> + /// <returns>The square root of `s`.</returns> public static real_t Sqrt(real_t s) { return (real_t)Math.Sqrt(s); } + /// <summary> + /// Returns the position of the first non-zero digit, after the + /// decimal point. Note that the maximum return value is 10, + /// which is a design decision in the implementation. + /// </summary> + /// <param name="step">The input value.</param> + /// <returns>The position of the first non-zero digit.</returns> public static int StepDecimals(real_t step) { double[] sd = new double[] { @@ -326,32 +613,68 @@ namespace Godot return 0; } + /// <summary> + /// Snaps float value `s` to a given `step`. + /// This can also be used to round a floating point + /// number to an arbitrary number of decimals. + /// </summary> + /// <param name="s">The value to stepify.</param> + /// <param name="step">The step size to snap to.</param> + /// <returns></returns> public static real_t Stepify(real_t s, real_t step) { if (step != 0f) { - s = Floor(s / step + 0.5f) * step; + return Floor(s / step + 0.5f) * step; } return s; } + /// <summary> + /// Returns the tangent of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The tangent of that angle.</returns> public static real_t Tan(real_t s) { return (real_t)Math.Tan(s); } + /// <summary> + /// Returns the hyperbolic tangent of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The hyperbolic tangent of that angle.</returns> public static real_t Tanh(real_t s) { return (real_t)Math.Tanh(s); } + /// <summary> + /// Wraps `value` between `min` and `max`. Usable for creating loop-alike + /// behavior or infinite surfaces. If `min` is `0`, this is equivalent + /// to <see cref="PosMod(int, int)"/>, so prefer using that instead. + /// </summary> + /// <param name="value">The value to wrap.</param> + /// <param name="min">The minimum allowed value and lower bound of the range.</param> + /// <param name="max">The maximum allowed value and upper bound of the range.</param> + /// <returns>The wrapped value.</returns> public static int Wrap(int value, int min, int max) { int range = max - min; return range == 0 ? min : min + ((value - min) % range + range) % range; } + /// <summary> + /// Wraps `value` between `min` and `max`. Usable for creating loop-alike + /// behavior or infinite surfaces. If `min` is `0`, this is equivalent + /// to <see cref="PosMod(real_t, real_t)"/>, so prefer using that instead. + /// </summary> + /// <param name="value">The value to wrap.</param> + /// <param name="min">The minimum allowed value and lower bound of the range.</param> + /// <param name="max">The maximum allowed value and upper bound of the range.</param> + /// <returns>The wrapped value.</returns> public static real_t Wrap(real_t value, real_t min, real_t max) { real_t range = max - min; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs index 1b7fd4906f..c2f4701b5f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs @@ -12,40 +12,89 @@ namespace Godot { // Define constants with Decimal precision and cast down to double or float. + /// <summary> + /// The natural number `e`. + /// </summary> public const real_t E = (real_t) 2.7182818284590452353602874714M; // 2.7182817f and 2.718281828459045 + + /// <summary> + /// The square root of 2. + /// </summary> public const real_t Sqrt2 = (real_t) 1.4142135623730950488016887242M; // 1.4142136f and 1.414213562373095 + /// <summary> + /// A very small number used for float comparison with error tolerance. + /// 1e-06 with single-precision floats, but 1e-14 if `REAL_T_IS_DOUBLE`. + /// </summary> #if REAL_T_IS_DOUBLE public const real_t Epsilon = 1e-14; // Epsilon size should depend on the precision used. #else public const real_t Epsilon = 1e-06f; #endif + /// <summary> + /// Returns the amount of digits after the decimal place. + /// </summary> + /// <param name="s">The input value.</param> + /// <returns>The amount of digits.</returns> public static int DecimalCount(real_t s) { return DecimalCount((decimal)s); } + /// <summary> + /// Returns the amount of digits after the decimal place. + /// </summary> + /// <param name="s">The input <see cref="System.Decimal"/> value.</param> + /// <returns>The amount of digits.</returns> public static int DecimalCount(decimal s) { return BitConverter.GetBytes(decimal.GetBits(s)[3])[2]; } + /// <summary> + /// Rounds `s` upward (towards positive infinity). + /// + /// This is the same as <see cref="Ceil(real_t)"/>, but returns an `int`. + /// </summary> + /// <param name="s">The number to ceil.</param> + /// <returns>The smallest whole number that is not less than `s`.</returns> public static int CeilToInt(real_t s) { return (int)Math.Ceiling(s); } + /// <summary> + /// Rounds `s` downward (towards negative infinity). + /// + /// This is the same as <see cref="Floor(real_t)"/>, but returns an `int`. + /// </summary> + /// <param name="s">The number to floor.</param> + /// <returns>The largest whole number that is not more than `s`.</returns> public static int FloorToInt(real_t s) { return (int)Math.Floor(s); } + /// <summary> + /// + /// </summary> + /// <param name="s"></param> + /// <returns></returns> public static int RoundToInt(real_t s) { return (int)Math.Round(s); } + /// <summary> + /// Returns true if `a` and `b` are approximately equal to each other. + /// The comparison is done using the provided tolerance value. + /// If you want the tolerance to be calculated for you, use <see cref="IsEqualApprox(real_t, real_t)"/>. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <param name="tolerance">The pre-calculated tolerance value.</param> + /// <returns>A bool for whether or not the two values are equal.</returns> public static bool IsEqualApprox(real_t a, real_t b, real_t tolerance) { // Check for exact equality first, required to handle "infinity" values. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs index 885845e3a4..2f8b5f297c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs @@ -8,18 +8,33 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// Plane represents a normalized plane equation. + /// "Over" or "Above" the plane is considered the side of + /// the plane towards where the normal is pointing. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Plane : IEquatable<Plane> { private Vector3 _normal; + /// <summary> + /// The normal of the plane, which must be normalized. + /// In the scalar equation of the plane `ax + by + cz = d`, this is + /// the vector `(a, b, c)`, where `d` is the <see cref="D"/> property. + /// </summary> + /// <value>Equivalent to `x`, `y`, and `z`.</value> public Vector3 Normal { get { return _normal; } set { _normal = value; } } + /// <summary> + /// The X component of the plane's normal vector. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/>'s X value.</value> public real_t x { get @@ -32,6 +47,10 @@ namespace Godot } } + /// <summary> + /// The Y component of the plane's normal vector. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/>'s Y value.</value> public real_t y { get @@ -44,6 +63,10 @@ namespace Godot } } + /// <summary> + /// The Z component of the plane's normal vector. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/>'s Z value.</value> public real_t z { get @@ -56,38 +79,71 @@ namespace Godot } } + /// <summary> + /// The distance from the origin to the plane (in the direction of + /// <see cref="Normal"/>). This value is typically non-negative. + /// In the scalar equation of the plane `ax + by + cz = d`, + /// this is `d`, while the `(a, b, c)` coordinates are represented + /// by the <see cref="Normal"/> property. + /// </summary> + /// <value>The plane's distance from the origin.</value> public real_t D { get; set; } + /// <summary> + /// The center of the plane, the point where the normal line intersects the plane. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/> multiplied by `D`.</value> public Vector3 Center { get { return _normal * D; } + set + { + _normal = value.Normalized(); + D = value.Length(); + } } + /// <summary> + /// Returns the shortest distance from this plane to the position `point`. + /// </summary> + /// <param name="point">The position to use for the calculation.</param> + /// <returns>The shortest distance.</returns> public real_t DistanceTo(Vector3 point) { return _normal.Dot(point) - D; } - public Vector3 GetAnyPoint() - { - return _normal * D; - } - + /// <summary> + /// Returns true if point is inside the plane. + /// Comparison uses a custom minimum epsilon threshold. + /// </summary> + /// <param name="point">The point to check.</param> + /// <param name="epsilon">The tolerance threshold.</param> + /// <returns>A bool for whether or not the plane has the point.</returns> public bool HasPoint(Vector3 point, real_t epsilon = Mathf.Epsilon) { real_t dist = _normal.Dot(point) - D; return Mathf.Abs(dist) <= epsilon; } + /// <summary> + /// Returns the intersection point of the three planes: `b`, `c`, + /// and this plane. If no intersection is found, `null` is returned. + /// </summary> + /// <param name="b">One of the three planes to use in the calculation.</param> + /// <param name="c">One of the three planes to use in the calculation.</param> + /// <returns>The intersection, or `null` if none is found.</returns> public Vector3? Intersect3(Plane b, Plane c) { real_t denom = _normal.Cross(b._normal).Dot(c._normal); if (Mathf.IsZeroApprox(denom)) + { return null; + } Vector3 result = b._normal.Cross(c._normal) * D + c._normal.Cross(_normal) * b.D + @@ -96,54 +152,94 @@ namespace Godot return result / denom; } + /// <summary> + /// Returns the intersection point of a ray consisting of the + /// position `from` and the direction normal `dir` with this plane. + /// If no intersection is found, `null` is returned. + /// </summary> + /// <param name="from">The start of the ray.</param> + /// <param name="dir">The direction of the ray, normalized.</param> + /// <returns>The intersection, or `null` if none is found.</returns> public Vector3? IntersectRay(Vector3 from, Vector3 dir) { real_t den = _normal.Dot(dir); if (Mathf.IsZeroApprox(den)) + { return null; + } real_t dist = (_normal.Dot(from) - D) / den; // This is a ray, before the emitting pos (from) does not exist if (dist > Mathf.Epsilon) + { return null; + } return from + dir * -dist; } + /// <summary> + /// Returns the intersection point of a line segment from + /// position `begin` to position `end` with this plane. + /// If no intersection is found, `null` is returned. + /// </summary> + /// <param name="begin">The start of the line segment.</param> + /// <param name="end">The end of the line segment.</param> + /// <returns>The intersection, or `null` if none is found.</returns> public Vector3? IntersectSegment(Vector3 begin, Vector3 end) { Vector3 segment = begin - end; real_t den = _normal.Dot(segment); if (Mathf.IsZeroApprox(den)) + { return null; + } real_t dist = (_normal.Dot(begin) - D) / den; // Only allow dist to be in the range of 0 to 1, with tolerance. if (dist < -Mathf.Epsilon || dist > 1.0f + Mathf.Epsilon) + { return null; + } return begin + segment * -dist; } + /// <summary> + /// Returns true if `point` is located above the plane. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the point is above the plane.</returns> public bool IsPointOver(Vector3 point) { return _normal.Dot(point) > D; } + /// <summary> + /// Returns the plane scaled to unit length. + /// </summary> + /// <returns>A normalized version of the plane.</returns> public Plane Normalized() { real_t len = _normal.Length(); if (len == 0) + { return new Plane(0, 0, 0, 0); + } return new Plane(_normal / len, D / len); } + /// <summary> + /// Returns the orthogonal projection of `point` into the plane. + /// </summary> + /// <param name="point">The point to project.</param> + /// <returns>The projected point.</returns> public Vector3 Project(Vector3 point) { return point - _normal * DistanceTo(point); @@ -154,22 +250,56 @@ namespace Godot private static readonly Plane _planeXZ = new Plane(0, 1, 0, 0); private static readonly Plane _planeXY = new Plane(0, 0, 1, 0); + /// <summary> + /// A plane that extends in the Y and Z axes (normal vector points +X). + /// </summary> + /// <value>Equivalent to `new Plane(1, 0, 0, 0)`.</value> public static Plane PlaneYZ { get { return _planeYZ; } } + + /// <summary> + /// A plane that extends in the X and Z axes (normal vector points +Y). + /// </summary> + /// <value>Equivalent to `new Plane(0, 1, 0, 0)`.</value> public static Plane PlaneXZ { get { return _planeXZ; } } + + /// <summary> + /// A plane that extends in the X and Y axes (normal vector points +Z). + /// </summary> + /// <value>Equivalent to `new Plane(0, 0, 1, 0)`.</value> public static Plane PlaneXY { get { return _planeXY; } } - // Constructors + /// <summary> + /// Constructs a plane from four values. `a`, `b` and `c` become the + /// components of the resulting plane's <see cref="Normal"/> vector. + /// `d` becomes the plane's distance from the origin. + /// </summary> + /// <param name="a">The X component of the plane's normal vector.</param> + /// <param name="b">The Y component of the plane's normal vector.</param> + /// <param name="c">The Z component of the plane's normal vector.</param> + /// <param name="d">The plane's distance from the origin. This value is typically non-negative.</param> public Plane(real_t a, real_t b, real_t c, real_t d) { _normal = new Vector3(a, b, c); this.D = d; } + + /// <summary> + /// Constructs a plane from a normal vector and the plane's distance to the origin. + /// </summary> + /// <param name="normal">The normal of the plane, must be normalized.</param> + /// <param name="d">The plane's distance from the origin. This value is typically non-negative.</param> public Plane(Vector3 normal, real_t d) { this._normal = normal; this.D = d; } + /// <summary> + /// Constructs a plane from the three points, given in clockwise order. + /// </summary> + /// <param name="v1">The first point.</param> + /// <param name="v2">The second point.</param> + /// <param name="v3">The third point.</param> public Plane(Vector3 v1, Vector3 v2, Vector3 v3) { _normal = (v1 - v3).Cross(v1 - v2); @@ -207,6 +337,12 @@ namespace Godot return _normal == other._normal && D == other.D; } + /// <summary> + /// Returns true if this plane and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other plane to compare.</param> + /// <returns>Whether or not the planes are approximately equal.</returns> public bool IsEqualApprox(Plane other) { return _normal.IsEqualApprox(other._normal) && Mathf.IsEqualApprox(D, other.D); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs index bbc617ea6e..b33490f9cb 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs @@ -8,15 +8,51 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// A unit quaternion used for representing 3D rotations. + /// Quaternions need to be normalized to be used for rotation. + /// + /// It is similar to Basis, which implements matrix representation of + /// rotations, and can be parametrized using both an axis-angle pair + /// or Euler angles. Basis stores rotation, scale, and shearing, + /// while Quat only stores rotation. + /// + /// Due to its compactness and the way it is stored in memory, certain + /// operations (obtaining axis-angle and performing SLERP, in particular) + /// are more efficient and robust against floating-point errors. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Quat : IEquatable<Quat> { + /// <summary> + /// X component of the quaternion (imaginary `i` axis part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t x; + + /// <summary> + /// Y component of the quaternion (imaginary `j` axis part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t y; + + /// <summary> + /// Z component of the quaternion (imaginary `k` axis part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t z; + + /// <summary> + /// W component of the quaternion (real part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t w; + /// <summary> + /// Access quaternion components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`, `[2]` is equivalent to `.z`, `[3]` is equivalent to `.w`.</value> public real_t this[int index] { get @@ -57,16 +93,35 @@ namespace Godot } } + /// <summary> + /// Returns the length (magnitude) of the quaternion. + /// </summary> + /// <value>Equivalent to `Mathf.Sqrt(LengthSquared)`.</value> public real_t Length { get { return Mathf.Sqrt(LengthSquared); } } + /// <summary> + /// Returns the squared length (squared magnitude) of the quaternion. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare quaternions or need the squared length for some formula. + /// </summary> + /// <value>Equivalent to `Dot(this)`.</value> public real_t LengthSquared { get { return Dot(this); } } + /// <summary> + /// Performs a cubic spherical interpolation between quaternions `preA`, + /// this vector, `b`, and `postB`, by the given amount `t`. + /// </summary> + /// <param name="b">The destination quaternion.</param> + /// <param name="preA">A quaternion before this quaternion.</param> + /// <param name="postB">A quaternion after `b`.</param> + /// <param name="t">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated quaternion.</returns> public Quat CubicSlerp(Quat b, Quat preA, Quat postB, real_t t) { real_t t2 = (1.0f - t) * t * 2f; @@ -75,85 +130,131 @@ namespace Godot return sp.Slerpni(sq, t2); } + /// <summary> + /// Returns the dot product of two quaternions. + /// </summary> + /// <param name="b">The other quaternion.</param> + /// <returns>The dot product.</returns> public real_t Dot(Quat b) { return x * b.x + y * b.y + z * b.z + w * b.w; } + /// <summary> + /// Returns Euler angles (in the YXZ convention: when decomposing, + /// first Z, then X, and Y last) corresponding to the rotation + /// represented by the unit quaternion. Returned vector contains + /// the rotation angles in the format (X angle, Y angle, Z angle). + /// </summary> + /// <returns>The Euler angle representation of this quaternion.</returns> public Vector3 GetEuler() { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); + } #endif var basis = new Basis(this); return basis.GetEuler(); } + /// <summary> + /// Returns the inverse of the quaternion. + /// </summary> + /// <returns>The inverse quaternion.</returns> public Quat Inverse() { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); + } #endif return new Quat(-x, -y, -z, w); } + /// <summary> + /// Returns whether the quaternion is normalized or not. + /// </summary> + /// <returns>A bool for whether the quaternion is normalized or not.</returns> + public bool IsNormalized() + { + return Mathf.Abs(LengthSquared - 1) <= Mathf.Epsilon; + } + + /// <summary> + /// Returns a copy of the quaternion, normalized to unit length. + /// </summary> + /// <returns>The normalized quaternion.</returns> public Quat Normalized() { return this / Length; } - public Quat Slerp(Quat b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this quaternion and `to` by amount `weight`. + /// + /// Note: Both quaternions must be normalized. + /// </summary> + /// <param name="to">The destination quaternion for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting quaternion of the interpolation.</returns> + public Quat Slerp(Quat to, real_t weight) { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); - if (!b.IsNormalized()) - throw new ArgumentException("Argument is not normalized", nameof(b)); + } + if (!to.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(to)); + } #endif - // Calculate cosine - real_t cosom = x * b.x + y * b.y + z * b.z + w * b.w; + // Calculate cosine. + real_t cosom = x * to.x + y * to.y + z * to.z + w * to.w; var to1 = new Quat(); - // Adjust signs if necessary + // Adjust signs if necessary. if (cosom < 0.0) { cosom = -cosom; - to1.x = -b.x; - to1.y = -b.y; - to1.z = -b.z; - to1.w = -b.w; + to1.x = -to.x; + to1.y = -to.y; + to1.z = -to.z; + to1.w = -to.w; } else { - to1.x = b.x; - to1.y = b.y; - to1.z = b.z; - to1.w = b.w; + to1.x = to.x; + to1.y = to.y; + to1.z = to.z; + to1.w = to.w; } real_t sinom, scale0, scale1; - // Calculate coefficients + // Calculate coefficients. if (1.0 - cosom > Mathf.Epsilon) { - // Standard case (Slerp) + // Standard case (Slerp). real_t omega = Mathf.Acos(cosom); sinom = Mathf.Sin(omega); - scale0 = Mathf.Sin((1.0f - t) * omega) / sinom; - scale1 = Mathf.Sin(t * omega) / sinom; + scale0 = Mathf.Sin((1.0f - weight) * omega) / sinom; + scale1 = Mathf.Sin(weight * omega) / sinom; } else { - // Quaternions are very close so we can do a linear interpolation - scale0 = 1.0f - t; - scale1 = t; + // Quaternions are very close so we can do a linear interpolation. + scale0 = 1.0f - weight; + scale1 = weight; } - // Calculate final values + // Calculate final values. return new Quat ( scale0 * x + scale1 * to1.x, @@ -163,9 +264,17 @@ namespace Godot ); } - public Quat Slerpni(Quat b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this quaternion and `to` by amount `weight`, but without + /// checking if the rotation path is not bigger than 90 degrees. + /// </summary> + /// <param name="to">The destination quaternion for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting quaternion of the interpolation.</returns> + public Quat Slerpni(Quat to, real_t weight) { - real_t dot = Dot(b); + real_t dot = Dot(to); if (Mathf.Abs(dot) > 0.9999f) { @@ -174,33 +283,54 @@ namespace Godot real_t theta = Mathf.Acos(dot); real_t sinT = 1.0f / Mathf.Sin(theta); - real_t newFactor = Mathf.Sin(t * theta) * sinT; - real_t invFactor = Mathf.Sin((1.0f - t) * theta) * sinT; + real_t newFactor = Mathf.Sin(weight * theta) * sinT; + real_t invFactor = Mathf.Sin((1.0f - weight) * theta) * sinT; return new Quat ( - invFactor * x + newFactor * b.x, - invFactor * y + newFactor * b.y, - invFactor * z + newFactor * b.z, - invFactor * w + newFactor * b.w + invFactor * x + newFactor * to.x, + invFactor * y + newFactor * to.y, + invFactor * z + newFactor * to.z, + invFactor * w + newFactor * to.w ); } + /// <summary> + /// Returns a vector transformed (multiplied) by this quaternion. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector3 Xform(Vector3 v) { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); + } #endif var u = new Vector3(x, y, z); Vector3 uv = u.Cross(v); return v + ((uv * w) + u.Cross(uv)) * 2; } - // Static Readonly Properties - public static Quat Identity { get; } = new Quat(0f, 0f, 0f, 1f); - - // Constructors + // Constants + private static readonly Quat _identity = new Quat(0, 0, 0, 1); + + /// <summary> + /// The identity quaternion, representing no rotation. + /// Equivalent to an identity <see cref="Basis"/> matrix. If a vector is transformed by + /// an identity quaternion, it will not change. + /// </summary> + /// <value>Equivalent to `new Quat(0, 0, 0, 1)`.</value> + public static Quat Identity { get { return _identity; } } + + /// <summary> + /// Constructs a quaternion defined by the given values. + /// </summary> + /// <param name="x">X component of the quaternion (imaginary `i` axis part).</param> + /// <param name="y">Y component of the quaternion (imaginary `j` axis part).</param> + /// <param name="z">Z component of the quaternion (imaginary `k` axis part).</param> + /// <param name="w">W component of the quaternion (real part).</param> public Quat(real_t x, real_t y, real_t z, real_t w) { this.x = x; @@ -209,21 +339,31 @@ namespace Godot this.w = w; } - public bool IsNormalized() - { - return Mathf.Abs(LengthSquared - 1) <= Mathf.Epsilon; - } - + /// <summary> + /// Constructs a quaternion from the given quaternion. + /// </summary> + /// <param name="q">The existing quaternion.</param> public Quat(Quat q) { this = q; } + /// <summary> + /// Constructs a quaternion from the given <see cref="Basis"/>. + /// </summary> + /// <param name="basis">The basis to construct from.</param> public Quat(Basis basis) { this = basis.Quat(); } + /// <summary> + /// Constructs a quaternion that will perform a rotation specified by + /// Euler angles (in the YXZ convention: when decomposing, + /// first Z, then X, and Y last), + /// given in the vector format as (X angle, Y angle, Z angle). + /// </summary> + /// <param name="eulerYXZ"></param> public Quat(Vector3 eulerYXZ) { real_t half_a1 = eulerYXZ.y * 0.5f; @@ -247,11 +387,19 @@ namespace Godot w = sin_a1 * sin_a2 * sin_a3 + cos_a1 * cos_a2 * cos_a3; } + /// <summary> + /// Constructs a quaternion that will rotate around the given axis + /// by the specified angle. The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="angle">The angle to rotate, in radians.</param> public Quat(Vector3 axis, real_t angle) { #if DEBUG if (!axis.IsNormalized()) + { throw new ArgumentException("Argument is not normalized", nameof(axis)); + } #endif real_t d = axis.Length(); @@ -364,6 +512,12 @@ namespace Godot return x == other.x && y == other.y && z == other.z && w == other.w; } + /// <summary> + /// Returns true if this quaternion and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other quaternion to compare.</param> + /// <returns>Whether or not the quaternions are approximately equal.</returns> public bool IsEqualApprox(Quat other) { return Mathf.IsEqualApprox(x, other.x) && Mathf.IsEqualApprox(y, other.y) && Mathf.IsEqualApprox(z, other.z) && Mathf.IsEqualApprox(w, other.w); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs index 91e614dc7b..f7703c77cc 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs @@ -8,6 +8,10 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 2D axis-aligned bounding box. Rect2 consists of a position, a size, and + /// several utility functions. It is typically used for fast overlap tests. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Rect2 : IEquatable<Rect2> @@ -15,29 +19,52 @@ namespace Godot private Vector2 _position; private Vector2 _size; + /// <summary> + /// Beginning corner. Typically has values lower than End. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2 Position { get { return _position; } set { _position = value; } } + /// <summary> + /// Size from Position to End. Typically all components are positive. + /// If the size is negative, you can use <see cref="Abs"/> to fix it. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2 Size { get { return _size; } set { _size = value; } } + /// <summary> + /// Ending corner. This is calculated as <see cref="Position"/> plus + /// <see cref="Size"/>. Setting this value will change the size. + /// </summary> + /// <value>Getting is equivalent to `value = Position + Size`, setting is equivalent to `Size = value - Position`.</value> public Vector2 End { get { return _position + _size; } set { _size = value - _position; } } + /// <summary> + /// The area of this rect. + /// </summary> + /// <value>Equivalent to <see cref="GetArea()"/>.</value> public real_t Area { get { return GetArea(); } } + /// <summary> + /// Returns a Rect2 with equivalent position and size, modified so that + /// the top-left corner is the origin and width and height are positive. + /// </summary> + /// <returns>The modified rect.</returns> public Rect2 Abs() { Vector2 end = End; @@ -45,12 +72,19 @@ namespace Godot return new Rect2(topLeft, _size.Abs()); } + /// <summary> + /// Returns the intersection of this Rect2 and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The clipped rect.</returns> public Rect2 Clip(Rect2 b) { var newRect = b; if (!Intersects(newRect)) + { return new Rect2(); + } newRect._position.x = Mathf.Max(b._position.x, _position.x); newRect._position.y = Mathf.Max(b._position.y, _position.y); @@ -64,6 +98,11 @@ namespace Godot return newRect; } + /// <summary> + /// Returns true if this Rect2 completely encloses another one. + /// </summary> + /// <param name="b">The other rect that may be enclosed.</param> + /// <returns>A bool for whether or not this rect encloses `b`.</returns> public bool Encloses(Rect2 b) { return b._position.x >= _position.x && b._position.y >= _position.y && @@ -71,6 +110,11 @@ namespace Godot b._position.y + b._size.y < _position.y + _size.y; } + /// <summary> + /// Returns this Rect2 expanded to include a given point. + /// </summary> + /// <param name="to">The point to include.</param> + /// <returns>The expanded rect.</returns> public Rect2 Expand(Vector2 to) { var expanded = this; @@ -79,14 +123,22 @@ namespace Godot Vector2 end = expanded._position + expanded._size; if (to.x < begin.x) + { begin.x = to.x; + } if (to.y < begin.y) + { begin.y = to.y; + } if (to.x > end.x) + { end.x = to.x; + } if (to.y > end.y) + { end.y = to.y; + } expanded._position = begin; expanded._size = end - begin; @@ -94,11 +146,20 @@ namespace Godot return expanded; } + /// <summary> + /// Returns the area of the Rect2. + /// </summary> + /// <returns>The area.</returns> public real_t GetArea() { return _size.x * _size.y; } + /// <summary> + /// Returns a copy of the Rect2 grown a given amount of units towards all the sides. + /// </summary> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2 Grow(real_t by) { var g = this; @@ -111,6 +172,14 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2 grown a given amount of units towards each direction individually. + /// </summary> + /// <param name="left">The amount to grow by on the left.</param> + /// <param name="top">The amount to grow by on the top.</param> + /// <param name="right">The amount to grow by on the right.</param> + /// <param name="bottom">The amount to grow by on the bottom.</param> + /// <returns>The grown rect.</returns> public Rect2 GrowIndividual(real_t left, real_t top, real_t right, real_t bottom) { var g = this; @@ -123,11 +192,17 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2 grown a given amount of units towards the <see cref="Margin"/> direction. + /// </summary> + /// <param name="margin">The direction to grow in.</param> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2 GrowMargin(Margin margin, real_t by) { var g = this; - g.GrowIndividual(Margin.Left == margin ? by : 0, + g = g.GrowIndividual(Margin.Left == margin ? by : 0, Margin.Top == margin ? by : 0, Margin.Right == margin ? by : 0, Margin.Bottom == margin ? by : 0); @@ -135,11 +210,20 @@ namespace Godot return g; } + /// <summary> + /// Returns true if the Rect2 is flat or empty, or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the rect has area.</returns> public bool HasNoArea() { return _size.x <= 0 || _size.y <= 0; } + /// <summary> + /// Returns true if the Rect2 contains a point, or false otherwise. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the rect contains `point`.</returns> public bool HasPoint(Vector2 point) { if (point.x < _position.x) @@ -155,20 +239,65 @@ namespace Godot return true; } - public bool Intersects(Rect2 b) + /// <summary> + /// Returns true if the Rect2 overlaps with `b` + /// (i.e. they have at least one point in common). + /// + /// If `includeBorders` is true, they will also be considered overlapping + /// if their borders touch, even without intersection. + /// </summary> + /// <param name="b">The other rect to check for intersections with.</param> + /// <param name="includeBorders">Whether or not to consider borders.</param> + /// <returns>A bool for whether or not they are intersecting.</returns> + public bool Intersects(Rect2 b, bool includeBorders = false) { - if (_position.x >= b._position.x + b._size.x) - return false; - if (_position.x + _size.x <= b._position.x) - return false; - if (_position.y >= b._position.y + b._size.y) - return false; - if (_position.y + _size.y <= b._position.y) - return false; + if (includeBorders) + { + if (_position.x > b._position.x + b._size.x) + { + return false; + } + if (_position.x + _size.x < b._position.x) + { + return false; + } + if (_position.y > b._position.y + b._size.y) + { + return false; + } + if (_position.y + _size.y < b._position.y) + { + return false; + } + } + else + { + if (_position.x >= b._position.x + b._size.x) + { + return false; + } + if (_position.x + _size.x <= b._position.x) + { + return false; + } + if (_position.y >= b._position.y + b._size.y) + { + return false; + } + if (_position.y + _size.y <= b._position.y) + { + return false; + } + } return true; } + /// <summary> + /// Returns a larger Rect2 that contains this Rect2 and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The merged rect.</returns> public Rect2 Merge(Rect2 b) { Rect2 newRect; @@ -179,27 +308,53 @@ namespace Godot newRect._size.x = Mathf.Max(b._position.x + b._size.x, _position.x + _size.x); newRect._size.y = Mathf.Max(b._position.y + b._size.y, _position.y + _size.y); - newRect._size = newRect._size - newRect._position; // Make relative again + newRect._size -= newRect._position; // Make relative again return newRect; } - // Constructors + /// <summary> + /// Constructs a Rect2 from a position and size. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="size">The size.</param> public Rect2(Vector2 position, Vector2 size) { _position = position; _size = size; } + + /// <summary> + /// Constructs a Rect2 from a position, width, and height. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2(Vector2 position, real_t width, real_t height) { _position = position; _size = new Vector2(width, height); } + + /// <summary> + /// Constructs a Rect2 from x, y, and size. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="size">The size.</param> public Rect2(real_t x, real_t y, Vector2 size) { _position = new Vector2(x, y); _size = size; } + + /// <summary> + /// Constructs a Rect2 from x, y, width, and height. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2(real_t x, real_t y, real_t width, real_t height) { _position = new Vector2(x, y); @@ -231,6 +386,12 @@ namespace Godot return _position.Equals(other._position) && _size.Equals(other._size); } + /// <summary> + /// Returns true if this rect and `other` are approximately equal, by running + /// <see cref="Vector2.IsEqualApprox(Vector2)"/> on each component. + /// </summary> + /// <param name="other">The other rect to compare.</param> + /// <returns>Whether or not the rects are approximately equal.</returns> public bool IsEqualApprox(Rect2 other) { return _position.IsEqualApprox(other._position) && _size.IsEqualApprox(other.Size); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs index bc2cad8713..8f71c00d76 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs @@ -3,6 +3,10 @@ using System.Runtime.InteropServices; namespace Godot { + /// <summary> + /// 2D axis-aligned bounding box using integers. Rect2i consists of a position, a size, and + /// several utility functions. It is typically used for fast overlap tests. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Rect2i : IEquatable<Rect2i> @@ -10,29 +14,52 @@ namespace Godot private Vector2i _position; private Vector2i _size; + /// <summary> + /// Beginning corner. Typically has values lower than End. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2i Position { get { return _position; } set { _position = value; } } + /// <summary> + /// Size from Position to End. Typically all components are positive. + /// If the size is negative, you can use <see cref="Abs"/> to fix it. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2i Size { get { return _size; } set { _size = value; } } + /// <summary> + /// Ending corner. This is calculated as <see cref="Position"/> plus + /// <see cref="Size"/>. Setting this value will change the size. + /// </summary> + /// <value>Getting is equivalent to `value = Position + Size`, setting is equivalent to `Size = value - Position`.</value> public Vector2i End { get { return _position + _size; } set { _size = value - _position; } } + /// <summary> + /// The area of this rect. + /// </summary> + /// <value>Equivalent to <see cref="GetArea()"/>.</value> public int Area { get { return GetArea(); } } + /// <summary> + /// Returns a Rect2i with equivalent position and size, modified so that + /// the top-left corner is the origin and width and height are positive. + /// </summary> + /// <returns>The modified rect.</returns> public Rect2i Abs() { Vector2i end = End; @@ -40,12 +67,19 @@ namespace Godot return new Rect2i(topLeft, _size.Abs()); } + /// <summary> + /// Returns the intersection of this Rect2i and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The clipped rect.</returns> public Rect2i Clip(Rect2i b) { var newRect = b; if (!Intersects(newRect)) + { return new Rect2i(); + } newRect._position.x = Mathf.Max(b._position.x, _position.x); newRect._position.y = Mathf.Max(b._position.y, _position.y); @@ -59,6 +93,11 @@ namespace Godot return newRect; } + /// <summary> + /// Returns true if this Rect2i completely encloses another one. + /// </summary> + /// <param name="b">The other rect that may be enclosed.</param> + /// <returns>A bool for whether or not this rect encloses `b`.</returns> public bool Encloses(Rect2i b) { return b._position.x >= _position.x && b._position.y >= _position.y && @@ -66,6 +105,11 @@ namespace Godot b._position.y + b._size.y < _position.y + _size.y; } + /// <summary> + /// Returns this Rect2i expanded to include a given point. + /// </summary> + /// <param name="to">The point to include.</param> + /// <returns>The expanded rect.</returns> public Rect2i Expand(Vector2i to) { var expanded = this; @@ -74,14 +118,22 @@ namespace Godot Vector2i end = expanded._position + expanded._size; if (to.x < begin.x) + { begin.x = to.x; + } if (to.y < begin.y) + { begin.y = to.y; + } if (to.x > end.x) + { end.x = to.x; + } if (to.y > end.y) + { end.y = to.y; + } expanded._position = begin; expanded._size = end - begin; @@ -89,11 +141,20 @@ namespace Godot return expanded; } + /// <summary> + /// Returns the area of the Rect2. + /// </summary> + /// <returns>The area.</returns> public int GetArea() { return _size.x * _size.y; } + /// <summary> + /// Returns a copy of the Rect2i grown a given amount of units towards all the sides. + /// </summary> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2i Grow(int by) { var g = this; @@ -106,6 +167,14 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2i grown a given amount of units towards each direction individually. + /// </summary> + /// <param name="left">The amount to grow by on the left.</param> + /// <param name="top">The amount to grow by on the top.</param> + /// <param name="right">The amount to grow by on the right.</param> + /// <param name="bottom">The amount to grow by on the bottom.</param> + /// <returns>The grown rect.</returns> public Rect2i GrowIndividual(int left, int top, int right, int bottom) { var g = this; @@ -118,11 +187,17 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2i grown a given amount of units towards the <see cref="Margin"/> direction. + /// </summary> + /// <param name="margin">The direction to grow in.</param> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2i GrowMargin(Margin margin, int by) { var g = this; - g.GrowIndividual(Margin.Left == margin ? by : 0, + g = g.GrowIndividual(Margin.Left == margin ? by : 0, Margin.Top == margin ? by : 0, Margin.Right == margin ? by : 0, Margin.Bottom == margin ? by : 0); @@ -130,11 +205,20 @@ namespace Godot return g; } + /// <summary> + /// Returns true if the Rect2 is flat or empty, or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the rect has area.</returns> public bool HasNoArea() { return _size.x <= 0 || _size.y <= 0; } + /// <summary> + /// Returns true if the Rect2 contains a point, or false otherwise. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the rect contains `point`.</returns> public bool HasPoint(Vector2i point) { if (point.x < _position.x) @@ -150,20 +234,49 @@ namespace Godot return true; } - public bool Intersects(Rect2i b) + /// <summary> + /// Returns true if the Rect2i overlaps with `b` + /// (i.e. they have at least one point in common). + /// + /// If `includeBorders` is true, they will also be considered overlapping + /// if their borders touch, even without intersection. + /// </summary> + /// <param name="b">The other rect to check for intersections with.</param> + /// <param name="includeBorders">Whether or not to consider borders.</param> + /// <returns>A bool for whether or not they are intersecting.</returns> + public bool Intersects(Rect2i b, bool includeBorders = false) { - if (_position.x >= b._position.x + b._size.x) - return false; - if (_position.x + _size.x <= b._position.x) - return false; - if (_position.y >= b._position.y + b._size.y) - return false; - if (_position.y + _size.y <= b._position.y) - return false; + if (includeBorders) + { + if (_position.x > b._position.x + b._size.x) + return false; + if (_position.x + _size.x < b._position.x) + return false; + if (_position.y > b._position.y + b._size.y) + return false; + if (_position.y + _size.y < b._position.y) + return false; + } + else + { + if (_position.x >= b._position.x + b._size.x) + return false; + if (_position.x + _size.x <= b._position.x) + return false; + if (_position.y >= b._position.y + b._size.y) + return false; + if (_position.y + _size.y <= b._position.y) + return false; + } return true; } + /// <summary> + /// Returns a larger Rect2i that contains this Rect2 and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The merged rect.</returns> public Rect2i Merge(Rect2i b) { Rect2i newRect; @@ -174,27 +287,53 @@ namespace Godot newRect._size.x = Mathf.Max(b._position.x + b._size.x, _position.x + _size.x); newRect._size.y = Mathf.Max(b._position.y + b._size.y, _position.y + _size.y); - newRect._size = newRect._size - newRect._position; // Make relative again + newRect._size -= newRect._position; // Make relative again return newRect; } - // Constructors + /// <summary> + /// Constructs a Rect2i from a position and size. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="size">The size.</param> public Rect2i(Vector2i position, Vector2i size) { _position = position; _size = size; } + + /// <summary> + /// Constructs a Rect2i from a position, width, and height. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2i(Vector2i position, int width, int height) { _position = position; _size = new Vector2i(width, height); } + + /// <summary> + /// Constructs a Rect2i from x, y, and size. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="size">The size.</param> public Rect2i(int x, int y, Vector2i size) { _position = new Vector2i(x, y); _size = size; } + + /// <summary> + /// Constructs a Rect2i from x, y, width, and height. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2i(int x, int y, int width, int height) { _position = new Vector2i(x, y); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index 099eacd7dd..41b4e9367f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -12,7 +12,7 @@ namespace Godot { private static int GetSliceCount(this string instance, string splitter) { - if (instance.Empty() || splitter.Empty()) + if (string.IsNullOrEmpty(instance) || string.IsNullOrEmpty(splitter)) return 0; int pos = 0; @@ -29,7 +29,7 @@ namespace Godot private static string GetSliceCharacter(this string instance, char splitter, int slice) { - if (!instance.Empty() && slice >= 0) + if (!string.IsNullOrEmpty(instance) && slice >= 0) { int i = 0; int prev = 0; @@ -237,10 +237,10 @@ namespace Godot // </summary> public static int CompareTo(this string instance, string to, bool caseSensitive = true) { - if (instance.Empty()) - return to.Empty() ? 0 : -1; + if (string.IsNullOrEmpty(instance)) + return string.IsNullOrEmpty(to) ? 0 : -1; - if (to.Empty()) + if (string.IsNullOrEmpty(to)) return 1; int instanceIndex = 0; @@ -287,14 +287,6 @@ namespace Godot } // <summary> - // Return true if the string is empty. - // </summary> - public static bool Empty(this string instance) - { - return string.IsNullOrEmpty(instance); - } - - // <summary> // Return true if the strings ends with the given string. // </summary> public static bool EndsWith(this string instance, string text) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs index aa8815d1aa..ac47f6029f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs @@ -8,11 +8,28 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 3×4 matrix (3 rows, 4 columns) used for 3D linear transformations. + /// It can represent transformations such as translation, rotation, or scaling. + /// It consists of a <see cref="Basis"/> (first 3 columns) and a + /// <see cref="Vector3"/> for the origin (last column). + /// + /// For more information, read this documentation article: + /// https://docs.godotengine.org/en/latest/tutorials/math/matrices_and_transforms.html + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Transform : IEquatable<Transform> { + /// <summary> + /// The <see cref="Basis"/> of this transform. Contains the X, Y, and Z basis + /// vectors (columns 0 to 2) and is responsible for rotation and scale. + /// </summary> public Basis basis; + + /// <summary> + /// The origin vector (column 3, the fourth column). Equivalent to array index `[3]`. + /// </summary> public Vector3 origin; /// <summary> @@ -85,13 +102,24 @@ namespace Godot } } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation, scaling, and translation. + /// </summary> + /// <returns>The inverse transformation matrix.</returns> public Transform AffineInverse() { Basis basisInv = basis.Inverse(); return new Transform(basisInv, basisInv.Xform(-origin)); } - public Transform InterpolateWith(Transform transform, real_t c) + /// <summary> + /// Interpolates this transform to the other `transform` by `weight`. + /// </summary> + /// <param name="transform">The other transform.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated transform.</returns> + public Transform InterpolateWith(Transform transform, real_t weight) { /* not sure if very "efficient" but good enough? */ @@ -104,18 +132,37 @@ namespace Godot Vector3 destinationLocation = transform.origin; var interpolated = new Transform(); - interpolated.basis.SetQuatScale(sourceRotation.Slerp(destinationRotation, c).Normalized(), sourceScale.LinearInterpolate(destinationScale, c)); - interpolated.origin = sourceLocation.LinearInterpolate(destinationLocation, c); + interpolated.basis.SetQuatScale(sourceRotation.Slerp(destinationRotation, weight).Normalized(), sourceScale.Lerp(destinationScale, weight)); + interpolated.origin = sourceLocation.Lerp(destinationLocation, weight); return interpolated; } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation and translation + /// (no scaling, use <see cref="AffineInverse"/> for transforms with scaling). + /// </summary> + /// <returns>The inverse matrix.</returns> public Transform Inverse() { Basis basisTr = basis.Transposed(); return new Transform(basisTr, basisTr.Xform(-origin)); } + /// <summary> + /// Returns a copy of the transform rotated such that its + /// -Z axis (forward) points towards the target position. + /// + /// The transform will first be rotated around the given up vector, + /// and then fully aligned to the target by a further rotation around + /// an axis perpendicular to both the target and up vectors. + /// + /// Operations take place in global space. + /// </summary> + /// <param name="target">The object to look at.</param> + /// <param name="up">The relative up direction</param> + /// <returns>The resulting transform.</returns> public Transform LookingAt(Vector3 target, Vector3 up) { var t = this; @@ -123,22 +170,39 @@ namespace Godot return t; } + /// <summary> + /// Returns the transform with the basis orthogonal (90 degrees), + /// and normalized axis vectors (scale of 1 or -1). + /// </summary> + /// <returns>The orthonormalized transform.</returns> public Transform Orthonormalized() { return new Transform(basis.Orthonormalized(), origin); } + /// <summary> + /// Rotates the transform around the given `axis` by `phi` (in radians), + /// using matrix multiplication. The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate, in radians.</param> + /// <returns>The rotated transformation matrix.</returns> public Transform Rotated(Vector3 axis, real_t phi) { return new Transform(new Basis(axis, phi), new Vector3()) * this; } + /// <summary> + /// Scales the transform by the given 3D scaling factor, using matrix multiplication. + /// </summary> + /// <param name="scale">The scale to introduce.</param> + /// <returns>The scaled transformation matrix.</returns> public Transform Scaled(Vector3 scale) { return new Transform(basis.Scaled(scale), origin * scale); } - public void SetLookAt(Vector3 eye, Vector3 target, Vector3 up) + private void SetLookAt(Vector3 eye, Vector3 target, Vector3 up) { // Make rotation matrix // Z vector @@ -161,16 +225,30 @@ namespace Godot origin = eye; } - public Transform Translated(Vector3 ofs) + /// <summary> + /// Translates the transform by the given `offset`, + /// relative to the transform's basis vectors. + /// + /// Unlike <see cref="Rotated"/> and <see cref="Scaled"/>, + /// this does not use matrix multiplication. + /// </summary> + /// <param name="offset">The offset to translate by.</param> + /// <returns>The translated matrix.</returns> + public Transform Translated(Vector3 offset) { return new Transform(basis, new Vector3 ( - origin[0] += basis.Row0.Dot(ofs), - origin[1] += basis.Row1.Dot(ofs), - origin[2] += basis.Row2.Dot(ofs) + origin[0] += basis.Row0.Dot(offset), + origin[1] += basis.Row1.Dot(offset), + origin[2] += basis.Row2.Dot(offset) )); } + /// <summary> + /// Returns a vector transformed (multiplied) by this transformation matrix. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector3 Xform(Vector3 v) { return new Vector3 @@ -181,6 +259,14 @@ namespace Godot ); } + /// <summary> + /// Returns a vector transformed (multiplied) by the transposed transformation matrix. + /// + /// Note: This results in a multiplication by the inverse of the + /// transformation matrix only if it represents a rotation-reflection. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector3 XformInv(Vector3 v) { Vector3 vInv = v - origin; @@ -199,24 +285,58 @@ namespace Godot private static readonly Transform _flipY = new Transform(new Basis(1, 0, 0, 0, -1, 0, 0, 0, 1), Vector3.Zero); private static readonly Transform _flipZ = new Transform(new Basis(1, 0, 0, 0, 1, 0, 0, 0, -1), Vector3.Zero); + /// <summary> + /// The identity transform, with no translation, rotation, or scaling applied. + /// This is used as a replacement for `Transform()` in GDScript. + /// Do not use `new Transform()` with no arguments in C#, because it sets all values to zero. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Right, Vector3.Up, Vector3.Back, Vector3.Zero)`.</value> public static Transform Identity { get { return _identity; } } + /// <summary> + /// The transform that will flip something along the X axis. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Left, Vector3.Up, Vector3.Back, Vector3.Zero)`.</value> public static Transform FlipX { get { return _flipX; } } + /// <summary> + /// The transform that will flip something along the Y axis. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Right, Vector3.Down, Vector3.Back, Vector3.Zero)`.</value> public static Transform FlipY { get { return _flipY; } } + /// <summary> + /// The transform that will flip something along the Z axis. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Right, Vector3.Up, Vector3.Forward, Vector3.Zero)`.</value> public static Transform FlipZ { get { return _flipZ; } } - // Constructors + /// <summary> + /// Constructs a transformation matrix from 4 vectors (matrix columns). + /// </summary> + /// <param name="column0">The X vector, or column index 0.</param> + /// <param name="column1">The Y vector, or column index 1.</param> + /// <param name="column2">The Z vector, or column index 2.</param> + /// <param name="origin">The origin vector, or column index 3.</param> public Transform(Vector3 column0, Vector3 column1, Vector3 column2, Vector3 origin) { basis = new Basis(column0, column1, column2); this.origin = origin; } + /// <summary> + /// Constructs a transformation matrix from the given quaternion and origin vector. + /// </summary> + /// <param name="quat">The <see cref="Godot.Quat"/> to create the basis from.</param> + /// <param name="origin">The origin vector, or column index 3.</param> public Transform(Quat quat, Vector3 origin) { basis = new Basis(quat); this.origin = origin; } + /// <summary> + /// Constructs a transformation matrix from the given basis and origin vector. + /// </summary> + /// <param name="basis">The <see cref="Godot.Basis"/> to create the basis from.</param> + /// <param name="origin">The origin vector, or column index 3.</param> public Transform(Basis basis, Vector3 origin) { this.basis = basis; @@ -255,6 +375,12 @@ namespace Godot return basis.Equals(other.basis) && origin.Equals(other.origin); } + /// <summary> + /// Returns true if this transform and `other` are approximately equal, by running + /// <see cref="Vector3.IsEqualApprox(Vector3)"/> on each component. + /// </summary> + /// <param name="other">The other transform to compare.</param> + /// <returns>Whether or not the matrices are approximately equal.</returns> public bool IsEqualApprox(Transform other) { return basis.IsEqualApprox(other.basis) && origin.IsEqualApprox(other.origin); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs index e72a44809a..06bbe98497 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs @@ -8,25 +8,44 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 2×3 matrix (2 rows, 3 columns) used for 2D linear transformations. + /// It can represent transformations such as translation, rotation, or scaling. + /// It consists of a three <see cref="Vector2"/> values: x, y, and the origin. + /// + /// For more information, read this documentation article: + /// https://docs.godotengine.org/en/latest/tutorials/math/matrices_and_transforms.html + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Transform2D : IEquatable<Transform2D> { + /// <summary> + /// The basis matrix's X vector (column 0). Equivalent to array index `[0]`. + /// </summary> + /// <value></value> public Vector2 x; + + /// <summary> + /// The basis matrix's Y vector (column 1). Equivalent to array index `[1]`. + /// </summary> public Vector2 y; + + /// <summary> + /// The origin vector (column 2, the third column). Equivalent to array index `[2]`. + /// The origin vector represents translation. + /// </summary> public Vector2 origin; + /// <summary> + /// The rotation of this transformation matrix. + /// </summary> + /// <value>Getting is equivalent to calling <see cref="Mathf.Atan2(real_t, real_t)"/> with the values of <see cref="x"/>.</value> public real_t Rotation { get { - real_t det = BasisDeterminant(); - Transform2D t = Orthonormalized(); - if (det < 0) - { - t.ScaleBasis(new Vector2(1, -1)); - } - return Mathf.Atan2(t.x.y, t.x.x); + return Mathf.Atan2(x.y, x.x); } set { @@ -38,6 +57,10 @@ namespace Godot } } + /// <summary> + /// The scale of this transformation matrix. + /// </summary> + /// <value>Equivalent to the lengths of each column vector, but Y is negative if the determinant is negative.</value> public Vector2 Scale { get @@ -47,8 +70,7 @@ namespace Godot } set { - x = x.Normalized(); - y = y.Normalized(); + value /= Scale; // Value becomes what's called "delta_scale" in core. x *= value.x; y *= value.y; } @@ -112,6 +134,11 @@ namespace Godot } } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation, scaling, and translation. + /// </summary> + /// <returns>The inverse transformation matrix.</returns> public Transform2D AffineInverse() { real_t det = BasisDeterminant(); @@ -135,28 +162,58 @@ namespace Godot return inv; } + /// <summary> + /// Returns the determinant of the basis matrix. If the basis is + /// uniformly scaled, its determinant is the square of the scale. + /// + /// A negative determinant means the Y scale is negative. + /// A zero determinant means the basis isn't invertible, + /// and is usually considered invalid. + /// </summary> + /// <returns>The determinant of the basis matrix.</returns> private real_t BasisDeterminant() { return x.x * y.y - x.y * y.x; } + /// <summary> + /// Returns a vector transformed (multiplied) by the basis matrix. + /// This method does not account for translation (the origin vector). + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector2 BasisXform(Vector2 v) { return new Vector2(Tdotx(v), Tdoty(v)); } + /// <summary> + /// Returns a vector transformed (multiplied) by the inverse basis matrix. + /// This method does not account for translation (the origin vector). + /// + /// Note: This results in a multiplication by the inverse of the + /// basis matrix only if it represents a rotation-reflection. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector2 BasisXformInv(Vector2 v) { return new Vector2(x.Dot(v), y.Dot(v)); } - public Transform2D InterpolateWith(Transform2D m, real_t c) + /// <summary> + /// Interpolates this transform to the other `transform` by `weight`. + /// </summary> + /// <param name="transform">The other transform.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated transform.</returns> + public Transform2D InterpolateWith(Transform2D transform, real_t weight) { real_t r1 = Rotation; - real_t r2 = m.Rotation; + real_t r2 = transform.Rotation; Vector2 s1 = Scale; - Vector2 s2 = m.Scale; + Vector2 s2 = transform.Scale; // Slerp rotation var v1 = new Vector2(Mathf.Cos(r1), Mathf.Sin(r1)); @@ -172,28 +229,34 @@ namespace Godot if (dot > 0.9995f) { // Linearly interpolate to avoid numerical precision issues - v = v1.LinearInterpolate(v2, c).Normalized(); + v = v1.Lerp(v2, weight).Normalized(); } else { - real_t angle = c * Mathf.Acos(dot); + real_t angle = weight * Mathf.Acos(dot); Vector2 v3 = (v2 - v1 * dot).Normalized(); v = v1 * Mathf.Cos(angle) + v3 * Mathf.Sin(angle); } // Extract parameters Vector2 p1 = origin; - Vector2 p2 = m.origin; + Vector2 p2 = transform.origin; // Construct matrix - var res = new Transform2D(Mathf.Atan2(v.y, v.x), p1.LinearInterpolate(p2, c)); - Vector2 scale = s1.LinearInterpolate(s2, c); + var res = new Transform2D(Mathf.Atan2(v.y, v.x), p1.Lerp(p2, weight)); + Vector2 scale = s1.Lerp(s2, weight); res.x *= scale; res.y *= scale; return res; } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation and translation + /// (no scaling, use <see cref="AffineInverse"/> for transforms with scaling). + /// </summary> + /// <returns>The inverse matrix.</returns> public Transform2D Inverse() { var inv = this; @@ -208,6 +271,11 @@ namespace Godot return inv; } + /// <summary> + /// Returns the transform with the basis orthogonal (90 degrees), + /// and normalized axis vectors (scale of 1 or -1). + /// </summary> + /// <returns>The orthonormalized transform.</returns> public Transform2D Orthonormalized() { var on = this; @@ -225,11 +293,21 @@ namespace Godot return on; } + /// <summary> + /// Rotates the transform by `phi` (in radians), using matrix multiplication. + /// </summary> + /// <param name="phi">The angle to rotate, in radians.</param> + /// <returns>The rotated transformation matrix.</returns> public Transform2D Rotated(real_t phi) { return this * new Transform2D(phi, new Vector2()); } + /// <summary> + /// Scales the transform by the given scaling factor, using matrix multiplication. + /// </summary> + /// <param name="scale">The scale to introduce.</param> + /// <returns>The scaled transformation matrix.</returns> public Transform2D Scaled(Vector2 scale) { var copy = this; @@ -257,6 +335,15 @@ namespace Godot return this[0, 1] * with[0] + this[1, 1] * with[1]; } + /// <summary> + /// Translates the transform by the given `offset`, + /// relative to the transform's basis vectors. + /// + /// Unlike <see cref="Rotated"/> and <see cref="Scaled"/>, + /// this does not use matrix multiplication. + /// </summary> + /// <param name="offset">The offset to translate by.</param> + /// <returns>The translated matrix.</returns> public Transform2D Translated(Vector2 offset) { var copy = this; @@ -264,11 +351,21 @@ namespace Godot return copy; } + /// <summary> + /// Returns a vector transformed (multiplied) by this transformation matrix. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector2 Xform(Vector2 v) { return new Vector2(Tdotx(v), Tdoty(v)) + origin; } + /// <summary> + /// Returns a vector transformed (multiplied) by the inverse transformation matrix. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector2 XformInv(Vector2 v) { Vector2 vInv = v - origin; @@ -280,11 +377,30 @@ namespace Godot private static readonly Transform2D _flipX = new Transform2D(-1, 0, 0, 1, 0, 0); private static readonly Transform2D _flipY = new Transform2D(1, 0, 0, -1, 0, 0); - public static Transform2D Identity => _identity; - public static Transform2D FlipX => _flipX; - public static Transform2D FlipY => _flipY; + /// <summary> + /// The identity transform, with no translation, rotation, or scaling applied. + /// This is used as a replacement for `Transform2D()` in GDScript. + /// Do not use `new Transform2D()` with no arguments in C#, because it sets all values to zero. + /// </summary> + /// <value>Equivalent to `new Transform2D(Vector2.Right, Vector2.Down, Vector2.Zero)`.</value> + public static Transform2D Identity { get { return _identity; } } + /// <summary> + /// The transform that will flip something along the X axis. + /// </summary> + /// <value>Equivalent to `new Transform2D(Vector2.Left, Vector2.Down, Vector2.Zero)`.</value> + public static Transform2D FlipX { get { return _flipX; } } + /// <summary> + /// The transform that will flip something along the Y axis. + /// </summary> + /// <value>Equivalent to `new Transform2D(Vector2.Right, Vector2.Up, Vector2.Zero)`.</value> + public static Transform2D FlipY { get { return _flipY; } } - // Constructors + /// <summary> + /// Constructs a transformation matrix from 3 vectors (matrix columns). + /// </summary> + /// <param name="xAxis">The X vector, or column index 0.</param> + /// <param name="yAxis">The Y vector, or column index 1.</param> + /// <param name="originPos">The origin vector, or column index 2.</param> public Transform2D(Vector2 xAxis, Vector2 yAxis, Vector2 originPos) { x = xAxis; @@ -292,7 +408,16 @@ namespace Godot origin = originPos; } - // Arguments are named such that xy is equal to calling x.y + /// <summary> + /// Constructs a transformation matrix from the given components. + /// Arguments are named such that xy is equal to calling x.y + /// </summary> + /// <param name="xx">The X component of the X column vector, accessed via `t.x.x` or `[0][0]`</param> + /// <param name="xy">The Y component of the X column vector, accessed via `t.x.y` or `[0][1]`</param> + /// <param name="yx">The X component of the Y column vector, accessed via `t.y.x` or `[1][0]`</param> + /// <param name="yy">The Y component of the Y column vector, accessed via `t.y.y` or `[1][1]`</param> + /// <param name="ox">The X component of the origin vector, accessed via `t.origin.x` or `[2][0]`</param> + /// <param name="oy">The Y component of the origin vector, accessed via `t.origin.y` or `[2][1]`</param> public Transform2D(real_t xx, real_t xy, real_t yx, real_t yy, real_t ox, real_t oy) { x = new Vector2(xx, xy); @@ -300,6 +425,11 @@ namespace Godot origin = new Vector2(ox, oy); } + /// <summary> + /// Constructs a transformation matrix from a rotation value and origin vector. + /// </summary> + /// <param name="rot">The rotation of the new transform, in radians.</param> + /// <param name="pos">The origin vector, or column index 2.</param> public Transform2D(real_t rot, Vector2 pos) { x.x = y.y = Mathf.Cos(rot); @@ -345,6 +475,12 @@ namespace Godot return x.Equals(other.x) && y.Equals(other.y) && origin.Equals(other.origin); } + /// <summary> + /// Returns true if this transform and `other` are approximately equal, by running + /// <see cref="Vector2.IsEqualApprox(Vector2)"/> on each component. + /// </summary> + /// <param name="other">The other transform to compare.</param> + /// <returns>Whether or not the matrices are approximately equal.</returns> public bool IsEqualApprox(Transform2D other) { return x.IsEqualApprox(other.x) && y.IsEqualApprox(other.y) && origin.IsEqualApprox(other.origin); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs index f7b13198f8..26bd828a5b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs @@ -21,15 +21,29 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector2 : IEquatable<Vector2> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, Y } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public real_t x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public real_t y; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`.</value> public real_t this[int index] { get @@ -76,41 +90,80 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(real_t)"/> called on each component.</returns> public Vector2 Abs() { return new Vector2(Mathf.Abs(x), Mathf.Abs(y)); } + /// <summary> + /// Returns this vector's angle with respect to the X axis, or (1, 0) vector, in radians. + /// + /// Equivalent to the result of <see cref="Mathf.Atan2(real_t, real_t)"/> when + /// called with the vector's `y` and `x` as parameters: `Mathf.Atan2(v.y, v.x)`. + /// </summary> + /// <returns>The angle of this vector, in radians.</returns> public real_t Angle() { return Mathf.Atan2(y, x); } + /// <summary> + /// Returns the angle to the given vector, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleTo(Vector2 to) { return Mathf.Atan2(Cross(to), Dot(to)); } + /// <summary> + /// Returns the angle between the line connecting the two points and the X axis, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleToPoint(Vector2 to) { return Mathf.Atan2(y - to.y, x - to.x); } + /// <summary> + /// Returns the aspect ratio of this vector, the ratio of `x` to `y`. + /// </summary> + /// <returns>The `x` component divided by the `y` component.</returns> public real_t Aspect() { return x / y; } - public Vector2 Bounce(Vector2 n) + /// <summary> + /// Returns the vector "bounced off" from a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to bounce off. Must be normalized.</param> + /// <returns>The bounced vector.</returns> + public Vector2 Bounce(Vector2 normal) { - return -Reflect(n); + return -Reflect(normal); } + /// <summary> + /// Returns a new vector with all components rounded up (towards positive infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Ceil"/> called on each component.</returns> public Vector2 Ceil() { return new Vector2(Mathf.Ceil(x), Mathf.Ceil(y)); } + /// <summary> + /// Returns the vector with a maximum length by limiting its length to `length`. + /// </summary> + /// <param name="length">The length to limit to.</param> + /// <returns>The vector with its length limited.</returns> public Vector2 Clamped(real_t length) { var v = this; @@ -125,17 +178,30 @@ namespace Godot return v; } + /// <summary> + /// Returns the cross product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>The cross product value.</returns> public real_t Cross(Vector2 b) { return x * b.y - y * b.x; } + /// <summary> + /// Performs a cubic interpolation between vectors `preA`, this vector, `b`, and `postB`, by the given amount `t`. + /// </summary> + /// <param name="b">The destination vector.</param> + /// <param name="preA">A vector before this vector.</param> + /// <param name="postB">A vector after `b`.</param> + /// <param name="t">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated vector.</returns> public Vector2 CubicInterpolate(Vector2 b, Vector2 preA, Vector2 postB, real_t t) { - var p0 = preA; - var p1 = this; - var p2 = b; - var p3 = postB; + Vector2 p0 = preA; + Vector2 p1 = this; + Vector2 p2 = b; + Vector2 p3 = postB; real_t t2 = t * t; real_t t3 = t2 * t; @@ -146,56 +212,153 @@ namespace Godot (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3); } + /// <summary> + /// Returns the normalized vector pointing from this vector to `b`. + /// </summary> + /// <param name="b">The other vector to point towards.</param> + /// <returns>The direction from this vector to `b`.</returns> public Vector2 DirectionTo(Vector2 b) { return new Vector2(b.x - x, b.y - y).Normalized(); } + /// <summary> + /// Returns the squared distance between this vector and `to`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public real_t DistanceSquaredTo(Vector2 to) { return (x - to.x) * (x - to.x) + (y - to.y) * (y - to.y); } + /// <summary> + /// Returns the distance between this vector and `to`. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector2 to) { return Mathf.Sqrt((x - to.x) * (x - to.x) + (y - to.y) * (y - to.y)); } + /// <summary> + /// Returns the dot product of this vector and `with`. + /// </summary> + /// <param name="with">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public real_t Dot(Vector2 with) { return x * with.x + y * with.y; } + /// <summary> + /// Returns a new vector with all components rounded down (towards negative infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Floor"/> called on each component.</returns> public Vector2 Floor() { return new Vector2(Mathf.Floor(x), Mathf.Floor(y)); } + /// <summary> + /// Returns the inverse of this vector. This is the same as `new Vector2(1 / v.x, 1 / v.y)`. + /// </summary> + /// <returns>The inverse of this vector.</returns> + public Vector2 Inverse() + { + return new Vector2(1 / x, 1 / y); + } + + /// <summary> + /// Returns true if the vector is normalized, and false otherwise. + /// </summary> + /// <returns>A bool indicating whether or not the vector is normalized.</returns> public bool IsNormalized() { return Mathf.Abs(LengthSquared() - 1.0f) < Mathf.Epsilon; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { return Mathf.Sqrt(x * x + y * y); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public real_t LengthSquared() { return x * x + y * y; } - public Vector2 LinearInterpolate(Vector2 b, real_t t) - { - var res = this; - - res.x += t * (b.x - x); - res.y += t * (b.y - y); - - return res; - } - + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector2 Lerp(Vector2 to, real_t weight) + { + return new Vector2 + ( + Mathf.Lerp(x, to.x, weight), + Mathf.Lerp(y, to.y, weight) + ); + } + + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by the vector amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A vector with components on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector2 Lerp(Vector2 to, Vector2 weight) + { + return new Vector2 + ( + Mathf.Lerp(x, to.x, weight.x), + Mathf.Lerp(y, to.y, weight.y) + ); + } + + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> + public Axis MaxAxis() + { + return x < y ? Axis.Y : Axis.X; + } + + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.Y"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> + public Axis MinAxis() + { + return x < y ? Axis.X : Axis.Y; + } + + /// <summary> + /// Moves this vector toward `to` by the fixed `delta` amount. + /// </summary> + /// <param name="to">The vector to move towards.</param> + /// <param name="delta">The amount to move towards by.</param> + /// <returns>The resulting vector.</returns> public Vector2 MoveToward(Vector2 to, real_t delta) { var v = this; @@ -204,6 +367,10 @@ namespace Godot return len <= delta || len < Mathf.Epsilon ? to : v + vd / len * delta; } + /// <summary> + /// Returns the vector scaled to unit length. Equivalent to `v / v.Length()`. + /// </summary> + /// <returns>A normalized version of the vector.</returns> public Vector2 Normalized() { var v = this; @@ -211,6 +378,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `mod`.</returns> public Vector2 PosMod(real_t mod) { Vector2 v; @@ -219,6 +391,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `modv`'s components.</returns> public Vector2 PosMod(Vector2 modv) { Vector2 v; @@ -227,27 +404,59 @@ namespace Godot return v; } + /// <summary> + /// Returns this vector projected onto another vector `b`. + /// </summary> + /// <param name="onNormal">The vector to project onto.</param> + /// <returns>The projected vector.</returns> public Vector2 Project(Vector2 onNormal) { return onNormal * (Dot(onNormal) / onNormal.LengthSquared()); } - public Vector2 Reflect(Vector2 n) + /// <summary> + /// Returns this vector reflected from a plane defined by the given `normal`. + /// </summary> + /// <param name="normal">The normal vector defining the plane to reflect from. Must be normalized.</param> + /// <returns>The reflected vector.</returns> + public Vector2 Reflect(Vector2 normal) { - return 2 * Dot(n) * n - this; +#if DEBUG + if (!normal.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(normal)); + } +#endif + return 2 * Dot(normal) * normal - this; } + /// <summary> + /// Rotates this vector by `phi` radians. + /// </summary> + /// <param name="phi">The angle to rotate by, in radians.</param> + /// <returns>The rotated vector.</returns> public Vector2 Rotated(real_t phi) { real_t rads = Angle() + phi; return new Vector2(Mathf.Cos(rads), Mathf.Sin(rads)) * Length(); } + /// <summary> + /// Returns this vector with all components rounded to the nearest integer, + /// with halfway cases rounded towards the nearest multiple of two. + /// </summary> + /// <returns>The rounded vector.</returns> public Vector2 Round() { return new Vector2(Mathf.Round(x), Mathf.Round(y)); } + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(real_t)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector2 Sign() { Vector2 v; @@ -256,23 +465,57 @@ namespace Godot return v; } - public Vector2 Slerp(Vector2 b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this vector and `to` by amount `weight`. + /// + /// Note: Both vectors must be normalized. + /// </summary> + /// <param name="to">The destination vector for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector2 Slerp(Vector2 to, real_t weight) { - real_t theta = AngleTo(b); - return Rotated(theta * t); +#if DEBUG + if (!IsNormalized()) + { + throw new InvalidOperationException("Vector2.Slerp: From vector is not normalized."); + } + if (!to.IsNormalized()) + { + throw new InvalidOperationException("Vector2.Slerp: `to` is not normalized."); + } +#endif + return Rotated(AngleTo(to) * weight); } - public Vector2 Slide(Vector2 n) + /// <summary> + /// Returns this vector slid along a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to slide on.</param> + /// <returns>The slid vector.</returns> + public Vector2 Slide(Vector2 normal) { - return this - n * Dot(n); + return this - normal * Dot(normal); } - public Vector2 Snapped(Vector2 by) + /// <summary> + /// Returns this vector with each component snapped to the nearest multiple of `step`. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public Vector2 Snapped(Vector2 step) { - return new Vector2(Mathf.Stepify(x, by.x), Mathf.Stepify(y, by.y)); + return new Vector2(Mathf.Stepify(x, step.x), Mathf.Stepify(y, step.y)); } - public Vector2 Tangent() + /// <summary> + /// Returns a perpendicular vector rotated 90 degrees counter-clockwise + /// compared to the original, with the same length. + /// </summary> + /// <returns>The perpendicular vector.</returns> + public Vector2 Perpendicular() { return new Vector2(y, -x); } @@ -280,7 +523,6 @@ namespace Godot // Constants private static readonly Vector2 _zero = new Vector2(0, 0); private static readonly Vector2 _one = new Vector2(1, 1); - private static readonly Vector2 _negOne = new Vector2(-1, -1); private static readonly Vector2 _inf = new Vector2(Mathf.Inf, Mathf.Inf); private static readonly Vector2 _up = new Vector2(0, -1); @@ -288,22 +530,58 @@ namespace Godot private static readonly Vector2 _right = new Vector2(1, 0); private static readonly Vector2 _left = new Vector2(-1, 0); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector2(0, 0)`</value> public static Vector2 Zero { get { return _zero; } } - public static Vector2 NegOne { get { return _negOne; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector2(1, 1)`</value> public static Vector2 One { get { return _one; } } + /// <summary> + /// Infinity vector, a vector with all components set to `Mathf.Inf`. + /// </summary> + /// <value>Equivalent to `new Vector2(Mathf.Inf, Mathf.Inf)`</value> public static Vector2 Inf { get { return _inf; } } + /// <summary> + /// Up unit vector. Y is down in 2D, so this vector points -Y. + /// </summary> + /// <value>Equivalent to `new Vector2(0, -1)`</value> public static Vector2 Up { get { return _up; } } + /// <summary> + /// Down unit vector. Y is down in 2D, so this vector points +Y. + /// </summary> + /// <value>Equivalent to `new Vector2(0, 1)`</value> public static Vector2 Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the direction of right. + /// </summary> + /// <value>Equivalent to `new Vector2(1, 0)`</value> public static Vector2 Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the direction of left. + /// </summary> + /// <value>Equivalent to `new Vector2(-1, 0)`</value> public static Vector2 Left { get { return _left; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector2"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> public Vector2(real_t x, real_t y) { this.x = x; this.y = y; } + + /// <summary> + /// Constructs a new <see cref="Vector2"/> from an existing <see cref="Vector2"/>. + /// </summary> + /// <param name="v">The existing <see cref="Vector2"/>.</param> public Vector2(Vector2 v) { x = v.x; @@ -445,6 +723,12 @@ namespace Godot return x == other.x && y == other.y; } + /// <summary> + /// Returns true if this vector and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other vector to compare.</param> + /// <returns>Whether or not the vectors are approximately equal.</returns> public bool IsEqualApprox(Vector2 other) { return Mathf.IsEqualApprox(x, other.x) && Mathf.IsEqualApprox(y, other.y); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs index 7dc22d7918..8dd9ab2f0d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs @@ -16,15 +16,29 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector2i : IEquatable<Vector2i> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, Y } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public int x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public int y; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`.</value> public int this[int index] { get @@ -55,56 +69,102 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(int)"/> called on each component.</returns> public Vector2i Abs() { return new Vector2i(Mathf.Abs(x), Mathf.Abs(y)); } + /// <summary> + /// Returns this vector's angle with respect to the X axis, or (1, 0) vector, in radians. + /// + /// Equivalent to the result of <see cref="Mathf.Atan2(real_t, real_t)"/> when + /// called with the vector's `y` and `x` as parameters: `Mathf.Atan2(v.y, v.x)`. + /// </summary> + /// <returns>The angle of this vector, in radians.</returns> public real_t Angle() { return Mathf.Atan2(y, x); } + /// <summary> + /// Returns the angle to the given vector, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleTo(Vector2i to) { return Mathf.Atan2(Cross(to), Dot(to)); } + /// <summary> + /// Returns the angle between the line connecting the two points and the X axis, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleToPoint(Vector2i to) { return Mathf.Atan2(y - to.y, x - to.x); } + /// <summary> + /// Returns the aspect ratio of this vector, the ratio of `x` to `y`. + /// </summary> + /// <returns>The `x` component divided by the `y` component.</returns> public real_t Aspect() { return x / (real_t)y; } - public Vector2i Bounce(Vector2i n) - { - return -Reflect(n); - } - + /// <summary> + /// Returns the cross product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>The cross product vector.</returns> public int Cross(Vector2i b) { return x * b.y - y * b.x; } + /// <summary> + /// Returns the squared distance between this vector and `b`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public int DistanceSquaredTo(Vector2i b) { return (b - this).LengthSquared(); } + /// <summary> + /// Returns the distance between this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector2i b) { return (b - this).Length(); } + /// <summary> + /// Returns the dot product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public int Dot(Vector2i b) { return x * b.x + y * b.y; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { int x2 = x * x; @@ -113,6 +173,12 @@ namespace Godot return Mathf.Sqrt(x2 + y2); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public int LengthSquared() { int x2 = x * x; @@ -121,16 +187,31 @@ namespace Godot return x2 + y2; } + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> public Axis MaxAxis() { return x < y ? Axis.Y : Axis.X; } + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.Y"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> public Axis MinAxis() { - return x > y ? Axis.Y : Axis.X; + return x < y ? Axis.X : Axis.Y; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `mod`.</returns> public Vector2i PosMod(int mod) { Vector2i v = this; @@ -139,6 +220,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `modv`'s components.</returns> public Vector2i PosMod(Vector2i modv) { Vector2i v = this; @@ -147,11 +233,12 @@ namespace Godot return v; } - public Vector2i Reflect(Vector2i n) - { - return 2 * Dot(n) * n - this; - } - + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(int)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector2i Sign() { Vector2i v = this; @@ -160,9 +247,14 @@ namespace Godot return v; } - public Vector2i Tangent() + /// <summary> + /// Returns a vector rotated 90 degrees counter-clockwise + /// compared to the original, with the same length. + /// </summary> + /// <returns>The perpendicular vector.</returns> + public Vector2 Perpendicular() { - return new Vector2i(y, -x); + return new Vector2(y, -x); } // Constants @@ -174,25 +266,64 @@ namespace Godot private static readonly Vector2i _right = new Vector2i(1, 0); private static readonly Vector2i _left = new Vector2i(-1, 0); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector2i(0, 0)`</value> public static Vector2i Zero { get { return _zero; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector2i(1, 1)`</value> public static Vector2i One { get { return _one; } } + /// <summary> + /// Up unit vector. Y is down in 2D, so this vector points -Y. + /// </summary> + /// <value>Equivalent to `new Vector2i(0, -1)`</value> public static Vector2i Up { get { return _up; } } + /// <summary> + /// Down unit vector. Y is down in 2D, so this vector points +Y. + /// </summary> + /// <value>Equivalent to `new Vector2i(0, 1)`</value> public static Vector2i Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the direction of right. + /// </summary> + /// <value>Equivalent to `new Vector2i(1, 0)`</value> public static Vector2i Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the direction of left. + /// </summary> + /// <value>Equivalent to `new Vector2i(-1, 0)`</value> public static Vector2i Left { get { return _left; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector2i"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> public Vector2i(int x, int y) { this.x = x; this.y = y; } + + /// <summary> + /// Constructs a new <see cref="Vector2i"/> from an existing <see cref="Vector2i"/>. + /// </summary> + /// <param name="vi">The existing <see cref="Vector2i"/>.</param> public Vector2i(Vector2i vi) { this.x = vi.x; this.y = vi.y; } + + /// <summary> + /// Constructs a new <see cref="Vector2i"/> from an existing <see cref="Vector2"/> + /// by rounding the components via <see cref="Mathf.RoundToInt(real_t)"/>. + /// </summary> + /// <param name="v">The <see cref="Vector2"/> to convert.</param> public Vector2i(Vector2 v) { this.x = Mathf.RoundToInt(v.x); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs index a43836e985..d9b16a6afd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs @@ -21,6 +21,10 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector3 : IEquatable<Vector3> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, @@ -28,10 +32,23 @@ namespace Godot Z } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public real_t x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public real_t y; + /// <summary> + /// The vector's Z component. Also accessible by using the index position `[2]`. + /// </summary> public real_t z; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`, `[2]` is equivalent to `.z`.</value> public real_t this[int index] { get @@ -84,26 +101,49 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(real_t)"/> called on each component.</returns> public Vector3 Abs() { return new Vector3(Mathf.Abs(x), Mathf.Abs(y), Mathf.Abs(z)); } + /// <summary> + /// Returns the minimum angle to the given vector, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleTo(Vector3 to) { return Mathf.Atan2(Cross(to).Length(), Dot(to)); } - public Vector3 Bounce(Vector3 n) + /// <summary> + /// Returns this vector "bounced off" from a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to bounce off. Must be normalized.</param> + /// <returns>The bounced vector.</returns> + public Vector3 Bounce(Vector3 normal) { - return -Reflect(n); + return -Reflect(normal); } + /// <summary> + /// Returns a new vector with all components rounded up (towards positive infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Ceil"/> called on each component.</returns> public Vector3 Ceil() { return new Vector3(Mathf.Ceil(x), Mathf.Ceil(y), Mathf.Ceil(z)); } + /// <summary> + /// Returns the cross product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>The cross product vector.</returns> public Vector3 Cross(Vector3 b) { return new Vector3 @@ -114,12 +154,21 @@ namespace Godot ); } + /// <summary> + /// Performs a cubic interpolation between vectors `preA`, this vector, + /// `b`, and `postB`, by the given amount `t`. + /// </summary> + /// <param name="b">The destination vector.</param> + /// <param name="preA">A vector before this vector.</param> + /// <param name="postB">A vector after `b`.</param> + /// <param name="t">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated vector.</returns> public Vector3 CubicInterpolate(Vector3 b, Vector3 preA, Vector3 postB, real_t t) { - var p0 = preA; - var p1 = this; - var p2 = b; - var p3 = postB; + Vector3 p0 = preA; + Vector3 p1 = this; + Vector3 p2 = b; + Vector3 p3 = postB; real_t t2 = t * t; real_t t3 = t2 * t; @@ -131,41 +180,79 @@ namespace Godot ); } + /// <summary> + /// Returns the normalized vector pointing from this vector to `b`. + /// </summary> + /// <param name="b">The other vector to point towards.</param> + /// <returns>The direction from this vector to `b`.</returns> public Vector3 DirectionTo(Vector3 b) { return new Vector3(b.x - x, b.y - y, b.z - z).Normalized(); } + /// <summary> + /// Returns the squared distance between this vector and `b`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public real_t DistanceSquaredTo(Vector3 b) { return (b - this).LengthSquared(); } + /// <summary> + /// Returns the distance between this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector3 b) { return (b - this).Length(); } + /// <summary> + /// Returns the dot product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public real_t Dot(Vector3 b) { return x * b.x + y * b.y + z * b.z; } + /// <summary> + /// Returns a new vector with all components rounded down (towards negative infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Floor"/> called on each component.</returns> public Vector3 Floor() { return new Vector3(Mathf.Floor(x), Mathf.Floor(y), Mathf.Floor(z)); } + /// <summary> + /// Returns the inverse of this vector. This is the same as `new Vector3(1 / v.x, 1 / v.y, 1 / v.z)`. + /// </summary> + /// <returns>The inverse of this vector.</returns> public Vector3 Inverse() { - return new Vector3(1.0f / x, 1.0f / y, 1.0f / z); + return new Vector3(1 / x, 1 / y, 1 / z); } + /// <summary> + /// Returns true if the vector is normalized, and false otherwise. + /// </summary> + /// <returns>A bool indicating whether or not the vector is normalized.</returns> public bool IsNormalized() { return Mathf.Abs(LengthSquared() - 1.0f) < Mathf.Epsilon; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { real_t x2 = x * x; @@ -175,6 +262,12 @@ namespace Godot return Mathf.Sqrt(x2 + y2 + z2); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public real_t LengthSquared() { real_t x2 = x * x; @@ -184,34 +277,78 @@ namespace Godot return x2 + y2 + z2; } - public Vector3 LinearInterpolate(Vector3 b, real_t t) + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector3 Lerp(Vector3 to, real_t weight) { return new Vector3 ( - x + t * (b.x - x), - y + t * (b.y - y), - z + t * (b.z - z) + Mathf.Lerp(x, to.x, weight), + Mathf.Lerp(y, to.y, weight), + Mathf.Lerp(z, to.z, weight) ); } - public Vector3 MoveToward(Vector3 to, real_t delta) + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by the vector amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A vector with components on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector3 Lerp(Vector3 to, Vector3 weight) { - var v = this; - var vd = to - v; - var len = vd.Length(); - return len <= delta || len < Mathf.Epsilon ? to : v + vd / len * delta; + return new Vector3 + ( + Mathf.Lerp(x, to.x, weight.x), + Mathf.Lerp(y, to.y, weight.y), + Mathf.Lerp(z, to.z, weight.z) + ); } + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> public Axis MaxAxis() { return x < y ? (y < z ? Axis.Z : Axis.Y) : (x < z ? Axis.Z : Axis.X); } + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.Z"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> public Axis MinAxis() { return x < y ? (x < z ? Axis.X : Axis.Z) : (y < z ? Axis.Y : Axis.Z); } + /// <summary> + /// Moves this vector toward `to` by the fixed `delta` amount. + /// </summary> + /// <param name="to">The vector to move towards.</param> + /// <param name="delta">The amount to move towards by.</param> + /// <returns>The resulting vector.</returns> + public Vector3 MoveToward(Vector3 to, real_t delta) + { + var v = this; + var vd = to - v; + var len = vd.Length(); + return len <= delta || len < Mathf.Epsilon ? to : v + vd / len * delta; + } + + /// <summary> + /// Returns the vector scaled to unit length. Equivalent to `v / v.Length()`. + /// </summary> + /// <returns>A normalized version of the vector.</returns> public Vector3 Normalized() { var v = this; @@ -219,6 +356,11 @@ namespace Godot return v; } + /// <summary> + /// Returns the outer product with `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>A <see cref="Basis"/> representing the outer product matrix.</returns> public Basis Outer(Vector3 b) { return new Basis( @@ -228,6 +370,11 @@ namespace Godot ); } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `mod`.</returns> public Vector3 PosMod(real_t mod) { Vector3 v; @@ -237,6 +384,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `modv`'s components.</returns> public Vector3 PosMod(Vector3 modv) { Vector3 v; @@ -246,30 +398,66 @@ namespace Godot return v; } + /// <summary> + /// Returns this vector projected onto another vector `b`. + /// </summary> + /// <param name="onNormal">The vector to project onto.</param> + /// <returns>The projected vector.</returns> public Vector3 Project(Vector3 onNormal) { return onNormal * (Dot(onNormal) / onNormal.LengthSquared()); } - public Vector3 Reflect(Vector3 n) + /// <summary> + /// Returns this vector reflected from a plane defined by the given `normal`. + /// </summary> + /// <param name="normal">The normal vector defining the plane to reflect from. Must be normalized.</param> + /// <returns>The reflected vector.</returns> + public Vector3 Reflect(Vector3 normal) { #if DEBUG - if (!n.IsNormalized()) - throw new ArgumentException("Argument is not normalized", nameof(n)); + if (!normal.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(normal)); + } #endif - return 2.0f * n * Dot(n) - this; + return 2.0f * Dot(normal) * normal - this; } - public Vector3 Round() + /// <summary> + /// Rotates this vector around a given `axis` vector by `phi` radians. + /// The `axis` vector must be a normalized vector. + /// </summary> + /// <param name="axis">The vector to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate by, in radians.</param> + /// <returns>The rotated vector.</returns> + public Vector3 Rotated(Vector3 axis, real_t phi) { - return new Vector3(Mathf.Round(x), Mathf.Round(y), Mathf.Round(z)); +#if DEBUG + if (!axis.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(axis)); + } +#endif + return new Basis(axis, phi).Xform(this); } - public Vector3 Rotated(Vector3 axis, real_t phi) + /// <summary> + /// Returns this vector with all components rounded to the nearest integer, + /// with halfway cases rounded towards the nearest multiple of two. + /// </summary> + /// <returns>The rounded vector.</returns> + public Vector3 Round() { - return new Basis(axis, phi).Xform(this); + return new Vector3(Mathf.Round(x), Mathf.Round(y), Mathf.Round(z)); } + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(real_t)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector3 Sign() { Vector3 v; @@ -279,44 +467,76 @@ namespace Godot return v; } - public Vector3 Slerp(Vector3 b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this vector and `to` by amount `weight`. + /// + /// Note: Both vectors must be normalized. + /// </summary> + /// <param name="to">The destination vector for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector3 Slerp(Vector3 to, real_t weight) { #if DEBUG if (!IsNormalized()) - throw new InvalidOperationException("Vector3 is not normalized"); + { + throw new InvalidOperationException("Vector3.Slerp: From vector is not normalized."); + } + if (!to.IsNormalized()) + { + throw new InvalidOperationException("Vector3.Slerp: `to` is not normalized."); + } #endif - real_t theta = AngleTo(b); - return Rotated(Cross(b), theta * t); + real_t theta = AngleTo(to); + return Rotated(Cross(to), theta * weight); } - public Vector3 Slide(Vector3 n) + /// <summary> + /// Returns this vector slid along a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to slide on.</param> + /// <returns>The slid vector.</returns> + public Vector3 Slide(Vector3 normal) { - return this - n * Dot(n); + return this - normal * Dot(normal); } - public Vector3 Snapped(Vector3 by) + /// <summary> + /// Returns this vector with each component snapped to the nearest multiple of `step`. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public Vector3 Snapped(Vector3 step) { return new Vector3 ( - Mathf.Stepify(x, by.x), - Mathf.Stepify(y, by.y), - Mathf.Stepify(z, by.z) + Mathf.Stepify(x, step.x), + Mathf.Stepify(y, step.y), + Mathf.Stepify(z, step.z) ); } + /// <summary> + /// Returns a diagonal matrix with the vector as main diagonal. + /// + /// This is equivalent to a Basis with no rotation or shearing and + /// this vector's components set as the scale. + /// </summary> + /// <returns>A Basis with the vector as its main diagonal.</returns> public Basis ToDiagonalMatrix() { return new Basis( - x, 0f, 0f, - 0f, y, 0f, - 0f, 0f, z + x, 0, 0, + 0, y, 0, + 0, 0, z ); } // Constants private static readonly Vector3 _zero = new Vector3(0, 0, 0); private static readonly Vector3 _one = new Vector3(1, 1, 1); - private static readonly Vector3 _negOne = new Vector3(-1, -1, -1); private static readonly Vector3 _inf = new Vector3(Mathf.Inf, Mathf.Inf, Mathf.Inf); private static readonly Vector3 _up = new Vector3(0, 1, 0); @@ -326,25 +546,74 @@ namespace Godot private static readonly Vector3 _forward = new Vector3(0, 0, -1); private static readonly Vector3 _back = new Vector3(0, 0, 1); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 0, 0)`</value> public static Vector3 Zero { get { return _zero; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector3(1, 1, 1)`</value> public static Vector3 One { get { return _one; } } - public static Vector3 NegOne { get { return _negOne; } } + /// <summary> + /// Infinity vector, a vector with all components set to `Mathf.Inf`. + /// </summary> + /// <value>Equivalent to `new Vector3(Mathf.Inf, Mathf.Inf, Mathf.Inf)`</value> public static Vector3 Inf { get { return _inf; } } + /// <summary> + /// Up unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 1, 0)`</value> public static Vector3 Up { get { return _up; } } + /// <summary> + /// Down unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3(0, -1, 0)`</value> public static Vector3 Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the local direction of right, + /// and the global direction of east. + /// </summary> + /// <value>Equivalent to `new Vector3(1, 0, 0)`</value> public static Vector3 Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the local direction of left, + /// and the global direction of west. + /// </summary> + /// <value>Equivalent to `new Vector3(-1, 0, 0)`</value> public static Vector3 Left { get { return _left; } } + /// <summary> + /// Forward unit vector. Represents the local direction of forward, + /// and the global direction of north. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 0, -1)`</value> public static Vector3 Forward { get { return _forward; } } + /// <summary> + /// Back unit vector. Represents the local direction of back, + /// and the global direction of south. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 0, 1)`</value> public static Vector3 Back { get { return _back; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector3"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> + /// <param name="z">The vector's Z component.</param> public Vector3(real_t x, real_t y, real_t z) { this.x = x; this.y = y; this.z = z; } + + /// <summary> + /// Constructs a new <see cref="Vector3"/> from an existing <see cref="Vector3"/>. + /// </summary> + /// <param name="v">The existing <see cref="Vector3"/>.</param> public Vector3(Vector3 v) { x = v.x; @@ -505,6 +774,12 @@ namespace Godot return x == other.x && y == other.y && z == other.z; } + /// <summary> + /// Returns true if this vector and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other vector to compare.</param> + /// <returns>Whether or not the vectors are approximately equal.</returns> public bool IsEqualApprox(Vector3 other) { return Mathf.IsEqualApprox(x, other.x) && Mathf.IsEqualApprox(y, other.y) && Mathf.IsEqualApprox(z, other.z); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs index c17f900131..bf25ba9cb3 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs @@ -16,6 +16,10 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector3i : IEquatable<Vector3i> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, @@ -23,10 +27,23 @@ namespace Godot Z } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public int x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public int y; + /// <summary> + /// The vector's Z component. Also accessible by using the index position `[2]`. + /// </summary> public int z; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`, `[2]` is equivalent to `.z`.</value> public int this[int index] { get @@ -62,39 +79,51 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(int)"/> called on each component.</returns> public Vector3i Abs() { - Vector3i v = this; - if (v.x < 0) - { - v.x = -v.x; - } - if (v.y < 0) - { - v.y = -v.y; - } - if (v.z < 0) - { - v.z = -v.z; - } - return v; + return new Vector3i(Mathf.Abs(x), Mathf.Abs(y), Mathf.Abs(z)); } + /// <summary> + /// Returns the squared distance between this vector and `b`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public int DistanceSquaredTo(Vector3i b) { return (b - this).LengthSquared(); } + /// <summary> + /// Returns the distance between this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector3i b) { return (b - this).Length(); } + /// <summary> + /// Returns the dot product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public int Dot(Vector3i b) { return x * b.x + y * b.y + z * b.z; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { int x2 = x * x; @@ -104,6 +133,12 @@ namespace Godot return Mathf.Sqrt(x2 + y2 + z2); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public int LengthSquared() { int x2 = x * x; @@ -113,16 +148,31 @@ namespace Godot return x2 + y2 + z2; } + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> public Axis MaxAxis() { return x < y ? (y < z ? Axis.Z : Axis.Y) : (x < z ? Axis.Z : Axis.X); } + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.Z"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> public Axis MinAxis() { return x < y ? (x < z ? Axis.X : Axis.Z) : (y < z ? Axis.Y : Axis.Z); } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `mod`.</returns> public Vector3i PosMod(int mod) { Vector3i v = this; @@ -132,6 +182,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `modv`'s components.</returns> public Vector3i PosMod(Vector3i modv) { Vector3i v = this; @@ -141,6 +196,12 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(int)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector3i Sign() { Vector3i v = this; @@ -161,29 +222,81 @@ namespace Godot private static readonly Vector3i _forward = new Vector3i(0, 0, -1); private static readonly Vector3i _back = new Vector3i(0, 0, 1); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 0, 0)`</value> public static Vector3i Zero { get { return _zero; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector3i(1, 1, 1)`</value> public static Vector3i One { get { return _one; } } + /// <summary> + /// Up unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 1, 0)`</value> public static Vector3i Up { get { return _up; } } + /// <summary> + /// Down unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, -1, 0)`</value> public static Vector3i Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the local direction of right, + /// and the global direction of east. + /// </summary> + /// <value>Equivalent to `new Vector3i(1, 0, 0)`</value> public static Vector3i Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the local direction of left, + /// and the global direction of west. + /// </summary> + /// <value>Equivalent to `new Vector3i(-1, 0, 0)`</value> public static Vector3i Left { get { return _left; } } + /// <summary> + /// Forward unit vector. Represents the local direction of forward, + /// and the global direction of north. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 0, -1)`</value> public static Vector3i Forward { get { return _forward; } } + /// <summary> + /// Back unit vector. Represents the local direction of back, + /// and the global direction of south. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 0, 1)`</value> public static Vector3i Back { get { return _back; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector3i"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> + /// <param name="z">The vector's Z component.</param> public Vector3i(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } + + /// <summary> + /// Constructs a new <see cref="Vector3i"/> from an existing <see cref="Vector3i"/>. + /// </summary> + /// <param name="vi">The existing <see cref="Vector3i"/>.</param> public Vector3i(Vector3i vi) { this.x = vi.x; this.y = vi.y; this.z = vi.z; } + + /// <summary> + /// Constructs a new <see cref="Vector3i"/> from an existing <see cref="Vector3"/> + /// by rounding the components via <see cref="Mathf.RoundToInt(real_t)"/>. + /// </summary> + /// <param name="v">The <see cref="Vector3"/> to convert.</param> public Vector3i(Vector3 v) { this.x = Mathf.RoundToInt(v.x); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index ba0bbd7630..86a16c17f1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -1,38 +1,17 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{AEBF0036-DA76-4341-B651-A3F2856AB2FA}</ProjectGuid> - <OutputType>Library</OutputType> <OutputPath>bin/$(Configuration)</OutputPath> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <RootNamespace>Godot</RootNamespace> - <AssemblyName>GodotSharp</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <TargetFramework>netstandard2.1</TargetFramework> <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile> - <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath> + <EnableDefaultItems>false</EnableDefaultItems> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>portable</DebugType> - <Optimize>true</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> + <PropertyGroup> + <DefineConstants>$(DefineConstants);GODOT</DefineConstants> </PropertyGroup> <ItemGroup> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> <Compile Include="Core\AABB.cs" /> <Compile Include="Core\Array.cs" /> <Compile Include="Core\Attributes\ExportAttribute.cs" /> @@ -52,6 +31,7 @@ <Compile Include="Core\Extensions\NodeExtensions.cs" /> <Compile Include="Core\Extensions\ObjectExtensions.cs" /> <Compile Include="Core\Extensions\ResourceLoaderExtensions.cs" /> + <Compile Include="Core\Extensions\SceneTreeExtensions.cs" /> <Compile Include="Core\GD.cs" /> <Compile Include="Core\GodotSynchronizationContext.cs" /> <Compile Include="Core\GodotTaskScheduler.cs" /> @@ -88,5 +68,4 @@ Fortunately code completion, go to definition and such still work. --> <Import Project="Generated\GeneratedIncludes.props" /> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> </Project> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs index f84e0183f6..da6f293871 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs @@ -1,27 +1,3 @@ -using System.Reflection; using System.Runtime.CompilerServices; -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotSharp")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] [assembly: InternalsVisibleTo("GodotSharpEditor")] diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index 22853797c1..a8c4ba96b5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -1,45 +1,26 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{8FBEC238-D944-4074-8548-B3B524305905}</ProjectGuid> - <OutputType>Library</OutputType> <OutputPath>bin/$(Configuration)</OutputPath> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <RootNamespace>Godot</RootNamespace> - <AssemblyName>GodotSharpEditor</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <TargetFramework>netstandard2.1</TargetFramework> <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile> - <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath> + <EnableDefaultItems>false</EnableDefaultItems> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>portable</DebugType> - <Optimize>true</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> + <PropertyGroup> + <DefineConstants>$(DefineConstants);GODOT</DefineConstants> </PropertyGroup> <ItemGroup> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> - <Compile Include="Properties\AssemblyInfo.cs" /> - </ItemGroup> - <Import Project="Generated\GeneratedIncludes.props" /> - <ItemGroup> <ProjectReference Include="..\GodotSharp\GodotSharp.csproj"> - <Private>False</Private> + <Private>false</Private> </ProjectReference> </ItemGroup> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> + <!-- + We import a props file with auto-generated includes. This works well with Rider. + However, Visual Studio and MonoDevelop won't list them in the solution explorer. + We can't use wildcards as there may be undesired old files still hanging around. + Fortunately code completion, go to definition and such still work. + --> + <Import Project="Generated\GeneratedIncludes.props" /> </Project> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs deleted file mode 100644 index 3684b7a3cb..0000000000 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Reflection; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotSharpEditor")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/modules/mono/glue/arguments_vector.h b/modules/mono/glue/arguments_vector.h index aeb466ba72..ab48904571 100644 --- a/modules/mono/glue/arguments_vector.h +++ b/modules/mono/glue/arguments_vector.h @@ -35,14 +35,13 @@ template <typename T, int POOL_SIZE = 5> struct ArgumentsVector { - private: T pool[POOL_SIZE]; T *_ptr; int size; - ArgumentsVector(); - ArgumentsVector(const ArgumentsVector &); + ArgumentsVector() = delete; + ArgumentsVector(const ArgumentsVector &) = delete; public: T *ptr() { return _ptr; } diff --git a/modules/mono/glue/base_object_glue.cpp b/modules/mono/glue/base_object_glue.cpp index f370883320..ebcd6d5e9c 100644 --- a/modules/mono/glue/base_object_glue.cpp +++ b/modules/mono/glue/base_object_glue.cpp @@ -28,10 +28,10 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "base_object_glue.h" - #ifdef MONO_GLUE_ENABLED +#include "core/class_db.h" +#include "core/object.h" #include "core/reference.h" #include "core/string_name.h" @@ -39,6 +39,7 @@ #include "../mono_gd/gd_mono_cache.h" #include "../mono_gd/gd_mono_class.h" #include "../mono_gd/gd_mono_internals.h" +#include "../mono_gd/gd_mono_marshal.h" #include "../mono_gd/gd_mono_utils.h" #include "../signal_awaiter_utils.h" #include "arguments_vector.h" @@ -140,16 +141,18 @@ MethodBind *godot_icall_Object_ClassDB_get_method(StringName *p_type, MonoString } MonoObject *godot_icall_Object_weakref(Object *p_ptr) { - if (!p_ptr) + if (!p_ptr) { return nullptr; + } Ref<WeakRef> wref; Reference *ref = Object::cast_to<Reference>(p_ptr); if (ref) { REF r = ref; - if (!r.is_valid()) + if (!r.is_valid()) { return nullptr; + } wref.instance(); wref->set_ref(r); @@ -241,6 +244,7 @@ void godot_register_object_icalls() { mono_add_internal_call("Godot.Object::godot_icall_Object_Ctor", (void *)godot_icall_Object_Ctor); mono_add_internal_call("Godot.Object::godot_icall_Object_Disposed", (void *)godot_icall_Object_Disposed); mono_add_internal_call("Godot.Object::godot_icall_Reference_Disposed", (void *)godot_icall_Reference_Disposed); + mono_add_internal_call("Godot.Object::godot_icall_Object_ConnectEventSignals", (void *)godot_icall_Object_ConnectEventSignals); mono_add_internal_call("Godot.Object::godot_icall_Object_ClassDB_get_method", (void *)godot_icall_Object_ClassDB_get_method); mono_add_internal_call("Godot.Object::godot_icall_Object_ToString", (void *)godot_icall_Object_ToString); mono_add_internal_call("Godot.Object::godot_icall_Object_weakref", (void *)godot_icall_Object_weakref); diff --git a/modules/mono/glue/base_object_glue.h b/modules/mono/glue/base_object_glue.h deleted file mode 100644 index 67769f3061..0000000000 --- a/modules/mono/glue/base_object_glue.h +++ /dev/null @@ -1,73 +0,0 @@ -/*************************************************************************/ -/* base_object_glue.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 BASE_OBJECT_GLUE_H -#define BASE_OBJECT_GLUE_H - -#ifdef MONO_GLUE_ENABLED - -#include "core/class_db.h" -#include "core/object.h" - -#include "../mono_gd/gd_mono_marshal.h" - -Object *godot_icall_Object_Ctor(MonoObject *p_obj); - -void godot_icall_Object_Disposed(MonoObject *p_obj, Object *p_ptr); - -void godot_icall_Reference_Disposed(MonoObject *p_obj, Object *p_ptr, MonoBoolean p_is_finalizer); - -void godot_icall_Object_ConnectEventSignals(Object *p_ptr); - -MethodBind *godot_icall_Object_ClassDB_get_method(StringName *p_type, MonoString *p_method); - -MonoObject *godot_icall_Object_weakref(Object *p_ptr); - -Error godot_icall_SignalAwaiter_connect(Object *p_source, StringName *p_signal, Object *p_target, MonoObject *p_awaiter); - -// DynamicGodotObject - -MonoArray *godot_icall_DynamicGodotObject_SetMemberList(Object *p_ptr); - -MonoBoolean godot_icall_DynamicGodotObject_InvokeMember(Object *p_ptr, MonoString *p_name, MonoArray *p_args, MonoObject **r_result); - -MonoBoolean godot_icall_DynamicGodotObject_GetMember(Object *p_ptr, MonoString *p_name, MonoObject **r_result); - -MonoBoolean godot_icall_DynamicGodotObject_SetMember(Object *p_ptr, MonoString *p_name, MonoObject *p_value); - -MonoString *godot_icall_Object_ToString(Object *p_ptr); - -// Register internal calls - -void godot_register_object_icalls(); - -#endif // MONO_GLUE_ENABLED - -#endif // BASE_OBJECT_GLUE_H diff --git a/modules/mono/glue/collections_glue.cpp b/modules/mono/glue/collections_glue.cpp index 4e3dc9c4ea..766b00d612 100644 --- a/modules/mono/glue/collections_glue.cpp +++ b/modules/mono/glue/collections_glue.cpp @@ -28,14 +28,15 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "collections_glue.h" - #ifdef MONO_GLUE_ENABLED #include <mono/metadata/exception.h> +#include "core/array.h" + #include "../mono_gd/gd_mono_cache.h" #include "../mono_gd/gd_mono_class.h" +#include "../mono_gd/gd_mono_marshal.h" #include "../mono_gd/gd_mono_utils.h" Array *godot_icall_Array_Ctor() { diff --git a/modules/mono/glue/collections_glue.h b/modules/mono/glue/collections_glue.h deleted file mode 100644 index f8351a1fc7..0000000000 --- a/modules/mono/glue/collections_glue.h +++ /dev/null @@ -1,124 +0,0 @@ -/*************************************************************************/ -/* collections_glue.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 COLLECTIONS_GLUE_H -#define COLLECTIONS_GLUE_H - -#ifdef MONO_GLUE_ENABLED - -#include "core/array.h" - -#include "../mono_gd/gd_mono_marshal.h" - -// Array - -Array *godot_icall_Array_Ctor(); - -void godot_icall_Array_Dtor(Array *ptr); - -MonoObject *godot_icall_Array_At(Array *ptr, int index); - -MonoObject *godot_icall_Array_At_Generic(Array *ptr, int index, uint32_t type_encoding, GDMonoClass *type_class); - -void godot_icall_Array_SetAt(Array *ptr, int index, MonoObject *value); - -int godot_icall_Array_Count(Array *ptr); - -int godot_icall_Array_Add(Array *ptr, MonoObject *item); - -void godot_icall_Array_Clear(Array *ptr); - -MonoBoolean godot_icall_Array_Contains(Array *ptr, MonoObject *item); - -void godot_icall_Array_CopyTo(Array *ptr, MonoArray *array, int array_index); - -Array *godot_icall_Array_Duplicate(Array *ptr, MonoBoolean deep); - -int godot_icall_Array_IndexOf(Array *ptr, MonoObject *item); - -void godot_icall_Array_Insert(Array *ptr, int index, MonoObject *item); - -MonoBoolean godot_icall_Array_Remove(Array *ptr, MonoObject *item); - -void godot_icall_Array_RemoveAt(Array *ptr, int index); - -Error godot_icall_Array_Resize(Array *ptr, int new_size); - -void godot_icall_Array_Generic_GetElementTypeInfo(MonoReflectionType *refltype, uint32_t *type_encoding, GDMonoClass **type_class); - -MonoString *godot_icall_Array_ToString(Array *ptr); - -// Dictionary - -Dictionary *godot_icall_Dictionary_Ctor(); - -void godot_icall_Dictionary_Dtor(Dictionary *ptr); - -MonoObject *godot_icall_Dictionary_GetValue(Dictionary *ptr, MonoObject *key); - -MonoObject *godot_icall_Dictionary_GetValue_Generic(Dictionary *ptr, MonoObject *key, uint32_t type_encoding, GDMonoClass *type_class); - -void godot_icall_Dictionary_SetValue(Dictionary *ptr, MonoObject *key, MonoObject *value); - -Array *godot_icall_Dictionary_Keys(Dictionary *ptr); - -Array *godot_icall_Dictionary_Values(Dictionary *ptr); - -int godot_icall_Dictionary_Count(Dictionary *ptr); - -void godot_icall_Dictionary_Add(Dictionary *ptr, MonoObject *key, MonoObject *value); - -void godot_icall_Dictionary_Clear(Dictionary *ptr); - -MonoBoolean godot_icall_Dictionary_Contains(Dictionary *ptr, MonoObject *key, MonoObject *value); - -MonoBoolean godot_icall_Dictionary_ContainsKey(Dictionary *ptr, MonoObject *key); - -Dictionary *godot_icall_Dictionary_Duplicate(Dictionary *ptr, MonoBoolean deep); - -MonoBoolean godot_icall_Dictionary_RemoveKey(Dictionary *ptr, MonoObject *key); - -MonoBoolean godot_icall_Dictionary_Remove(Dictionary *ptr, MonoObject *key, MonoObject *value); - -MonoBoolean godot_icall_Dictionary_TryGetValue(Dictionary *ptr, MonoObject *key, MonoObject **value); - -MonoBoolean godot_icall_Dictionary_TryGetValue_Generic(Dictionary *ptr, MonoObject *key, MonoObject **value, uint32_t type_encoding, GDMonoClass *type_class); - -void godot_icall_Dictionary_Generic_GetValueTypeInfo(MonoReflectionType *refltype, uint32_t *type_encoding, GDMonoClass **type_class); - -MonoString *godot_icall_Dictionary_ToString(Dictionary *ptr); - -// Register internal calls - -void godot_register_collections_icalls(); - -#endif // MONO_GLUE_ENABLED - -#endif // COLLECTIONS_GLUE_H diff --git a/modules/mono/glue/gd_glue.cpp b/modules/mono/glue/gd_glue.cpp index 2da39a916a..5e892b616b 100644 --- a/modules/mono/glue/gd_glue.cpp +++ b/modules/mono/glue/gd_glue.cpp @@ -28,8 +28,6 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "gd_glue.h" - #ifdef MONO_GLUE_ENABLED #include "core/array.h" @@ -40,6 +38,7 @@ #include "core/variant_parser.h" #include "../mono_gd/gd_mono_cache.h" +#include "../mono_gd/gd_mono_marshal.h" #include "../mono_gd/gd_mono_utils.h" MonoObject *godot_icall_GD_bytes2var(MonoArray *p_bytes, MonoBoolean p_allow_objects) { @@ -91,7 +90,6 @@ void godot_icall_GD_print(MonoArray *p_what) { } void godot_icall_GD_printerr(MonoArray *p_what) { - String str; int length = mono_array_length(p_what); @@ -148,8 +146,9 @@ void godot_icall_GD_prints(MonoArray *p_what) { return; } - if (i) + if (i) { str += " "; + } str += elem_str; } @@ -172,8 +171,9 @@ void godot_icall_GD_printt(MonoArray *p_what) { return; } - if (i) + if (i) { str += "\t"; + } str += elem_str; } @@ -198,7 +198,7 @@ double godot_icall_GD_rand_range(double from, double to) { } uint32_t godot_icall_GD_rand_seed(uint64_t seed, uint64_t *newSeed) { - int ret = Math::rand_from_seed(&seed); + uint32_t ret = Math::rand_from_seed(&seed); *newSeed = seed; return ret; } @@ -214,10 +214,11 @@ MonoString *godot_icall_GD_str(MonoArray *p_what) { for (int i = 0; i < what.size(); i++) { String os = what[i].operator String(); - if (i == 0) + if (i == 0) { str = os; - else + } else { str += os; + } } return GDMonoMarshal::mono_string_from_godot(str); diff --git a/modules/mono/glue/gd_glue.h b/modules/mono/glue/gd_glue.h deleted file mode 100644 index 3ad6058205..0000000000 --- a/modules/mono/glue/gd_glue.h +++ /dev/null @@ -1,86 +0,0 @@ -/*************************************************************************/ -/* gd_glue.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 GD_GLUE_H -#define GD_GLUE_H - -#ifdef MONO_GLUE_ENABLED - -#include "../mono_gd/gd_mono_marshal.h" - -MonoObject *godot_icall_GD_bytes2var(MonoArray *p_bytes, MonoBoolean p_allow_objects); - -MonoObject *godot_icall_GD_convert(MonoObject *p_what, int32_t p_type); - -int godot_icall_GD_hash(MonoObject *p_var); - -MonoObject *godot_icall_GD_instance_from_id(uint64_t p_instance_id); - -void godot_icall_GD_print(MonoArray *p_what); - -void godot_icall_GD_printerr(MonoArray *p_what); - -void godot_icall_GD_printraw(MonoArray *p_what); - -void godot_icall_GD_prints(MonoArray *p_what); - -void godot_icall_GD_printt(MonoArray *p_what); - -float godot_icall_GD_randf(); - -uint32_t godot_icall_GD_randi(); - -void godot_icall_GD_randomize(); - -double godot_icall_GD_rand_range(double from, double to); - -uint32_t godot_icall_GD_rand_seed(uint64_t seed, uint64_t *newSeed); - -void godot_icall_GD_seed(uint64_t p_seed); - -MonoString *godot_icall_GD_str(MonoArray *p_what); - -MonoObject *godot_icall_GD_str2var(MonoString *p_str); - -MonoBoolean godot_icall_GD_type_exists(StringName *p_type); - -MonoArray *godot_icall_GD_var2bytes(MonoObject *p_var, MonoBoolean p_full_objects); - -MonoString *godot_icall_GD_var2str(MonoObject *p_var); - -MonoObject *godot_icall_DefaultGodotTaskScheduler(); - -// Register internal calls - -void godot_register_gd_icalls(); - -#endif // MONO_GLUE_ENABLED - -#endif // GD_GLUE_H diff --git a/modules/mono/glue/glue_header.h b/modules/mono/glue/glue_header.h index ee99a300b9..c1f1936711 100644 --- a/modules/mono/glue/glue_header.h +++ b/modules/mono/glue/glue_header.h @@ -30,13 +30,16 @@ #ifdef MONO_GLUE_ENABLED -#include "base_object_glue.h" -#include "collections_glue.h" -#include "gd_glue.h" -#include "nodepath_glue.h" -#include "rid_glue.h" -#include "string_glue.h" -#include "string_name_glue.h" +#include "../mono_gd/gd_mono_marshal.h" + +void godot_register_collections_icalls(); +void godot_register_gd_icalls(); +void godot_register_string_name_icalls(); +void godot_register_nodepath_icalls(); +void godot_register_object_icalls(); +void godot_register_rid_icalls(); +void godot_register_string_icalls(); +void godot_register_scene_tree_icalls(); /** * Registers internal calls that were not generated. This function is called @@ -50,6 +53,7 @@ void godot_register_glue_header_icalls() { godot_register_object_icalls(); godot_register_rid_icalls(); godot_register_string_icalls(); + godot_register_scene_tree_icalls(); } // Used by the generated glue diff --git a/modules/mono/glue/nodepath_glue.cpp b/modules/mono/glue/nodepath_glue.cpp index e413f404d8..2aa75dd309 100644 --- a/modules/mono/glue/nodepath_glue.cpp +++ b/modules/mono/glue/nodepath_glue.cpp @@ -28,12 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "nodepath_glue.h" - #ifdef MONO_GLUE_ENABLED +#include "core/node_path.h" #include "core/ustring.h" +#include "../mono_gd/gd_mono_marshal.h" + NodePath *godot_icall_NodePath_Ctor(MonoString *p_path) { return memnew(NodePath(GDMonoMarshal::mono_string_to_godot(p_path))); } @@ -51,7 +52,7 @@ MonoBoolean godot_icall_NodePath_is_absolute(NodePath *p_ptr) { return (MonoBoolean)p_ptr->is_absolute(); } -uint32_t godot_icall_NodePath_get_name_count(NodePath *p_ptr) { +int32_t godot_icall_NodePath_get_name_count(NodePath *p_ptr) { return p_ptr->get_name_count(); } @@ -59,7 +60,7 @@ MonoString *godot_icall_NodePath_get_name(NodePath *p_ptr, uint32_t p_idx) { return GDMonoMarshal::mono_string_from_godot(p_ptr->get_name(p_idx)); } -uint32_t godot_icall_NodePath_get_subname_count(NodePath *p_ptr) { +int32_t godot_icall_NodePath_get_subname_count(NodePath *p_ptr) { return p_ptr->get_subname_count(); } diff --git a/modules/mono/glue/nodepath_glue.h b/modules/mono/glue/nodepath_glue.h deleted file mode 100644 index 727679c278..0000000000 --- a/modules/mono/glue/nodepath_glue.h +++ /dev/null @@ -1,68 +0,0 @@ -/*************************************************************************/ -/* nodepath_glue.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 NODEPATH_GLUE_H -#define NODEPATH_GLUE_H - -#ifdef MONO_GLUE_ENABLED - -#include "core/node_path.h" - -#include "../mono_gd/gd_mono_marshal.h" - -NodePath *godot_icall_NodePath_Ctor(MonoString *p_path); - -void godot_icall_NodePath_Dtor(NodePath *p_ptr); - -MonoString *godot_icall_NodePath_operator_String(NodePath *p_np); - -MonoBoolean godot_icall_NodePath_is_absolute(NodePath *p_ptr); - -uint32_t godot_icall_NodePath_get_name_count(NodePath *p_ptr); - -MonoString *godot_icall_NodePath_get_name(NodePath *p_ptr, uint32_t p_idx); - -uint32_t godot_icall_NodePath_get_subname_count(NodePath *p_ptr); - -MonoString *godot_icall_NodePath_get_subname(NodePath *p_ptr, uint32_t p_idx); - -MonoString *godot_icall_NodePath_get_concatenated_subnames(NodePath *p_ptr); - -NodePath *godot_icall_NodePath_get_as_property_path(NodePath *p_ptr); - -MonoBoolean godot_icall_NodePath_is_empty(NodePath *p_ptr); - -// Register internal calls - -void godot_register_nodepath_icalls(); - -#endif // MONO_GLUE_ENABLED - -#endif // NODEPATH_GLUE_H diff --git a/modules/mono/glue/rid_glue.cpp b/modules/mono/glue/rid_glue.cpp index 66a49d8fec..6d2e6b559f 100644 --- a/modules/mono/glue/rid_glue.cpp +++ b/modules/mono/glue/rid_glue.cpp @@ -28,17 +28,20 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "rid_glue.h" - #ifdef MONO_GLUE_ENABLED +#include "core/object.h" #include "core/resource.h" +#include "core/rid.h" + +#include "../mono_gd/gd_mono_marshal.h" RID *godot_icall_RID_Ctor(Object *p_from) { Resource *res_from = Object::cast_to<Resource>(p_from); - if (res_from) + if (res_from) { return memnew(RID(res_from->get_rid())); + } return memnew(RID); } diff --git a/modules/mono/glue/rid_glue.h b/modules/mono/glue/rid_glue.h deleted file mode 100644 index 506d715451..0000000000 --- a/modules/mono/glue/rid_glue.h +++ /dev/null @@ -1,53 +0,0 @@ -/*************************************************************************/ -/* rid_glue.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 RID_GLUE_H -#define RID_GLUE_H - -#ifdef MONO_GLUE_ENABLED - -#include "core/object.h" -#include "core/rid.h" - -#include "../mono_gd/gd_mono_marshal.h" - -RID *godot_icall_RID_Ctor(Object *p_from); - -void godot_icall_RID_Dtor(RID *p_ptr); - -uint32_t godot_icall_RID_get_id(RID *p_ptr); - -// Register internal calls - -void godot_register_rid_icalls(); - -#endif // MONO_GLUE_ENABLED - -#endif // RID_GLUE_H diff --git a/modules/mono/glue/string_name_glue.h b/modules/mono/glue/scene_tree_glue.cpp index 88354ddd84..b43daccc1b 100644 --- a/modules/mono/glue/string_name_glue.h +++ b/modules/mono/glue/scene_tree_glue.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* string_name_glue.h */ +/* scene_tree_glue.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,27 +28,59 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef STRING_NAME_GLUE_H -#define STRING_NAME_GLUE_H - #ifdef MONO_GLUE_ENABLED +#include "core/array.h" +#include "core/class_db.h" #include "core/string_name.h" +#include "scene/main/node.h" +#include "scene/main/scene_tree.h" +#include "../csharp_script.h" #include "../mono_gd/gd_mono_marshal.h" +#include "../mono_gd/gd_mono_utils.h" -StringName *godot_icall_StringName_Ctor(MonoString *p_path); +Array *godot_icall_SceneTree_get_nodes_in_group_Generic(SceneTree *ptr, StringName *group, MonoReflectionType *refltype) { + List<Node *> nodes; + Array ret; -void godot_icall_StringName_Dtor(StringName *p_ptr); + // Retrieve all the nodes in the group + ptr->get_nodes_in_group(*group, &nodes); -MonoString *godot_icall_StringName_operator_String(StringName *p_np); + // No need to bother if the group is empty + if (!nodes.empty()) { + MonoType *elem_type = mono_reflection_type_get_type(refltype); + MonoClass *mono_class = mono_class_from_mono_type(elem_type); + GDMonoClass *klass = GDMono::get_singleton()->get_class(mono_class); -MonoBoolean godot_icall_StringName_is_empty(StringName *p_ptr); + if (klass == GDMonoUtils::get_class_native_base(klass)) { + // If we're trying to get native objects, just check the inheritance list + StringName native_class_name = GDMonoUtils::get_native_godot_class_name(klass); + for (int i = 0; i < nodes.size(); ++i) { + if (ClassDB::is_parent_class(nodes[i]->get_class(), native_class_name)) { + ret.push_back(nodes[i]); + } + } + } else { + // If we're trying to get csharpscript instances, get the mono object and compare the classes + for (int i = 0; i < nodes.size(); ++i) { + CSharpInstance *si = CAST_CSHARP_INSTANCE(nodes[i]->get_script_instance()); -// Register internal calls + if (si != nullptr) { + MonoObject *obj = si->get_mono_object(); + if (obj != nullptr && mono_object_get_class(obj) == mono_class) { + ret.push_back(nodes[i]); + } + } + } + } + } -void godot_register_string_name_icalls(); + return memnew(Array(ret)); +} -#endif // MONO_GLUE_ENABLED +void godot_register_scene_tree_icalls() { + mono_add_internal_call("Godot.SceneTree::godot_icall_SceneTree_get_nodes_in_group_Generic", (void *)godot_icall_SceneTree_get_nodes_in_group_Generic); +} -#endif // STRING_NAME_GLUE_H +#endif // MONO_GLUE_ENABLED diff --git a/modules/mono/glue/string_glue.cpp b/modules/mono/glue/string_glue.cpp index e407a70db9..595b8d71f1 100644 --- a/modules/mono/glue/string_glue.cpp +++ b/modules/mono/glue/string_glue.cpp @@ -28,14 +28,14 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "string_glue.h" - #ifdef MONO_GLUE_ENABLED #include "core/ustring.h" #include "core/variant.h" #include "core/vector.h" +#include "../mono_gd/gd_mono_marshal.h" + MonoArray *godot_icall_String_md5_buffer(MonoString *p_str) { Vector<uint8_t> ret = GDMonoMarshal::mono_string_to_godot(p_str).md5_buffer(); // TODO Check possible Array/Vector<uint8_t> problem? diff --git a/modules/mono/glue/string_glue.h b/modules/mono/glue/string_glue.h deleted file mode 100644 index a5e833ba61..0000000000 --- a/modules/mono/glue/string_glue.h +++ /dev/null @@ -1,56 +0,0 @@ -/*************************************************************************/ -/* string_glue.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 STRING_GLUE_H -#define STRING_GLUE_H - -#ifdef MONO_GLUE_ENABLED - -#include "../mono_gd/gd_mono_marshal.h" - -MonoArray *godot_icall_String_md5_buffer(MonoString *p_str); - -MonoString *godot_icall_String_md5_text(MonoString *p_str); - -int godot_icall_String_rfind(MonoString *p_str, MonoString *p_what, int p_from); - -int godot_icall_String_rfindn(MonoString *p_str, MonoString *p_what, int p_from); - -MonoArray *godot_icall_String_sha256_buffer(MonoString *p_str); - -MonoString *godot_icall_String_sha256_text(MonoString *p_str); - -// Register internal calls - -void godot_register_string_icalls(); - -#endif // MONO_GLUE_ENABLED - -#endif // STRING_GLUE_H diff --git a/modules/mono/glue/string_name_glue.cpp b/modules/mono/glue/string_name_glue.cpp index 81006e5849..4b2e88569b 100644 --- a/modules/mono/glue/string_name_glue.cpp +++ b/modules/mono/glue/string_name_glue.cpp @@ -28,12 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "string_name_glue.h" - #ifdef MONO_GLUE_ENABLED +#include "core/string_name.h" #include "core/ustring.h" +#include "../mono_gd/gd_mono_marshal.h" + StringName *godot_icall_StringName_Ctor(MonoString *p_path) { return memnew(StringName(GDMonoMarshal::mono_string_to_godot(p_path))); } @@ -48,7 +49,7 @@ MonoString *godot_icall_StringName_operator_String(StringName *p_np) { } MonoBoolean godot_icall_StringName_is_empty(StringName *p_ptr) { - return (MonoBoolean)(p_ptr == StringName()); + return (MonoBoolean)(*p_ptr == StringName()); } void godot_register_string_name_icalls() { diff --git a/modules/mono/godotsharp_dirs.cpp b/modules/mono/godotsharp_dirs.cpp index d596163926..692da991c7 100644 --- a/modules/mono/godotsharp_dirs.cpp +++ b/modules/mono/godotsharp_dirs.cpp @@ -86,7 +86,6 @@ String _get_mono_user_dir() { } class _GodotSharpDirs { - public: String res_data_dir; String res_metadata_dir; diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index dfd78a8244..dbe9c7fc5d 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -48,8 +48,9 @@ bool ManagedCallable::compare_equal(const CallableCustom *p_a, const CallableCus MonoDelegate *delegate_b = (MonoDelegate *)b->delegate_handle.get_target(); if (!delegate_a || !delegate_b) { - if (!delegate_a && !delegate_b) + if (!delegate_a && !delegate_b) { return true; + } return false; } @@ -58,8 +59,9 @@ bool ManagedCallable::compare_equal(const CallableCustom *p_a, const CallableCus } bool ManagedCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) { - if (compare_equal(p_a, p_b)) + if (compare_equal(p_a, p_b)) { return false; + } return p_a < p_b; } @@ -82,6 +84,7 @@ CallableCustom::CompareLessFunc ManagedCallable::get_compare_less_func() const { } ObjectID ManagedCallable::get_object() const { + // TODO: If the delegate target extends Godot.Object, use that instead! return CSharpLanguage::get_singleton()->get_managed_callable_middleman()->get_instance_id(); } diff --git a/modules/mono/mono_gc_handle.cpp b/modules/mono/mono_gc_handle.cpp index e362d5affb..16a6875406 100644 --- a/modules/mono/mono_gc_handle.cpp +++ b/modules/mono/mono_gc_handle.cpp @@ -56,11 +56,9 @@ MonoGCHandleData MonoGCHandleData::new_weak_handle(MonoObject *p_object) { } Ref<MonoGCHandleRef> MonoGCHandleRef::create_strong(MonoObject *p_object) { - return memnew(MonoGCHandleRef(MonoGCHandleData::new_strong_handle(p_object))); } Ref<MonoGCHandleRef> MonoGCHandleRef::create_weak(MonoObject *p_object) { - return memnew(MonoGCHandleRef(MonoGCHandleData::new_weak_handle(p_object))); } diff --git a/modules/mono/mono_gc_handle.h b/modules/mono/mono_gc_handle.h index fbcb405b0d..13cfad4654 100644 --- a/modules/mono/mono_gc_handle.h +++ b/modules/mono/mono_gc_handle.h @@ -47,8 +47,8 @@ enum class GCHandleType : char { // Manual release of the GC handle must be done when using this struct struct MonoGCHandleData { - uint32_t handle; - gdmono::GCHandleType type; + uint32_t handle = 0; + gdmono::GCHandleType type = gdmono::GCHandleType::NIL; _FORCE_INLINE_ bool is_released() const { return !handle; } _FORCE_INLINE_ bool is_weak() const { return type == gdmono::GCHandleType::WEAK_HANDLE; } @@ -68,10 +68,7 @@ struct MonoGCHandleData { MonoGCHandleData(const MonoGCHandleData &) = default; - MonoGCHandleData() : - handle(0), - type(gdmono::GCHandleType::NIL) { - } + MonoGCHandleData() {} MonoGCHandleData(uint32_t p_handle, gdmono::GCHandleType p_type) : handle(p_handle), @@ -84,7 +81,6 @@ struct MonoGCHandleData { }; class MonoGCHandleRef : public Reference { - GDCLASS(MonoGCHandleRef, Reference); MonoGCHandleData data; diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 3298c5da4c..cf5ac33d20 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -130,9 +130,12 @@ void gd_mono_profiler_init() { } void gd_mono_debug_init() { - CharString da_args = OS::get_singleton()->get_environment("GODOT_MONO_DEBUGGER_AGENT").utf8(); + if (da_args.length()) { + OS::get_singleton()->set_environment("GODOT_MONO_DEBUGGER_AGENT", String()); + } + #ifdef TOOLS_ENABLED int da_port = GLOBAL_DEF("mono/debugger_agent/port", 23685); bool da_suspend = GLOBAL_DEF("mono/debugger_agent/wait_for_debugger", false); @@ -141,8 +144,9 @@ void gd_mono_debug_init() { if (Engine::get_singleton()->is_editor_hint() || ProjectSettings::get_singleton()->get_resource_path().empty() || Main::is_project_manager()) { - if (da_args.size() == 0) + if (da_args.size() == 0) { return; + } } if (da_args.length() == 0) { @@ -247,7 +251,6 @@ void GDMono::add_mono_shared_libs_dir_to_path() { } void GDMono::determine_mono_dirs(String &r_assembly_rootdir, String &r_config_dir) { - String bundled_assembly_rootdir = GodotSharpDirs::get_data_mono_lib_dir(); String bundled_config_dir = GodotSharpDirs::get_data_mono_etc_dir(); @@ -310,7 +313,6 @@ void GDMono::determine_mono_dirs(String &r_assembly_rootdir, String &r_config_di } void GDMono::initialize() { - ERR_FAIL_NULL(Engine::get_singleton()); print_verbose("Mono: Initializing module..."); @@ -409,7 +411,6 @@ void GDMono::initialize() { } void GDMono::initialize_load_assemblies() { - #ifndef MONO_GLUE_ENABLED CRASH_NOW_MSG("Mono: This binary was built with 'mono_glue=no'; cannot load assemblies."); #endif @@ -422,22 +423,28 @@ void GDMono::initialize_load_assemblies() { #if defined(TOOLS_ENABLED) bool tool_assemblies_loaded = _load_tools_assemblies(); CRASH_COND_MSG(!tool_assemblies_loaded, "Mono: Failed to load '" TOOLS_ASM_NAME "' assemblies."); + + if (Main::is_project_manager()) { + return; + } #endif // Load the project's main assembly. This doesn't necessarily need to succeed. // The game may not be using .NET at all, or if the project does use .NET and // we're running in the editor, it may just happen to be it wasn't built yet. if (!_load_project_assembly()) { - if (OS::get_singleton()->is_stdout_verbose()) + if (OS::get_singleton()->is_stdout_verbose()) { print_error("Mono: Failed to load project assembly"); + } } } bool GDMono::_are_api_assemblies_out_of_sync() { bool out_of_sync = core_api_assembly.assembly && (core_api_assembly.out_of_sync || !GDMonoCache::cached_data.godot_api_cache_updated); #ifdef TOOLS_ENABLED - if (!out_of_sync) + if (!out_of_sync) { out_of_sync = editor_api_assembly.assembly && editor_api_assembly.out_of_sync; + } #endif return out_of_sync; } @@ -467,6 +474,7 @@ uint64_t get_editor_api_hash() { uint32_t get_bindings_version() { GD_UNREACHABLE(); } + uint32_t get_cs_glue_version() { GD_UNREACHABLE(); } @@ -508,25 +516,25 @@ void GDMono::_init_exception_policy() { } } -void GDMono::add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly) { - +void GDMono::add_assembly(int32_t p_domain_id, GDMonoAssembly *p_assembly) { assemblies[p_domain_id][p_assembly->get_name()] = p_assembly; } GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) { - - if (p_name == "mscorlib") - return get_corlib_assembly(); + if (p_name == "mscorlib" && corlib_assembly) { + return corlib_assembly; + } MonoDomain *domain = mono_domain_get(); - uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0; + int32_t domain_id = domain ? mono_domain_get_id(domain) : 0; GDMonoAssembly **result = assemblies[domain_id].getptr(p_name); return result ? *result : nullptr; } bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly) { - +#ifdef DEBUG_ENABLED CRASH_COND(!r_assembly); +#endif MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8()); bool result = load_assembly(p_name, aname, r_assembly, p_refonly); @@ -537,27 +545,27 @@ bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bo } bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly) { +#ifdef DEBUG_ENABLED + CRASH_COND(!r_assembly); +#endif + + return load_assembly(p_name, p_aname, r_assembly, p_refonly, GDMonoAssembly::get_default_search_dirs()); +} +bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs) { +#ifdef DEBUG_ENABLED CRASH_COND(!r_assembly); +#endif print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "..."); - MonoImageOpenStatus status = MONO_IMAGE_OK; - MonoAssembly *assembly = mono_assembly_load_full(p_aname, nullptr, &status, p_refonly); + GDMonoAssembly *assembly = GDMonoAssembly::load(p_name, p_aname, p_refonly, p_search_dirs); - if (!assembly) + if (!assembly) { return false; + } - ERR_FAIL_COND_V(status != MONO_IMAGE_OK, false); - - uint32_t domain_id = mono_domain_get_id(mono_domain_get()); - - GDMonoAssembly **stored_assembly = assemblies[domain_id].getptr(p_name); - - ERR_FAIL_COND_V(stored_assembly == nullptr, false); - ERR_FAIL_COND_V((*stored_assembly)->get_assembly() != assembly, false); - - *r_assembly = *stored_assembly; + *r_assembly = assembly; print_verbose("Mono: Assembly " + p_name + (p_refonly ? " (refonly)" : "") + " loaded from path: " + (*r_assembly)->get_path()); @@ -565,15 +573,15 @@ bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMo } bool GDMono::load_assembly_from(const String &p_name, const String &p_path, GDMonoAssembly **r_assembly, bool p_refonly) { - CRASH_COND(!r_assembly); print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "..."); GDMonoAssembly *assembly = GDMonoAssembly::load_from(p_name, p_path, p_refonly); - if (!assembly) + if (!assembly) { return false; + } *r_assembly = assembly; @@ -593,16 +601,19 @@ ApiAssemblyInfo::Version ApiAssemblyInfo::Version::get_from_loaded_assembly(GDMo if (nativecalls_klass) { GDMonoField *api_hash_field = nativecalls_klass->get_field("godot_api_hash"); - if (api_hash_field) + if (api_hash_field) { api_assembly_version.godot_api_hash = GDMonoMarshal::unbox<uint64_t>(api_hash_field->get_value(nullptr)); + } GDMonoField *binds_ver_field = nativecalls_klass->get_field("bindings_version"); - if (binds_ver_field) + if (binds_ver_field) { api_assembly_version.bindings_version = GDMonoMarshal::unbox<uint32_t>(binds_ver_field->get_value(nullptr)); + } GDMonoField *cs_glue_ver_field = nativecalls_klass->get_field("cs_glue_version"); - if (cs_glue_ver_field) + if (cs_glue_ver_field) { api_assembly_version.cs_glue_version = GDMonoMarshal::unbox<uint32_t>(cs_glue_ver_field->get_value(nullptr)); + } } return api_assembly_version; @@ -613,21 +624,21 @@ String ApiAssemblyInfo::to_string(ApiAssemblyInfo::Type p_type) { } bool GDMono::_load_corlib_assembly() { - - if (corlib_assembly) + if (corlib_assembly) { return true; + } bool success = load_assembly("mscorlib", &corlib_assembly); - if (success) + if (success) { GDMonoCache::update_corlib_cache(); + } return success; } #ifdef TOOLS_ENABLED bool GDMono::copy_prebuilt_api_assembly(ApiAssemblyInfo::Type p_api_type, const String &p_config) { - String src_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file(p_config); String dst_dir = GodotSharpDirs::get_res_assemblies_base_dir().plus_file(p_config); @@ -648,12 +659,14 @@ bool GDMono::copy_prebuilt_api_assembly(ApiAssemblyInfo::Type p_api_type, const DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); String xml_file = assembly_name + ".xml"; - if (da->copy(src_dir.plus_file(xml_file), dst_dir.plus_file(xml_file)) != OK) + if (da->copy(src_dir.plus_file(xml_file), dst_dir.plus_file(xml_file)) != OK) { WARN_PRINT("Failed to copy '" + xml_file + "'."); + } String pdb_file = assembly_name + ".pdb"; - if (da->copy(src_dir.plus_file(pdb_file), dst_dir.plus_file(pdb_file)) != OK) + if (da->copy(src_dir.plus_file(pdb_file), dst_dir.plus_file(pdb_file)) != OK) { WARN_PRINT("Failed to copy '" + pdb_file + "'."); + } String assembly_file = assembly_name + ".dll"; if (da->copy(src_dir.plus_file(assembly_file), dst_dir.plus_file(assembly_file)) != OK) { @@ -668,13 +681,15 @@ static bool try_get_cached_api_hash_for(const String &p_api_assemblies_dir, bool String core_api_assembly_path = p_api_assemblies_dir.plus_file(CORE_API_ASSEMBLY_NAME ".dll"); String editor_api_assembly_path = p_api_assemblies_dir.plus_file(EDITOR_API_ASSEMBLY_NAME ".dll"); - if (!FileAccess::exists(core_api_assembly_path) || !FileAccess::exists(editor_api_assembly_path)) + if (!FileAccess::exists(core_api_assembly_path) || !FileAccess::exists(editor_api_assembly_path)) { return false; + } String cached_api_hash_path = p_api_assemblies_dir.plus_file("api_hash_cache.cfg"); - if (!FileAccess::exists(cached_api_hash_path)) + if (!FileAccess::exists(cached_api_hash_path)) { return false; + } Ref<ConfigFile> cfg; cfg.instance(); @@ -698,7 +713,6 @@ static bool try_get_cached_api_hash_for(const String &p_api_assemblies_dir, bool } static void create_cached_api_hash_for(const String &p_api_assemblies_dir) { - String core_api_assembly_path = p_api_assemblies_dir.plus_file(CORE_API_ASSEMBLY_NAME ".dll"); String editor_api_assembly_path = p_api_assemblies_dir.plus_file(EDITOR_API_ASSEMBLY_NAME ".dll"); String cached_api_hash_path = p_api_assemblies_dir.plus_file("api_hash_cache.cfg"); @@ -741,7 +755,6 @@ bool GDMono::_temp_domain_load_are_assemblies_out_of_sync(const String &p_config } String GDMono::update_api_assemblies_from_prebuilt(const String &p_config, const bool *p_core_api_out_of_sync, const bool *p_editor_api_out_of_sync) { - #define FAIL_REASON(m_out_of_sync, m_prebuilt_exists) \ ( \ (m_out_of_sync ? \ @@ -769,8 +782,9 @@ String GDMono::update_api_assemblies_from_prebuilt(const String &p_config, const // Note: Even if only one of the assemblies if missing or out of sync, we update both - if (!api_assemblies_out_of_sync && FileAccess::exists(core_assembly_path) && FileAccess::exists(editor_assembly_path)) + if (!api_assemblies_out_of_sync && FileAccess::exists(core_assembly_path) && FileAccess::exists(editor_assembly_path)) { return String(); // No update needed + } print_verbose("Updating '" + p_config + "' API assemblies"); @@ -798,9 +812,9 @@ String GDMono::update_api_assemblies_from_prebuilt(const String &p_config, const #endif bool GDMono::_load_core_api_assembly(LoadedApiAssembly &r_loaded_api_assembly, const String &p_config, bool p_refonly) { - - if (r_loaded_api_assembly.assembly) + if (r_loaded_api_assembly.assembly) { return true; + } #ifdef TOOLS_ENABLED // For the editor and the editor player we want to load it from a specific path to make sure we can keep it up to date @@ -832,9 +846,9 @@ bool GDMono::_load_core_api_assembly(LoadedApiAssembly &r_loaded_api_assembly, c #ifdef TOOLS_ENABLED bool GDMono::_load_editor_api_assembly(LoadedApiAssembly &r_loaded_api_assembly, const String &p_config, bool p_refonly) { - - if (r_loaded_api_assembly.assembly) + if (r_loaded_api_assembly.assembly) { return true; + } // For the editor and the editor player we want to load it from a specific path to make sure we can keep it up to date @@ -864,30 +878,35 @@ bool GDMono::_load_editor_api_assembly(LoadedApiAssembly &r_loaded_api_assembly, bool GDMono::_try_load_api_assemblies(LoadedApiAssembly &r_core_api_assembly, LoadedApiAssembly &r_editor_api_assembly, const String &p_config, bool p_refonly, CoreApiAssemblyLoadedCallback p_callback) { if (!_load_core_api_assembly(r_core_api_assembly, p_config, p_refonly)) { - if (OS::get_singleton()->is_stdout_verbose()) + if (OS::get_singleton()->is_stdout_verbose()) { print_error("Mono: Failed to load Core API assembly"); + } return false; } #ifdef TOOLS_ENABLED if (!_load_editor_api_assembly(r_editor_api_assembly, p_config, p_refonly)) { - if (OS::get_singleton()->is_stdout_verbose()) + if (OS::get_singleton()->is_stdout_verbose()) { print_error("Mono: Failed to load Editor API assembly"); + } return false; } - if (r_editor_api_assembly.out_of_sync) + if (r_editor_api_assembly.out_of_sync) { return false; + } #endif // Check if the core API assembly is out of sync only after trying to load the // editor API assembly. Otherwise, if both assemblies are out of sync, we would // only update the former as we won't know the latter also needs to be updated. - if (r_core_api_assembly.out_of_sync) + if (r_core_api_assembly.out_of_sync) { return false; + } - if (p_callback) + if (p_callback) { return p_callback(); + } return true; } @@ -895,8 +914,9 @@ bool GDMono::_try_load_api_assemblies(LoadedApiAssembly &r_core_api_assembly, Lo bool GDMono::_on_core_api_assembly_loaded() { GDMonoCache::update_godot_api_cache(); - if (!GDMonoCache::cached_data.godot_api_cache_updated) + if (!GDMonoCache::cached_data.godot_api_cache_updated) { return false; + } get_singleton()->_install_trace_listener(); @@ -909,7 +929,6 @@ bool GDMono::_try_load_api_assemblies_preset() { } void GDMono::_load_api_assemblies() { - bool api_assemblies_loaded = _try_load_api_assemblies_preset(); #if defined(TOOLS_ENABLED) && !defined(GD_MONO_SINGLE_APPDOMAIN) @@ -962,9 +981,9 @@ void GDMono::_load_api_assemblies() { #ifdef TOOLS_ENABLED bool GDMono::_load_tools_assemblies() { - - if (tools_assembly && tools_project_editor_assembly) + if (tools_assembly && tools_project_editor_assembly) { return true; + } bool success = load_assembly(TOOLS_ASM_NAME, &tools_assembly) && load_assembly(TOOLS_PROJECT_EDITOR_ASM_NAME, &tools_project_editor_assembly); @@ -974,9 +993,9 @@ bool GDMono::_load_tools_assemblies() { #endif bool GDMono::_load_project_assembly() { - - if (project_assembly) + if (project_assembly) { return true; + } String appname = ProjectSettings::get_singleton()->get("application/config/name"); String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); @@ -994,7 +1013,6 @@ bool GDMono::_load_project_assembly() { } void GDMono::_install_trace_listener() { - #ifdef DEBUG_ENABLED // Install the trace listener now before the project assembly is loaded GDMonoClass *debug_utils = get_core_api_assembly()->get_class(BINDINGS_NAMESPACE, "DebuggingUtils"); @@ -1011,7 +1029,6 @@ void GDMono::_install_trace_listener() { #ifndef GD_MONO_SINGLE_APPDOMAIN Error GDMono::_load_scripts_domain() { - ERR_FAIL_COND_V(scripts_domain != nullptr, ERR_BUG); print_verbose("Mono: Loading scripts domain..."); @@ -1026,13 +1043,13 @@ Error GDMono::_load_scripts_domain() { } Error GDMono::_unload_scripts_domain() { - ERR_FAIL_NULL_V(scripts_domain, ERR_BUG); print_verbose("Mono: Finalizing scripts domain..."); - if (mono_domain_get() != root_domain) + if (mono_domain_get() != root_domain) { mono_domain_set(root_domain, true); + } finalizing_scripts_domain = true; @@ -1079,7 +1096,6 @@ Error GDMono::_unload_scripts_domain() { #ifdef GD_MONO_HOT_RELOAD Error GDMono::reload_scripts_domain() { - ERR_FAIL_COND_V(!runtime_initialized, ERR_BUG); if (scripts_domain) { @@ -1116,7 +1132,6 @@ Error GDMono::reload_scripts_domain() { #ifndef GD_MONO_SINGLE_APPDOMAIN Error GDMono::finalize_and_unload_domain(MonoDomain *p_domain) { - CRASH_COND(p_domain == nullptr); CRASH_COND(p_domain == GDMono::get_singleton()->get_scripts_domain()); // Should use _unload_scripts_domain() instead @@ -1124,8 +1139,9 @@ Error GDMono::finalize_and_unload_domain(MonoDomain *p_domain) { print_verbose("Mono: Unloading domain '" + domain_name + "'..."); - if (mono_domain_get() == p_domain) + if (mono_domain_get() == p_domain) { mono_domain_set(root_domain, true); + } if (!mono_domain_finalize(p_domain, 2000)) { ERR_PRINT("Mono: Domain finalization timeout."); @@ -1149,13 +1165,13 @@ Error GDMono::finalize_and_unload_domain(MonoDomain *p_domain) { #endif GDMonoClass *GDMono::get_class(MonoClass *p_raw_class) { - MonoImage *image = mono_class_get_image(p_raw_class); - if (image == corlib_assembly->get_image()) + if (image == corlib_assembly->get_image()) { return corlib_assembly->get_class(p_raw_class); + } - uint32_t domain_id = mono_domain_get_id(mono_domain_get()); + int32_t domain_id = mono_domain_get_id(mono_domain_get()); HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies[domain_id]; const String *k = nullptr; @@ -1163,9 +1179,9 @@ GDMonoClass *GDMono::get_class(MonoClass *p_raw_class) { GDMonoAssembly *assembly = domain_assemblies.get(*k); if (assembly->get_image() == image) { GDMonoClass *klass = assembly->get_class(p_raw_class); - - if (klass) + if (klass) { return klass; + } } } @@ -1173,27 +1189,27 @@ GDMonoClass *GDMono::get_class(MonoClass *p_raw_class) { } GDMonoClass *GDMono::get_class(const StringName &p_namespace, const StringName &p_name) { - GDMonoClass *klass = corlib_assembly->get_class(p_namespace, p_name); - if (klass) + if (klass) { return klass; + } - uint32_t domain_id = mono_domain_get_id(mono_domain_get()); + int32_t domain_id = mono_domain_get_id(mono_domain_get()); HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies[domain_id]; const String *k = nullptr; while ((k = domain_assemblies.next(k))) { GDMonoAssembly *assembly = domain_assemblies.get(*k); klass = assembly->get_class(p_namespace, p_name); - if (klass) + if (klass) { return klass; + } } return nullptr; } -void GDMono::_domain_assemblies_cleanup(uint32_t p_domain_id) { - +void GDMono::_domain_assemblies_cleanup(int32_t p_domain_id) { HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies[p_domain_id]; const String *k = nullptr; @@ -1205,15 +1221,15 @@ void GDMono::_domain_assemblies_cleanup(uint32_t p_domain_id) { } void GDMono::unhandled_exception_hook(MonoObject *p_exc, void *) { - // This method will be called by the runtime when a thrown exception is not handled. // It won't be called when we manually treat a thrown exception as unhandled. // We assume the exception was already printed before calling this hook. #ifdef DEBUG_ENABLED GDMonoUtils::debug_send_unhandled_exception_error((MonoException *)p_exc); - if (EngineDebugger::is_active()) + if (EngineDebugger::is_active()) { EngineDebugger::get_singleton()->poll_events(false); + } #endif exit(mono_environment_exitcode_get()); @@ -1222,7 +1238,6 @@ void GDMono::unhandled_exception_hook(MonoObject *p_exc, void *) { } GDMono::GDMono() { - singleton = this; gdmono_log = memnew(GDMonoLog); @@ -1249,7 +1264,6 @@ GDMono::GDMono() { } GDMono::~GDMono() { - if (is_runtime_initialized()) { #ifndef GD_MONO_SINGLE_APPDOMAIN if (scripts_domain) { @@ -1290,7 +1304,7 @@ GDMono::~GDMono() { // Leave the rest to 'mono_jit_cleanup' #endif - const uint32_t *k = nullptr; + const int32_t *k = nullptr; while ((k = assemblies.next(k))) { HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies.get(*k); @@ -1314,8 +1328,9 @@ GDMono::~GDMono() { gdmono::android::support::cleanup(); #endif - if (gdmono_log) + if (gdmono_log) { memdelete(gdmono_log); + } singleton = nullptr; } @@ -1323,79 +1338,76 @@ GDMono::~GDMono() { _GodotSharp *_GodotSharp::singleton = nullptr; void _GodotSharp::attach_thread() { - GDMonoUtils::attach_current_thread(); } void _GodotSharp::detach_thread() { - GDMonoUtils::detach_current_thread(); } int32_t _GodotSharp::get_domain_id() { - MonoDomain *domain = mono_domain_get(); - CRASH_COND(!domain); // User must check if runtime is initialized before calling this method + ERR_FAIL_NULL_V(domain, -1); return mono_domain_get_id(domain); } int32_t _GodotSharp::get_scripts_domain_id() { - + ERR_FAIL_NULL_V_MSG(GDMono::get_singleton(), + -1, "The Mono runtime is not initialized"); MonoDomain *domain = GDMono::get_singleton()->get_scripts_domain(); - CRASH_COND(!domain); // User must check if scripts domain is loaded before calling this method + ERR_FAIL_NULL_V(domain, -1); return mono_domain_get_id(domain); } bool _GodotSharp::is_scripts_domain_loaded() { - - return GDMono::get_singleton()->is_runtime_initialized() && GDMono::get_singleton()->get_scripts_domain() != nullptr; + return GDMono::get_singleton() != nullptr && + GDMono::get_singleton()->is_runtime_initialized() && + GDMono::get_singleton()->get_scripts_domain() != nullptr; } bool _GodotSharp::_is_domain_finalizing_for_unload(int32_t p_domain_id) { - return is_domain_finalizing_for_unload(p_domain_id); } -bool _GodotSharp::is_domain_finalizing_for_unload() { - - return is_domain_finalizing_for_unload(mono_domain_get()); -} - bool _GodotSharp::is_domain_finalizing_for_unload(int32_t p_domain_id) { - return is_domain_finalizing_for_unload(mono_domain_get_by_id(p_domain_id)); } bool _GodotSharp::is_domain_finalizing_for_unload(MonoDomain *p_domain) { + GDMono *gd_mono = GDMono::get_singleton(); - if (!p_domain) - return true; - if (p_domain == GDMono::get_singleton()->get_scripts_domain() && GDMono::get_singleton()->is_finalizing_scripts_domain()) + ERR_FAIL_COND_V_MSG(!gd_mono || !gd_mono->is_runtime_initialized(), + false, "The Mono runtime is not initialized"); + + ERR_FAIL_NULL_V(p_domain, true); + + if (p_domain == gd_mono->get_scripts_domain() && gd_mono->is_finalizing_scripts_domain()) { return true; + } + return mono_domain_is_unloading(p_domain); } bool _GodotSharp::is_runtime_shutting_down() { - return mono_runtime_is_shutting_down(); } bool _GodotSharp::is_runtime_initialized() { - - return GDMono::get_singleton()->is_runtime_initialized(); + return GDMono::get_singleton() != nullptr && GDMono::get_singleton()->is_runtime_initialized(); } void _GodotSharp::_reload_assemblies(bool p_soft_reload) { #ifdef GD_MONO_HOT_RELOAD + CRASH_COND(CSharpLanguage::get_singleton() == nullptr); // This method may be called more than once with `call_deferred`, so we need to check // again if reloading is needed to avoid reloading multiple times unnecessarily. - if (CSharpLanguage::get_singleton()->is_assembly_reloading_needed()) + if (CSharpLanguage::get_singleton()->is_assembly_reloading_needed()) { CSharpLanguage::get_singleton()->reload_assemblies(p_soft_reload); + } #endif } void _GodotSharp::_bind_methods() { - ClassDB::bind_method(D_METHOD("attach_thread"), &_GodotSharp::attach_thread); ClassDB::bind_method(D_METHOD("detach_thread"), &_GodotSharp::detach_thread); @@ -1410,11 +1422,9 @@ void _GodotSharp::_bind_methods() { } _GodotSharp::_GodotSharp() { - singleton = this; } _GodotSharp::~_GodotSharp() { - singleton = nullptr; } diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 4898833e8e..18f7418049 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -48,9 +48,9 @@ enum Type { }; struct Version { - uint64_t godot_api_hash; - uint32_t bindings_version; - uint32_t cs_glue_version; + uint64_t godot_api_hash = 0; + uint32_t bindings_version = 0; + uint32_t cs_glue_version = 0; bool operator==(const Version &p_other) const { return godot_api_hash == p_other.godot_api_hash && @@ -58,11 +58,7 @@ struct Version { cs_glue_version == p_other.cs_glue_version; } - Version() : - godot_api_hash(0), - bindings_version(0), - cs_glue_version(0) { - } + Version() {} Version(uint64_t p_godot_api_hash, uint32_t p_bindings_version, @@ -79,7 +75,6 @@ String to_string(Type p_type); } // namespace ApiAssemblyInfo class GDMono { - public: enum UnhandledExceptionPolicy { POLICY_TERMINATE_APP, @@ -87,13 +82,10 @@ public: }; struct LoadedApiAssembly { - GDMonoAssembly *assembly; - bool out_of_sync; + GDMonoAssembly *assembly = nullptr; + bool out_of_sync = false; - LoadedApiAssembly() : - assembly(nullptr), - out_of_sync(false) { - } + LoadedApiAssembly() {} }; private: @@ -105,7 +97,7 @@ private: MonoDomain *root_domain; MonoDomain *scripts_domain; - HashMap<uint32_t, HashMap<String, GDMonoAssembly *>> assemblies; + HashMap<int32_t, HashMap<String, GDMonoAssembly *>> assemblies; GDMonoAssembly *corlib_assembly; GDMonoAssembly *project_assembly; @@ -149,7 +141,7 @@ private: Error _unload_scripts_domain(); #endif - void _domain_assemblies_cleanup(uint32_t p_domain_id); + void _domain_assemblies_cleanup(int32_t p_domain_id); uint64_t api_core_hash; #ifdef TOOLS_ENABLED @@ -173,14 +165,16 @@ protected: public: #ifdef DEBUG_METHODS_ENABLED uint64_t get_api_core_hash() { - if (api_core_hash == 0) + if (api_core_hash == 0) { api_core_hash = ClassDB::get_api_hash(ClassDB::API_CORE); + } return api_core_hash; } #ifdef TOOLS_ENABLED uint64_t get_api_editor_hash() { - if (api_editor_hash == 0) + if (api_editor_hash == 0) { api_editor_hash = ClassDB::get_api_hash(ClassDB::API_EDITOR); + } return api_editor_hash; } #endif // TOOLS_ENABLED @@ -210,7 +204,7 @@ public: UnhandledExceptionPolicy get_unhandled_exception_policy() const { return unhandled_exception_policy; } // Do not use these, unless you know what you're doing - void add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly); + void add_assembly(int32_t p_domain_id, GDMonoAssembly *p_assembly); GDMonoAssembly *get_loaded_assembly(const String &p_name); _FORCE_INLINE_ bool is_runtime_initialized() const { return runtime_initialized && !mono_runtime_is_shutting_down() /* stays true after shutdown finished */; } @@ -241,6 +235,7 @@ public: bool load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly = false); bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly = false); + bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs); bool load_assembly_from(const String &p_name, const String &p_path, GDMonoAssembly **r_assembly, bool p_refonly = false); Error finalize_and_unload_domain(MonoDomain *p_domain); @@ -255,23 +250,22 @@ public: namespace gdmono { class ScopeDomain { - MonoDomain *prev_domain; public: ScopeDomain(MonoDomain *p_domain) { - MonoDomain *prev_domain = mono_domain_get(); + prev_domain = mono_domain_get(); if (prev_domain != p_domain) { - this->prev_domain = prev_domain; mono_domain_set(p_domain, false); } else { - this->prev_domain = nullptr; + prev_domain = nullptr; } } ~ScopeDomain() { - if (prev_domain) + if (prev_domain) { mono_domain_set(prev_domain, false); + } } }; @@ -284,8 +278,9 @@ public: } ~ScopeExitDomainUnload() { - if (domain) + if (domain) { GDMono::get_singleton()->finalize_and_unload_domain(domain); + } } }; @@ -306,9 +301,6 @@ class _GodotSharp : public Object { bool _is_domain_finalizing_for_unload(int32_t p_domain_id); - List<NodePath *> np_delete_queue; - List<RID *> rid_delete_queue; - void _reload_assemblies(bool p_soft_reload); protected: @@ -326,7 +318,6 @@ public: bool is_scripts_domain_loaded(); - bool is_domain_finalizing_for_unload(); bool is_domain_finalizing_for_unload(int32_t p_domain_id); bool is_domain_finalizing_for_unload(MonoDomain *p_domain); diff --git a/modules/mono/mono_gd/gd_mono_assembly.cpp b/modules/mono/mono_gd/gd_mono_assembly.cpp index 0f211eebc6..9dbeee57ce 100644 --- a/modules/mono/mono_gd/gd_mono_assembly.cpp +++ b/modules/mono/mono_gd/gd_mono_assembly.cpp @@ -33,6 +33,7 @@ #include <mono/metadata/mono-debug.h> #include <mono/metadata/tokentype.h> +#include "core/io/file_access_pack.h" #include "core/list.h" #include "core/os/file_access.h" #include "core/os/os.h" @@ -45,7 +46,6 @@ Vector<String> GDMonoAssembly::search_dirs; void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config, const String &p_custom_bcl_dir) { - String framework_dir; if (!p_custom_bcl_dir.empty()) { @@ -99,8 +99,7 @@ void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const Strin // - The 'load' hook is called after the assembly has been loaded. Its job is to add the // assembly to the list of loaded assemblies so that the 'search' hook can look it up. -void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) { - +void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, [[maybe_unused]] void *user_data) { String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly))); MonoImage *image = mono_assembly_get_image(assembly); @@ -109,8 +108,9 @@ void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) #ifdef GD_MONO_HOT_RELOAD const char *path = mono_image_get_filename(image); - if (FileAccess::exists(path)) + if (FileAccess::exists(path)) { gdassembly->modified_time = FileAccess::get_modified_time(path); + } #endif MonoDomain *domain = mono_domain_get(); @@ -133,30 +133,24 @@ MonoAssembly *GDMonoAssembly::assembly_refonly_preload_hook(MonoAssemblyName *an return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, true); } -MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly) { - - (void)user_data; // UNUSED - +MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, [[maybe_unused]] void *user_data, bool refonly) { String name = String::utf8(mono_assembly_name_get_name(aname)); bool has_extension = name.ends_with(".dll") || name.ends_with(".exe"); GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name); - if (loaded_asm) + if (loaded_asm) { return loaded_asm->get_assembly(); + } return nullptr; } -MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, void *user_data, bool refonly) { - - (void)user_data; // UNUSED - +MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, [[maybe_unused]] void *user_data, bool refonly) { String name = String::utf8(mono_assembly_name_get_name(aname)); - return _load_assembly_search(name, search_dirs, refonly); + return _load_assembly_search(name, aname, refonly, search_dirs); } -MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) { - +MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) { MonoAssembly *res = nullptr; String path; @@ -168,23 +162,26 @@ MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const if (has_extension) { path = search_dir.plus_file(p_name); if (FileAccess::exists(path)) { - res = _real_load_assembly_from(path, p_refonly); - if (res != nullptr) + res = _real_load_assembly_from(path, p_refonly, p_aname); + if (res != nullptr) { return res; + } } } else { path = search_dir.plus_file(p_name + ".dll"); if (FileAccess::exists(path)) { - res = _real_load_assembly_from(path, p_refonly); - if (res != nullptr) + res = _real_load_assembly_from(path, p_refonly, p_aname); + if (res != nullptr) { return res; + } } path = search_dir.plus_file(p_name + ".exe"); if (FileAccess::exists(path)) { - res = _real_load_assembly_from(path, p_refonly); - if (res != nullptr) + res = _real_load_assembly_from(path, p_refonly, p_aname); + if (res != nullptr) { return res; + } } } } @@ -193,7 +190,6 @@ MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const } String GDMonoAssembly::find_assembly(const String &p_name) { - String path; bool has_extension = p_name.ends_with(".dll") || p_name.ends_with(".exe"); @@ -203,16 +199,19 @@ String GDMonoAssembly::find_assembly(const String &p_name) { if (has_extension) { path = search_dir.plus_file(p_name); - if (FileAccess::exists(path)) + if (FileAccess::exists(path)) { return path; + } } else { path = search_dir.plus_file(p_name + ".dll"); - if (FileAccess::exists(path)) + if (FileAccess::exists(path)) { return path; + } path = search_dir.plus_file(p_name + ".exe"); - if (FileAccess::exists(path)) + if (FileAccess::exists(path)) { return path; + } } } @@ -220,7 +219,6 @@ String GDMonoAssembly::find_assembly(const String &p_name) { } void GDMonoAssembly::initialize() { - fill_search_dirs(search_dirs); mono_install_assembly_search_hook(&assembly_search_hook, nullptr); @@ -230,8 +228,7 @@ void GDMonoAssembly::initialize() { mono_install_assembly_load_hook(&assembly_load_hook, nullptr); } -MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly) { - +MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname) { Vector<uint8_t> data = FileAccess::get_file_as_array(p_path); ERR_FAIL_COND_V_MSG(data.empty(), nullptr, "Could read the assembly in the specified location"); @@ -255,7 +252,33 @@ MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, boo true, &status, p_refonly, image_filename.utf8()); - ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from the loaded data"); + ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from memory: '" + p_path + "'."); + + if (p_aname != nullptr) { + // Check assembly version + const MonoTableInfo *table = mono_image_get_table_info(image, MONO_TABLE_ASSEMBLY); + + ERR_FAIL_NULL_V(table, nullptr); + + if (mono_table_info_get_rows(table)) { + uint32_t cols[MONO_ASSEMBLY_SIZE]; + mono_metadata_decode_row(table, 0, cols, MONO_ASSEMBLY_SIZE); + + // Not sure about .NET's policy. We will only ensure major and minor are equal, and ignore build and revision. + uint16_t major = cols[MONO_ASSEMBLY_MAJOR_VERSION]; + uint16_t minor = cols[MONO_ASSEMBLY_MINOR_VERSION]; + + uint16_t required_minor; + uint16_t required_major = mono_assembly_name_get_version(p_aname, &required_minor, nullptr, nullptr); + + if (required_major != 0) { + if (major != required_major && minor != required_minor) { + mono_image_close(image); + return nullptr; + } + } + } + } #ifdef DEBUG_ENABLED Vector<uint8_t> pdb_data; @@ -264,8 +287,9 @@ MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, boo if (!FileAccess::exists(pdb_path)) { pdb_path = p_path.get_basename() + ".pdb"; // without .dll - if (!FileAccess::exists(pdb_path)) + if (!FileAccess::exists(pdb_path)) { goto no_pdb; + } } pdb_data = FileAccess::get_file_as_array(pdb_path); @@ -292,8 +316,9 @@ no_pdb: String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly))); bool has_extension = name.ends_with(".dll") || name.ends_with(".exe"); GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name); - if (!loaded_asm) + if (!loaded_asm) { assembly_load_hook(assembly, nullptr); + } } // Decrement refcount which was previously incremented by mono_image_open_from_data_with_name @@ -303,7 +328,6 @@ no_pdb: } void GDMonoAssembly::unload() { - ERR_FAIL_NULL(image); // Should not be called if already unloaded for (Map<MonoClass *, GDMonoClass *>::Element *E = cached_raw.front(); E; E = E->next()) { @@ -322,20 +346,21 @@ String GDMonoAssembly::get_path() const { } GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const StringName &p_name) { - ERR_FAIL_NULL_V(image, nullptr); ClassKey key(p_namespace, p_name); GDMonoClass **match = cached_classes.getptr(key); - if (match) + if (match) { return *match; + } MonoClass *mono_class = mono_class_from_name(image, String(p_namespace).utf8(), String(p_name).utf8()); - if (!mono_class) + if (!mono_class) { return nullptr; + } GDMonoClass *wrapped_class = memnew(GDMonoClass(p_namespace, p_name, mono_class, this)); @@ -346,13 +371,13 @@ GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const Stri } GDMonoClass *GDMonoAssembly::get_class(MonoClass *p_mono_class) { - ERR_FAIL_NULL_V(image, nullptr); Map<MonoClass *, GDMonoClass *>::Element *match = cached_raw.find(p_mono_class); - if (match) + if (match) { return match->value(); + } StringName namespace_name = mono_class_get_namespace(p_mono_class); StringName class_name = mono_class_get_name(p_mono_class); @@ -366,14 +391,14 @@ GDMonoClass *GDMonoAssembly::get_class(MonoClass *p_mono_class) { } GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class) { - GDMonoClass *match = nullptr; if (gdobject_class_cache_updated) { Map<StringName, GDMonoClass *>::Element *result = gdobject_class_cache.find(p_class); - if (result) + if (result) { match = result->get(); + } } else { List<GDMonoClass *> nested_classes; @@ -382,30 +407,34 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class) for (int i = 1; i < rows; i++) { MonoClass *mono_class = mono_class_get(image, (i + 1) | MONO_TOKEN_TYPE_DEF); - if (!mono_class_is_assignable_from(CACHED_CLASS_RAW(GodotObject), mono_class)) + if (!mono_class_is_assignable_from(CACHED_CLASS_RAW(GodotObject), mono_class)) { continue; + } GDMonoClass *current = get_class(mono_class); - if (!current) + if (!current) { continue; + } nested_classes.push_back(current); - if (!match && current->get_name() == p_class) + if (!match && current->get_name() == p_class) { match = current; + } while (!nested_classes.empty()) { GDMonoClass *current_nested = nested_classes.front()->get(); - nested_classes.pop_back(); + nested_classes.pop_front(); void *iter = nullptr; while (true) { MonoClass *raw_nested = mono_class_get_nested_types(current_nested->get_mono_ptr(), &iter); - if (!raw_nested) + if (!raw_nested) { break; + } GDMonoClass *nested_class = get_class(raw_nested); @@ -425,10 +454,30 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class) return match; } -GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) { +GDMonoAssembly *GDMonoAssembly::load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) { + if (GDMono::get_singleton()->get_corlib_assembly() && (p_name == "mscorlib" || p_name == "mscorlib.dll")) { + return GDMono::get_singleton()->get_corlib_assembly(); + } + + // We need to manually call the search hook in this case, as it won't be called in the next step + MonoAssembly *assembly = mono_assembly_invoke_search_hook(p_aname); + + if (!assembly) { + assembly = _load_assembly_search(p_name, p_aname, p_refonly, p_search_dirs); + ERR_FAIL_NULL_V(assembly, nullptr); + } + + GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name); + ERR_FAIL_NULL_V_MSG(loaded_asm, nullptr, "Loaded assembly missing from table. Did we not receive the load hook?"); + ERR_FAIL_COND_V(loaded_asm->get_assembly() != assembly, nullptr); + + return loaded_asm; +} - if (p_name == "mscorlib" || p_name == "mscorlib.dll") +GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) { + if (p_name == "mscorlib" || p_name == "mscorlib.dll") { return GDMono::get_singleton()->get_corlib_assembly(); + } // We need to manually call the search hook in this case, as it won't be called in the next step MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8()); @@ -447,18 +496,8 @@ GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_ return loaded_asm; } -GDMonoAssembly::GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly) : - name(p_name), - image(p_image), - assembly(p_assembly), -#ifdef GD_MONO_HOT_RELOAD - modified_time(0), -#endif - gdobject_class_cache_updated(false) { -} - GDMonoAssembly::~GDMonoAssembly() { - - if (image) + if (image) { unload(); + } } diff --git a/modules/mono/mono_gd/gd_mono_assembly.h b/modules/mono/mono_gd/gd_mono_assembly.h index 43c8225b74..63899dc9be 100644 --- a/modules/mono/mono_gd/gd_mono_assembly.h +++ b/modules/mono/mono_gd/gd_mono_assembly.h @@ -40,7 +40,6 @@ #include "gd_mono_utils.h" class GDMonoAssembly { - struct ClassKey { struct Hasher { static _FORCE_INLINE_ uint32_t hash(const ClassKey &p_key) { @@ -73,10 +72,10 @@ class GDMonoAssembly { MonoAssembly *assembly; #ifdef GD_MONO_HOT_RELOAD - uint64_t modified_time; + uint64_t modified_time = 0; #endif - bool gdobject_class_cache_updated; + bool gdobject_class_cache_updated = false; Map<StringName, GDMonoClass *> gdobject_class_cache; HashMap<ClassKey, GDMonoClass *, ClassKey::Hasher> cached_classes; @@ -93,8 +92,8 @@ class GDMonoAssembly { static MonoAssembly *_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly); static MonoAssembly *_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data, bool refonly); - static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly); - static MonoAssembly *_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly); + static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname = nullptr); + static MonoAssembly *_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs); friend class GDMono; static void initialize(); @@ -120,10 +119,16 @@ public: static String find_assembly(const String &p_name); static void fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config = String(), const String &p_custom_bcl_dir = String()); + static const Vector<String> &get_default_search_dirs() { return search_dirs; } + static GDMonoAssembly *load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs); static GDMonoAssembly *load_from(const String &p_name, const String &p_path, bool p_refonly); - GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly); + GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly) : + name(p_name), + image(p_image), + assembly(p_assembly) { + } ~GDMonoAssembly(); }; diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp index 5ddf18d544..29aef6e609 100644 --- a/modules/mono/mono_gd/gd_mono_cache.cpp +++ b/modules/mono/mono_gd/gd_mono_cache.cpp @@ -65,7 +65,6 @@ CachedData cached_data; #define CACHE_METHOD_THUNK_AND_CHECK(m_class, m_method, m_val) CACHE_METHOD_THUNK_AND_CHECK_IMPL(cached_data.methodthunk_##m_class##_##m_method, m_val) void CachedData::clear_corlib_cache() { - corlib_cache_updated = false; class_MonoObject = nullptr; @@ -98,7 +97,6 @@ void CachedData::clear_corlib_cache() { } void CachedData::clear_godot_api_cache() { - godot_api_cache_updated = false; rawclass_Dictionary = nullptr; @@ -193,7 +191,6 @@ void CachedData::clear_godot_api_cache() { #define GODOT_API_NS_CLASS(m_ns, m_class) (GDMono::get_singleton()->get_core_api_assembly()->get_class(m_ns, #m_class)) void update_corlib_cache() { - CACHE_CLASS_AND_CHECK(MonoObject, GDMono::get_singleton()->get_corlib_assembly()->get_class(mono_get_object_class())); CACHE_CLASS_AND_CHECK(bool, GDMono::get_singleton()->get_corlib_assembly()->get_class(mono_get_boolean_class())); CACHE_CLASS_AND_CHECK(int8_t, GDMono::get_singleton()->get_corlib_assembly()->get_class(mono_get_sbyte_class())); @@ -220,7 +217,7 @@ void update_corlib_cache() { CACHE_METHOD_AND_CHECK(System_Diagnostics_StackTrace, ctor_Exception_bool, CACHED_CLASS(System_Diagnostics_StackTrace)->get_method_with_desc("System.Diagnostics.StackTrace:.ctor(System.Exception,bool)", true)); #endif - CACHE_METHOD_THUNK_AND_CHECK(Delegate, Equals, GDMono::get_singleton()->get_corlib_assembly()->get_class("System", "Delegate")->get_method_with_desc("System.Delegate:Equals(object)", 1)); + CACHE_METHOD_THUNK_AND_CHECK(Delegate, Equals, GDMono::get_singleton()->get_corlib_assembly()->get_class("System", "Delegate")->get_method_with_desc("System.Delegate:Equals(object)", true)); CACHE_CLASS_AND_CHECK(KeyNotFoundException, GDMono::get_singleton()->get_corlib_assembly()->get_class("System.Collections.Generic", "KeyNotFoundException")); @@ -228,7 +225,6 @@ void update_corlib_cache() { } void update_godot_api_cache() { - CACHE_CLASS_AND_CHECK(Vector2, GODOT_API_CLASS(Vector2)); CACHE_CLASS_AND_CHECK(Vector2i, GODOT_API_CLASS(Vector2i)); CACHE_CLASS_AND_CHECK(Rect2, GODOT_API_CLASS(Rect2)); diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index 3cf2bd6ce5..a7bbc763a7 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -37,7 +37,6 @@ namespace GDMonoCache { struct CachedData { - // ----------------------------------------------- // corlib classes diff --git a/modules/mono/mono_gd/gd_mono_class.cpp b/modules/mono/mono_gd/gd_mono_class.cpp index 2c65f7e3a0..6575cbc1c8 100644 --- a/modules/mono/mono_gd/gd_mono_class.cpp +++ b/modules/mono/mono_gd/gd_mono_class.cpp @@ -79,19 +79,34 @@ bool GDMonoClass::is_assignable_from(GDMonoClass *p_from) const { return mono_class_is_assignable_from(mono_class, p_from->mono_class); } -GDMonoClass *GDMonoClass::get_parent_class() { +StringName GDMonoClass::get_namespace() const { + GDMonoClass *nesting_class = get_nesting_class(); + if (!nesting_class) { + return namespace_name; + } + return nesting_class->get_namespace(); +} + +String GDMonoClass::get_name_for_lookup() const { + GDMonoClass *nesting_class = get_nesting_class(); + if (!nesting_class) { + return class_name; + } + return nesting_class->get_name_for_lookup() + "/" + class_name; +} + +GDMonoClass *GDMonoClass::get_parent_class() const { MonoClass *parent_mono_class = mono_class_get_parent(mono_class); return parent_mono_class ? GDMono::get_singleton()->get_class(parent_mono_class) : nullptr; } -GDMonoClass *GDMonoClass::get_nesting_class() { +GDMonoClass *GDMonoClass::get_nesting_class() const { MonoClass *nesting_type = mono_class_get_nesting_type(mono_class); return nesting_type ? GDMono::get_singleton()->get_class(nesting_type) : nullptr; } #ifdef TOOLS_ENABLED Vector<MonoClassField *> GDMonoClass::get_enum_fields() { - bool class_is_enum = mono_class_is_enum(mono_class); ERR_FAIL_COND_V(!class_is_enum, Vector<MonoClassField *>()); @@ -114,37 +129,38 @@ Vector<MonoClassField *> GDMonoClass::get_enum_fields() { #endif bool GDMonoClass::has_attribute(GDMonoClass *p_attr_class) { - #ifdef DEBUG_ENABLED ERR_FAIL_NULL_V(p_attr_class, false); #endif - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return false; + } return mono_custom_attrs_has_attr(attributes, p_attr_class->get_mono_ptr()); } MonoObject *GDMonoClass::get_attribute(GDMonoClass *p_attr_class) { - #ifdef DEBUG_ENABLED ERR_FAIL_NULL_V(p_attr_class, nullptr); #endif - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return nullptr; + } return mono_custom_attrs_get_attr(attributes, p_attr_class->get_mono_ptr()); } void GDMonoClass::fetch_attributes() { - ERR_FAIL_COND(attributes != nullptr); attributes = mono_custom_attrs_from_class(get_mono_ptr()); @@ -152,11 +168,11 @@ void GDMonoClass::fetch_attributes() { } void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base) { - CRASH_COND(!CACHED_CLASS(GodotObject)->is_assignable_from(this)); - if (methods_fetched) + if (methods_fetched) { return; + } void *iter = nullptr; MonoMethod *raw_method = nullptr; @@ -168,7 +184,6 @@ void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base ERR_CONTINUE(!method); if (method->get_name() != name) { - #ifdef DEBUG_ENABLED String fullname = method->get_ret_type_full_name() + " " + name + "(" + method->get_signature_desc(true) + ")"; WARN_PRINT("Method '" + fullname + "' is hidden by Godot API method. Should be '" + @@ -182,7 +197,6 @@ void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base // This allows us to warn the user here if he is using snake_case by mistake. if (p_native_base != this) { - GDMonoClass *native_top = p_native_base; while (native_top) { GDMonoMethod *m = native_top->get_method(name, method->get_parameters_count()); @@ -195,8 +209,9 @@ void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base break; } - if (native_top == CACHED_CLASS(GodotObject)) + if (native_top == CACHED_CLASS(GodotObject)) { break; + } native_top = native_top->get_parent_class(); } @@ -205,8 +220,9 @@ void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base uint32_t flags = mono_method_get_flags(method->mono_method, nullptr); - if (!(flags & MONO_METHOD_ATTR_VIRTUAL)) + if (!(flags & MONO_METHOD_ATTR_VIRTUAL)) { continue; + } // Virtual method of Godot Object derived type, let's try to find GodotMethod attribute @@ -228,15 +244,17 @@ void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base #endif MethodKey key = MethodKey(godot_method_name, method->get_parameters_count()); GDMonoMethod **existing_method = methods.getptr(key); - if (existing_method) + if (existing_method) { memdelete(*existing_method); // Must delete old one + } methods.set(key, method); break; } - if (top == CACHED_CLASS(GodotObject)) + if (top == CACHED_CLASS(GodotObject)) { break; + } top = top->get_parent_class(); } @@ -246,46 +264,44 @@ void GDMonoClass::fetch_methods_with_godot_api_checks(GDMonoClass *p_native_base } GDMonoMethod *GDMonoClass::get_fetched_method_unknown_params(const StringName &p_name) { - ERR_FAIL_COND_V(!methods_fetched, nullptr); const MethodKey *k = nullptr; while ((k = methods.next(k))) { - if (k->name == p_name) + if (k->name == p_name) { return methods.get(*k); + } } return nullptr; } bool GDMonoClass::has_fetched_method_unknown_params(const StringName &p_name) { - return get_fetched_method_unknown_params(p_name) != nullptr; } bool GDMonoClass::implements_interface(GDMonoClass *p_interface) { - return mono_class_implements_interface(mono_class, p_interface->get_mono_ptr()); } bool GDMonoClass::has_public_parameterless_ctor() { - GDMonoMethod *ctor = get_method(".ctor", 0); return ctor && ctor->get_visibility() == IMonoClassMember::PUBLIC; } GDMonoMethod *GDMonoClass::get_method(const StringName &p_name, int p_params_count) { - MethodKey key = MethodKey(p_name, p_params_count); GDMonoMethod **match = methods.getptr(key); - if (match) + if (match) { return *match; + } - if (methods_fetched) + if (methods_fetched) { return nullptr; + } MonoMethod *raw_method = mono_class_get_method_from_name(mono_class, String(p_name).utf8().get_data(), p_params_count); @@ -300,7 +316,6 @@ GDMonoMethod *GDMonoClass::get_method(const StringName &p_name, int p_params_cou } GDMonoMethod *GDMonoClass::get_method(MonoMethod *p_raw_method) { - MonoMethodSignature *sig = mono_method_signature(p_raw_method); int params_count = mono_signature_get_param_count(sig); @@ -310,22 +325,21 @@ GDMonoMethod *GDMonoClass::get_method(MonoMethod *p_raw_method) { } GDMonoMethod *GDMonoClass::get_method(MonoMethod *p_raw_method, const StringName &p_name) { - MonoMethodSignature *sig = mono_method_signature(p_raw_method); int params_count = mono_signature_get_param_count(sig); return get_method(p_raw_method, p_name, params_count); } GDMonoMethod *GDMonoClass::get_method(MonoMethod *p_raw_method, const StringName &p_name, int p_params_count) { - ERR_FAIL_NULL_V(p_raw_method, nullptr); MethodKey key = MethodKey(p_name, p_params_count); GDMonoMethod **match = methods.getptr(key); - if (match) + if (match) { return *match; + } GDMonoMethod *method = memnew(GDMonoMethod(p_name, p_raw_method)); methods.set(key, method); @@ -334,13 +348,13 @@ GDMonoMethod *GDMonoClass::get_method(MonoMethod *p_raw_method, const StringName } GDMonoMethod *GDMonoClass::get_method_with_desc(const String &p_description, bool p_include_namespace) { - MonoMethodDesc *desc = mono_method_desc_new(p_description.utf8().get_data(), p_include_namespace); MonoMethod *method = mono_method_desc_search_in_class(desc, mono_class); mono_method_desc_free(desc); - if (!method) + if (!method) { return nullptr; + } ERR_FAIL_COND_V(mono_method_get_class(method) != mono_class, nullptr); @@ -348,14 +362,15 @@ GDMonoMethod *GDMonoClass::get_method_with_desc(const String &p_description, boo } GDMonoField *GDMonoClass::get_field(const StringName &p_name) { - Map<StringName, GDMonoField *>::Element *result = fields.find(p_name); - if (result) + if (result) { return result->value(); + } - if (fields_fetched) + if (fields_fetched) { return nullptr; + } MonoClassField *raw_field = mono_class_get_field_from_name(mono_class, String(p_name).utf8().get_data()); @@ -370,9 +385,9 @@ GDMonoField *GDMonoClass::get_field(const StringName &p_name) { } const Vector<GDMonoField *> &GDMonoClass::get_all_fields() { - - if (fields_fetched) + if (fields_fetched) { return fields_list; + } void *iter = nullptr; MonoClassField *raw_field = nullptr; @@ -396,14 +411,15 @@ const Vector<GDMonoField *> &GDMonoClass::get_all_fields() { } GDMonoProperty *GDMonoClass::get_property(const StringName &p_name) { - Map<StringName, GDMonoProperty *>::Element *result = properties.find(p_name); - if (result) + if (result) { return result->value(); + } - if (properties_fetched) + if (properties_fetched) { return nullptr; + } MonoProperty *raw_property = mono_class_get_property_from_name(mono_class, String(p_name).utf8().get_data()); @@ -418,9 +434,9 @@ GDMonoProperty *GDMonoClass::get_property(const StringName &p_name) { } const Vector<GDMonoProperty *> &GDMonoClass::get_all_properties() { - - if (properties_fetched) + if (properties_fetched) { return properties_list; + } void *iter = nullptr; MonoProperty *raw_property = nullptr; @@ -444,8 +460,9 @@ const Vector<GDMonoProperty *> &GDMonoClass::get_all_properties() { } const Vector<GDMonoClass *> &GDMonoClass::get_all_delegates() { - if (delegates_fetched) + if (delegates_fetched) { return delegates_list; + } void *iter = nullptr; MonoClass *raw_class = nullptr; @@ -471,7 +488,6 @@ const Vector<GDMonoClass *> &GDMonoClass::get_all_delegates() { } const Vector<GDMonoMethod *> &GDMonoClass::get_all_methods() { - if (!method_list_fetched) { void *iter = nullptr; MonoMethod *raw_method = nullptr; @@ -486,7 +502,6 @@ const Vector<GDMonoMethod *> &GDMonoClass::get_all_methods() { } GDMonoClass::GDMonoClass(const StringName &p_namespace, const StringName &p_name, MonoClass *p_class, GDMonoAssembly *p_assembly) { - namespace_name = p_namespace; class_name = p_name; mono_class = p_class; @@ -503,7 +518,6 @@ GDMonoClass::GDMonoClass(const StringName &p_namespace, const StringName &p_name } GDMonoClass::~GDMonoClass() { - if (attributes) { mono_custom_attrs_free(attributes); } diff --git a/modules/mono/mono_gd/gd_mono_class.h b/modules/mono/mono_gd/gd_mono_class.h index 9237aae057..44b146b87c 100644 --- a/modules/mono/mono_gd/gd_mono_class.h +++ b/modules/mono/mono_gd/gd_mono_class.h @@ -113,14 +113,15 @@ public: bool is_assignable_from(GDMonoClass *p_from) const; - _FORCE_INLINE_ StringName get_namespace() const { return namespace_name; } + StringName get_namespace() const; _FORCE_INLINE_ StringName get_name() const { return class_name; } + String get_name_for_lookup() const; _FORCE_INLINE_ MonoClass *get_mono_ptr() const { return mono_class; } _FORCE_INLINE_ const GDMonoAssembly *get_assembly() const { return assembly; } - GDMonoClass *get_parent_class(); - GDMonoClass *get_nesting_class(); + GDMonoClass *get_parent_class() const; + GDMonoClass *get_nesting_class() const; #ifdef TOOLS_ENABLED Vector<MonoClassField *> get_enum_fields(); diff --git a/modules/mono/mono_gd/gd_mono_field.cpp b/modules/mono/mono_gd/gd_mono_field.cpp index e76cb84d43..563c45e71f 100644 --- a/modules/mono/mono_gd/gd_mono_field.cpp +++ b/modules/mono/mono_gd/gd_mono_field.cpp @@ -501,7 +501,8 @@ void GDMonoField::set_value_from_variant(MonoObject *p_object, const Variant &p_ case Variant::PACKED_COLOR_ARRAY: { SET_FROM_ARRAY(PackedColorArray); } break; - default: break; + default: + break; } } break; @@ -596,11 +597,13 @@ String GDMonoField::get_string_value(MonoObject *p_object) { bool GDMonoField::has_attribute(GDMonoClass *p_attr_class) { ERR_FAIL_NULL_V(p_attr_class, false); - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return false; + } return mono_custom_attrs_has_attr(attributes, p_attr_class->get_mono_ptr()); } @@ -608,11 +611,13 @@ bool GDMonoField::has_attribute(GDMonoClass *p_attr_class) { MonoObject *GDMonoField::get_attribute(GDMonoClass *p_attr_class) { ERR_FAIL_NULL_V(p_attr_class, nullptr); - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return nullptr; + } return mono_custom_attrs_get_attr(attributes, p_attr_class->get_mono_ptr()); } diff --git a/modules/mono/mono_gd/gd_mono_field.h b/modules/mono/mono_gd/gd_mono_field.h index 61f2c8f071..5b40b439f9 100644 --- a/modules/mono/mono_gd/gd_mono_field.h +++ b/modules/mono/mono_gd/gd_mono_field.h @@ -36,7 +36,6 @@ #include "i_mono_class_member.h" class GDMonoField : public IMonoClassMember { - GDMonoClass *owner; MonoClassField *mono_field; diff --git a/modules/mono/mono_gd/gd_mono_internals.cpp b/modules/mono/mono_gd/gd_mono_internals.cpp index 1898785699..fe1c2d28dd 100644 --- a/modules/mono/mono_gd/gd_mono_internals.cpp +++ b/modules/mono/mono_gd/gd_mono_internals.cpp @@ -45,7 +45,6 @@ namespace GDMonoInternals { void tie_managed_to_unmanaged(MonoObject *managed, Object *unmanaged) { - // This method should not fail CRASH_COND(!unmanaged); @@ -115,7 +114,7 @@ void tie_managed_to_unmanaged(MonoObject *managed, Object *unmanaged) { } void unhandled_exception(MonoException *p_exc) { - mono_unhandled_exception((MonoObject *)p_exc); // prints the exception as well + mono_print_unhandled_exception((MonoObject *)p_exc); if (GDMono::get_singleton()->get_unhandled_exception_policy() == GDMono::POLICY_TERMINATE_APP) { // Too bad 'mono_invoke_unhandled_exception_hook' is not exposed to embedders @@ -123,9 +122,10 @@ void unhandled_exception(MonoException *p_exc) { GD_UNREACHABLE(); } else { #ifdef DEBUG_ENABLED - GDMonoUtils::debug_send_unhandled_exception_error((MonoException *)p_exc); - if (EngineDebugger::is_active()) + GDMonoUtils::debug_send_unhandled_exception_error(p_exc); + if (EngineDebugger::is_active()) { EngineDebugger::get_singleton()->poll_events(false); + } #endif } } diff --git a/modules/mono/mono_gd/gd_mono_log.cpp b/modules/mono/mono_gd/gd_mono_log.cpp index ca16c2b76a..c5a988b8c3 100644 --- a/modules/mono/mono_gd/gd_mono_log.cpp +++ b/modules/mono/mono_gd/gd_mono_log.cpp @@ -51,13 +51,13 @@ GDMonoLog *GDMonoLog::singleton = nullptr; #ifdef GD_MONO_LOG_ENABLED static int get_log_level_id(const char *p_log_level) { - const char *valid_log_levels[] = { "error", "critical", "warning", "message", "info", "debug", nullptr }; int i = 0; while (valid_log_levels[i]) { - if (!strcmp(valid_log_levels[i], p_log_level)) + if (!strcmp(valid_log_levels[i], p_log_level)) { return i; + } i++; } @@ -65,7 +65,6 @@ static int get_log_level_id(const char *p_log_level) { } void GDMonoLog::mono_log_callback(const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *) { - FileAccess *f = GDMonoLog::get_singleton()->log_file; if (GDMonoLog::get_singleton()->log_level_id >= get_log_level_id(log_level)) { @@ -94,7 +93,6 @@ void GDMonoLog::mono_log_callback(const char *log_domain, const char *log_level, } bool GDMonoLog::_try_create_logs_dir(const String &p_logs_dir) { - if (!DirAccess::exists(p_logs_dir)) { DirAccessRef diraccess = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); ERR_FAIL_COND_V(!diraccess, false); @@ -106,7 +104,6 @@ bool GDMonoLog::_try_create_logs_dir(const String &p_logs_dir) { } void GDMonoLog::_delete_old_log_files(const String &p_logs_dir) { - static const uint64_t MAX_SECS = 5 * 86400; // 5 days DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); @@ -119,10 +116,12 @@ void GDMonoLog::_delete_old_log_files(const String &p_logs_dir) { String current; while ((current = da->get_next()).length()) { - if (da->current_is_dir()) + if (da->current_is_dir()) { continue; - if (!current.ends_with(".txt")) + } + if (!current.ends_with(".txt")) { continue; + } uint64_t modified_time = FileAccess::get_modified_time(da->get_current_dir().plus_file(current)); @@ -135,7 +134,6 @@ void GDMonoLog::_delete_old_log_files(const String &p_logs_dir) { } void GDMonoLog::initialize() { - CharString log_level = OS::get_singleton()->get_environment("GODOT_MONO_LOG_LEVEL").utf8(); if (log_level.length() != 0 && get_log_level_id(log_level.get_data()) == -1) { @@ -175,7 +173,7 @@ void GDMonoLog::initialize() { log_level_id = get_log_level_id(log_level.get_data()); if (log_file) { - OS::get_singleton()->print("Mono: Logfile is: %s\n", log_file_path.utf8().get_data()); + OS::get_singleton()->print("Mono: Log file is: '%s'\n", log_file_path.utf8().get_data()); mono_trace_set_log_handler(mono_log_callback, this); } else { OS::get_singleton()->printerr("Mono: No log file, using default log handler\n"); @@ -183,14 +181,12 @@ void GDMonoLog::initialize() { } GDMonoLog::GDMonoLog() { - singleton = this; log_level_id = -1; } GDMonoLog::~GDMonoLog() { - singleton = nullptr; if (log_file) { @@ -207,12 +203,10 @@ void GDMonoLog::initialize() { } GDMonoLog::GDMonoLog() { - singleton = this; } GDMonoLog::~GDMonoLog() { - singleton = nullptr; } diff --git a/modules/mono/mono_gd/gd_mono_log.h b/modules/mono/mono_gd/gd_mono_log.h index 1fc21f7df5..3a52316060 100644 --- a/modules/mono/mono_gd/gd_mono_log.h +++ b/modules/mono/mono_gd/gd_mono_log.h @@ -45,7 +45,6 @@ #endif class GDMonoLog { - #ifdef GD_MONO_LOG_ENABLED int log_level_id; diff --git a/modules/mono/mono_gd/gd_mono_marshal.cpp b/modules/mono/mono_gd/gd_mono_marshal.cpp index 91ee07388b..6d7d5f76cd 100644 --- a/modules/mono/mono_gd/gd_mono_marshal.cpp +++ b/modules/mono/mono_gd/gd_mono_marshal.cpp @@ -72,92 +72,119 @@ Variant::Type managed_to_variant_type(const ManagedType &p_type, bool *r_nil_is_ case MONO_TYPE_VALUETYPE: { GDMonoClass *vtclass = p_type.type_class; - if (vtclass == CACHED_CLASS(Vector2)) + if (vtclass == CACHED_CLASS(Vector2)) { return Variant::VECTOR2; + } - if (vtclass == CACHED_CLASS(Vector2i)) + if (vtclass == CACHED_CLASS(Vector2i)) { return Variant::VECTOR2I; + } - if (vtclass == CACHED_CLASS(Rect2)) + if (vtclass == CACHED_CLASS(Rect2)) { return Variant::RECT2; + } - if (vtclass == CACHED_CLASS(Rect2i)) + if (vtclass == CACHED_CLASS(Rect2i)) { return Variant::RECT2I; + } - if (vtclass == CACHED_CLASS(Transform2D)) + if (vtclass == CACHED_CLASS(Transform2D)) { return Variant::TRANSFORM2D; + } - if (vtclass == CACHED_CLASS(Vector3)) + if (vtclass == CACHED_CLASS(Vector3)) { return Variant::VECTOR3; + } - if (vtclass == CACHED_CLASS(Vector3i)) + if (vtclass == CACHED_CLASS(Vector3i)) { return Variant::VECTOR3I; + } - if (vtclass == CACHED_CLASS(Basis)) + if (vtclass == CACHED_CLASS(Basis)) { return Variant::BASIS; + } - if (vtclass == CACHED_CLASS(Quat)) + if (vtclass == CACHED_CLASS(Quat)) { return Variant::QUAT; + } - if (vtclass == CACHED_CLASS(Transform)) + if (vtclass == CACHED_CLASS(Transform)) { return Variant::TRANSFORM; + } - if (vtclass == CACHED_CLASS(AABB)) + if (vtclass == CACHED_CLASS(AABB)) { return Variant::AABB; + } - if (vtclass == CACHED_CLASS(Color)) + if (vtclass == CACHED_CLASS(Color)) { return Variant::COLOR; + } - if (vtclass == CACHED_CLASS(Plane)) + if (vtclass == CACHED_CLASS(Plane)) { return Variant::PLANE; + } - if (vtclass == CACHED_CLASS(Callable)) + if (vtclass == CACHED_CLASS(Callable)) { return Variant::CALLABLE; + } - if (vtclass == CACHED_CLASS(SignalInfo)) + if (vtclass == CACHED_CLASS(SignalInfo)) { return Variant::SIGNAL; + } - if (mono_class_is_enum(vtclass->get_mono_ptr())) + if (mono_class_is_enum(vtclass->get_mono_ptr())) { return Variant::INT; + } } break; case MONO_TYPE_ARRAY: case MONO_TYPE_SZARRAY: { MonoArrayType *array_type = mono_type_get_array_type(p_type.type_class->get_mono_type()); - if (array_type->eklass == CACHED_CLASS_RAW(MonoObject)) + if (array_type->eklass == CACHED_CLASS_RAW(MonoObject)) { return Variant::ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(uint8_t)) + if (array_type->eklass == CACHED_CLASS_RAW(uint8_t)) { return Variant::PACKED_BYTE_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(int32_t)) + if (array_type->eklass == CACHED_CLASS_RAW(int32_t)) { return Variant::PACKED_INT32_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(int64_t)) + if (array_type->eklass == CACHED_CLASS_RAW(int64_t)) { return Variant::PACKED_INT64_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(float)) + if (array_type->eklass == CACHED_CLASS_RAW(float)) { return Variant::PACKED_FLOAT32_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(double)) + if (array_type->eklass == CACHED_CLASS_RAW(double)) { return Variant::PACKED_FLOAT64_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(String)) + if (array_type->eklass == CACHED_CLASS_RAW(String)) { return Variant::PACKED_STRING_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(Vector2)) + if (array_type->eklass == CACHED_CLASS_RAW(Vector2)) { return Variant::PACKED_VECTOR2_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(Vector3)) + if (array_type->eklass == CACHED_CLASS_RAW(Vector3)) { return Variant::PACKED_VECTOR3_ARRAY; + } - if (array_type->eklass == CACHED_CLASS_RAW(Color)) + if (array_type->eklass == CACHED_CLASS_RAW(Color)) { return Variant::PACKED_COLOR_ARRAY; + } GDMonoClass *array_type_class = GDMono::get_singleton()->get_class(array_type->eklass); - if (CACHED_CLASS(GodotObject)->is_assignable_from(array_type_class)) + if (CACHED_CLASS(GodotObject)->is_assignable_from(array_type_class)) { return Variant::ARRAY; + } } break; case MONO_TYPE_CLASS: { @@ -201,8 +228,9 @@ Variant::Type managed_to_variant_type(const ManagedType &p_type, bool *r_nil_is_ } break; case MONO_TYPE_OBJECT: { - if (r_nil_is_variant) + if (r_nil_is_variant) { *r_nil_is_variant = true; + } return Variant::NIL; } break; @@ -244,8 +272,9 @@ Variant::Type managed_to_variant_type(const ManagedType &p_type, bool *r_nil_is_ } break; } - if (r_nil_is_variant) + if (r_nil_is_variant) { *r_nil_is_variant = false; + } // Unknown return Variant::NIL; @@ -282,31 +311,6 @@ bool try_get_array_element_type(const ManagedType &p_array_type, ManagedType &r_ return false; } -bool try_get_dictionary_key_value_types(const ManagedType &p_dictionary_type, ManagedType &r_key_type, ManagedType &r_value_type) { - switch (p_dictionary_type.type_encoding) { - case MONO_TYPE_GENERICINST: { - MonoReflectionType *dict_reftype = mono_type_get_object(mono_domain_get(), p_dictionary_type.type_class->get_mono_type()); - - if (GDMonoUtils::Marshal::type_is_generic_dictionary(dict_reftype) || - GDMonoUtils::Marshal::type_is_system_generic_dictionary(dict_reftype) || - GDMonoUtils::Marshal::type_is_generic_idictionary(dict_reftype)) { - MonoReflectionType *key_reftype; - MonoReflectionType *value_reftype; - - GDMonoUtils::Marshal::dictionary_get_key_value_types(dict_reftype, &key_reftype, &value_reftype); - - r_key_type = ManagedType::from_reftype(key_reftype); - r_value_type = ManagedType::from_reftype(value_reftype); - return true; - } - } break; - default: { - } break; - } - - return false; -} - String mono_to_utf8_string(MonoString *p_mono_string) { MonoError error; char *utf8 = mono_string_to_utf8_checked(p_mono_string, &error); @@ -328,8 +332,9 @@ String mono_to_utf16_string(MonoString *p_mono_string) { int len = mono_string_length(p_mono_string); String ret; - if (len == 0) + if (len == 0) { return ret; + } ret.resize(len + 1); ret.set(len, 0); @@ -409,8 +414,9 @@ MonoObject *variant_to_mono_object(const Variant *p_var, const ManagedType &p_ty } case MONO_TYPE_STRING: { - if (p_var->get_type() == Variant::NIL) + if (p_var->get_type() == Variant::NIL) { return nullptr; // Otherwise, Variant -> String would return the string "Null" + } return (MonoObject *)mono_string_from_godot(p_var->operator String()); } break; @@ -547,39 +553,50 @@ MonoObject *variant_to_mono_object(const Variant *p_var, const ManagedType &p_ty case MONO_TYPE_SZARRAY: { MonoArrayType *array_type = mono_type_get_array_type(p_type.type_class->get_mono_type()); - if (array_type->eklass == CACHED_CLASS_RAW(MonoObject)) + if (array_type->eklass == CACHED_CLASS_RAW(MonoObject)) { return (MonoObject *)Array_to_mono_array(p_var->operator Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(uint8_t)) + if (array_type->eklass == CACHED_CLASS_RAW(uint8_t)) { return (MonoObject *)PackedByteArray_to_mono_array(p_var->operator PackedByteArray()); + } - if (array_type->eklass == CACHED_CLASS_RAW(int32_t)) + if (array_type->eklass == CACHED_CLASS_RAW(int32_t)) { return (MonoObject *)PackedInt32Array_to_mono_array(p_var->operator PackedInt32Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(int64_t)) + if (array_type->eklass == CACHED_CLASS_RAW(int64_t)) { return (MonoObject *)PackedInt64Array_to_mono_array(p_var->operator PackedInt64Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(float)) + if (array_type->eklass == CACHED_CLASS_RAW(float)) { return (MonoObject *)PackedFloat32Array_to_mono_array(p_var->operator PackedFloat32Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(double)) + if (array_type->eklass == CACHED_CLASS_RAW(double)) { return (MonoObject *)PackedFloat64Array_to_mono_array(p_var->operator PackedFloat64Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(String)) + if (array_type->eklass == CACHED_CLASS_RAW(String)) { return (MonoObject *)PackedStringArray_to_mono_array(p_var->operator PackedStringArray()); + } - if (array_type->eklass == CACHED_CLASS_RAW(Vector2)) + if (array_type->eklass == CACHED_CLASS_RAW(Vector2)) { return (MonoObject *)PackedVector2Array_to_mono_array(p_var->operator PackedVector2Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(Vector3)) + if (array_type->eklass == CACHED_CLASS_RAW(Vector3)) { return (MonoObject *)PackedVector3Array_to_mono_array(p_var->operator PackedVector3Array()); + } - if (array_type->eklass == CACHED_CLASS_RAW(Color)) + if (array_type->eklass == CACHED_CLASS_RAW(Color)) { return (MonoObject *)PackedColorArray_to_mono_array(p_var->operator PackedColorArray()); + } GDMonoClass *array_type_class = GDMono::get_singleton()->get_class(array_type->eklass); - if (CACHED_CLASS(GodotObject)->is_assignable_from(array_type_class)) + if (CACHED_CLASS(GodotObject)->is_assignable_from(array_type_class)) { return (MonoObject *)Array_to_mono_array(p_var->operator Array(), array_type_class); + } ERR_FAIL_V_MSG(nullptr, "Attempted to convert Variant to a managed array of unmarshallable element type."); } break; @@ -624,8 +641,8 @@ MonoObject *variant_to_mono_object(const Variant *p_var, const ManagedType &p_ty return BOX_BOOLEAN(val); } case Variant::INT: { - int32_t val = p_var->operator signed int(); - return BOX_INT32(val); + int64_t val = p_var->operator int64_t(); + return BOX_INT64(val); } case Variant::FLOAT: { #ifdef REAL_T_IS_DOUBLE @@ -787,7 +804,6 @@ MonoObject *variant_to_mono_object(const Variant *p_var, const ManagedType &p_ty } Variant mono_object_to_variant_impl(MonoObject *p_obj, const ManagedType &p_type, bool p_fail_with_err = true) { - ERR_FAIL_COND_V(!p_type.type_class, Variant()); switch (p_type.type_encoding) { @@ -821,100 +837,128 @@ Variant mono_object_to_variant_impl(MonoObject *p_obj, const ManagedType &p_type return unbox<double>(p_obj); case MONO_TYPE_STRING: { - if (p_obj == nullptr) + if (p_obj == nullptr) { return Variant(); // NIL + } return mono_string_to_godot_not_null((MonoString *)p_obj); } break; case MONO_TYPE_VALUETYPE: { GDMonoClass *vtclass = p_type.type_class; - if (vtclass == CACHED_CLASS(Vector2)) + if (vtclass == CACHED_CLASS(Vector2)) { return MARSHALLED_IN(Vector2, unbox_addr<GDMonoMarshal::M_Vector2>(p_obj)); + } - if (vtclass == CACHED_CLASS(Vector2i)) + if (vtclass == CACHED_CLASS(Vector2i)) { return MARSHALLED_IN(Vector2i, unbox_addr<GDMonoMarshal::M_Vector2i>(p_obj)); + } - if (vtclass == CACHED_CLASS(Rect2)) + if (vtclass == CACHED_CLASS(Rect2)) { return MARSHALLED_IN(Rect2, unbox_addr<GDMonoMarshal::M_Rect2>(p_obj)); + } - if (vtclass == CACHED_CLASS(Rect2i)) + if (vtclass == CACHED_CLASS(Rect2i)) { return MARSHALLED_IN(Rect2i, unbox_addr<GDMonoMarshal::M_Rect2i>(p_obj)); + } - if (vtclass == CACHED_CLASS(Transform2D)) + if (vtclass == CACHED_CLASS(Transform2D)) { return MARSHALLED_IN(Transform2D, unbox_addr<GDMonoMarshal::M_Transform2D>(p_obj)); + } - if (vtclass == CACHED_CLASS(Vector3)) + if (vtclass == CACHED_CLASS(Vector3)) { return MARSHALLED_IN(Vector3, unbox_addr<GDMonoMarshal::M_Vector3>(p_obj)); + } - if (vtclass == CACHED_CLASS(Vector3i)) + if (vtclass == CACHED_CLASS(Vector3i)) { return MARSHALLED_IN(Vector3i, unbox_addr<GDMonoMarshal::M_Vector3i>(p_obj)); + } - if (vtclass == CACHED_CLASS(Basis)) + if (vtclass == CACHED_CLASS(Basis)) { return MARSHALLED_IN(Basis, unbox_addr<GDMonoMarshal::M_Basis>(p_obj)); + } - if (vtclass == CACHED_CLASS(Quat)) + if (vtclass == CACHED_CLASS(Quat)) { return MARSHALLED_IN(Quat, unbox_addr<GDMonoMarshal::M_Quat>(p_obj)); + } - if (vtclass == CACHED_CLASS(Transform)) + if (vtclass == CACHED_CLASS(Transform)) { return MARSHALLED_IN(Transform, unbox_addr<GDMonoMarshal::M_Transform>(p_obj)); + } - if (vtclass == CACHED_CLASS(AABB)) + if (vtclass == CACHED_CLASS(AABB)) { return MARSHALLED_IN(AABB, unbox_addr<GDMonoMarshal::M_AABB>(p_obj)); + } - if (vtclass == CACHED_CLASS(Color)) + if (vtclass == CACHED_CLASS(Color)) { return MARSHALLED_IN(Color, unbox_addr<GDMonoMarshal::M_Color>(p_obj)); + } - if (vtclass == CACHED_CLASS(Plane)) + if (vtclass == CACHED_CLASS(Plane)) { return MARSHALLED_IN(Plane, unbox_addr<GDMonoMarshal::M_Plane>(p_obj)); + } - if (vtclass == CACHED_CLASS(Callable)) + if (vtclass == CACHED_CLASS(Callable)) { return managed_to_callable(unbox<GDMonoMarshal::M_Callable>(p_obj)); + } - if (vtclass == CACHED_CLASS(SignalInfo)) + if (vtclass == CACHED_CLASS(SignalInfo)) { return managed_to_signal_info(unbox<GDMonoMarshal::M_SignalInfo>(p_obj)); + } - if (mono_class_is_enum(vtclass->get_mono_ptr())) + if (mono_class_is_enum(vtclass->get_mono_ptr())) { return unbox<int32_t>(p_obj); + } } break; case MONO_TYPE_ARRAY: case MONO_TYPE_SZARRAY: { MonoArrayType *array_type = mono_type_get_array_type(p_type.type_class->get_mono_type()); - if (array_type->eklass == CACHED_CLASS_RAW(MonoObject)) + if (array_type->eklass == CACHED_CLASS_RAW(MonoObject)) { return mono_array_to_Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(uint8_t)) + if (array_type->eklass == CACHED_CLASS_RAW(uint8_t)) { return mono_array_to_PackedByteArray((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(int32_t)) + if (array_type->eklass == CACHED_CLASS_RAW(int32_t)) { return mono_array_to_PackedInt32Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(int64_t)) + if (array_type->eklass == CACHED_CLASS_RAW(int64_t)) { return mono_array_to_PackedInt64Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(float)) + if (array_type->eklass == CACHED_CLASS_RAW(float)) { return mono_array_to_PackedFloat32Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(double)) + if (array_type->eklass == CACHED_CLASS_RAW(double)) { return mono_array_to_PackedFloat64Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(String)) + if (array_type->eklass == CACHED_CLASS_RAW(String)) { return mono_array_to_PackedStringArray((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(Vector2)) + if (array_type->eklass == CACHED_CLASS_RAW(Vector2)) { return mono_array_to_PackedVector2Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(Vector3)) + if (array_type->eklass == CACHED_CLASS_RAW(Vector3)) { return mono_array_to_PackedVector3Array((MonoArray *)p_obj); + } - if (array_type->eklass == CACHED_CLASS_RAW(Color)) + if (array_type->eklass == CACHED_CLASS_RAW(Color)) { return mono_array_to_PackedColorArray((MonoArray *)p_obj); + } GDMonoClass *array_type_class = GDMono::get_singleton()->get_class(array_type->eklass); - if (CACHED_CLASS(GodotObject)->is_assignable_from(array_type_class)) + if (CACHED_CLASS(GodotObject)->is_assignable_from(array_type_class)) { return mono_array_to_Array((MonoArray *)p_obj); + } if (p_fail_with_err) { ERR_FAIL_V_MSG(Variant(), "Attempted to convert a managed array of unmarshallable element type to Variant."); @@ -1013,8 +1057,9 @@ Variant mono_object_to_variant_impl(MonoObject *p_obj, const ManagedType &p_type } Variant mono_object_to_variant(MonoObject *p_obj) { - if (!p_obj) + if (!p_obj) { return Variant(); + } ManagedType type = ManagedType::from_class(mono_object_get_class(p_obj)); @@ -1022,20 +1067,26 @@ Variant mono_object_to_variant(MonoObject *p_obj) { } Variant mono_object_to_variant(MonoObject *p_obj, const ManagedType &p_type) { - if (!p_obj) + if (!p_obj) { return Variant(); + } return mono_object_to_variant_impl(p_obj, p_type); } Variant mono_object_to_variant_no_err(MonoObject *p_obj, const ManagedType &p_type) { - if (!p_obj) + if (!p_obj) { return Variant(); + } return mono_object_to_variant_impl(p_obj, p_type, /* fail_with_err: */ false); } String mono_object_to_variant_string(MonoObject *p_obj, MonoException **r_exc) { + if (p_obj == nullptr) { + return String("null"); + } + ManagedType type = ManagedType::from_class(mono_object_get_class(p_obj)); Variant var = GDMonoMarshal::mono_object_to_variant_no_err(p_obj, type); @@ -1045,8 +1096,9 @@ String mono_object_to_variant_string(MonoObject *p_obj, MonoException **r_exc) { MonoString *mono_str = GDMonoUtils::object_to_string(p_obj, &exc); if (exc) { - if (r_exc) + if (r_exc) { *r_exc = exc; + } return String(); } @@ -1156,8 +1208,9 @@ MonoArray *Array_to_mono_array(const Array &p_array, GDMonoClass *p_array_type_c Array mono_array_to_Array(MonoArray *p_array) { Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); @@ -1175,22 +1228,23 @@ MonoArray *PackedInt32Array_to_mono_array(const PackedInt32Array &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(int32_t), length); - int32_t *dst = (int32_t *)mono_array_addr(ret, int32_t, 0); - memcpy(dst, src, length); + int32_t *dst = mono_array_addr(ret, int32_t, 0); + memcpy(dst, src, length * sizeof(int32_t)); return ret; } PackedInt32Array mono_array_to_PackedInt32Array(MonoArray *p_array) { PackedInt32Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); int32_t *dst = ret.ptrw(); - const int32_t *src = (const int32_t *)mono_array_addr(p_array, int32_t, 0); - memcpy(dst, src, length); + const int32_t *src = mono_array_addr(p_array, int32_t, 0); + memcpy(dst, src, length * sizeof(int32_t)); return ret; } @@ -1201,22 +1255,23 @@ MonoArray *PackedInt64Array_to_mono_array(const PackedInt64Array &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(int64_t), length); - int64_t *dst = (int64_t *)mono_array_addr(ret, int64_t, 0); - memcpy(dst, src, length); + int64_t *dst = mono_array_addr(ret, int64_t, 0); + memcpy(dst, src, length * sizeof(int64_t)); return ret; } PackedInt64Array mono_array_to_PackedInt64Array(MonoArray *p_array) { PackedInt64Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); int64_t *dst = ret.ptrw(); - const int64_t *src = (const int64_t *)mono_array_addr(p_array, int64_t, 0); - memcpy(dst, src, length); + const int64_t *src = mono_array_addr(p_array, int64_t, 0); + memcpy(dst, src, length * sizeof(int64_t)); return ret; } @@ -1227,22 +1282,23 @@ MonoArray *PackedByteArray_to_mono_array(const PackedByteArray &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(uint8_t), length); - uint8_t *dst = (uint8_t *)mono_array_addr(ret, uint8_t, 0); - memcpy(dst, src, length); + uint8_t *dst = mono_array_addr(ret, uint8_t, 0); + memcpy(dst, src, length * sizeof(uint8_t)); return ret; } PackedByteArray mono_array_to_PackedByteArray(MonoArray *p_array) { PackedByteArray ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); uint8_t *dst = ret.ptrw(); - const uint8_t *src = (const uint8_t *)mono_array_addr(p_array, uint8_t, 0); - memcpy(dst, src, length); + const uint8_t *src = mono_array_addr(p_array, uint8_t, 0); + memcpy(dst, src, length * sizeof(uint8_t)); return ret; } @@ -1253,22 +1309,23 @@ MonoArray *PackedFloat32Array_to_mono_array(const PackedFloat32Array &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(float), length); - float *dst = (float *)mono_array_addr(ret, float, 0); - memcpy(dst, src, length); + float *dst = mono_array_addr(ret, float, 0); + memcpy(dst, src, length * sizeof(float)); return ret; } PackedFloat32Array mono_array_to_PackedFloat32Array(MonoArray *p_array) { PackedFloat32Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); float *dst = ret.ptrw(); - const float *src = (const float *)mono_array_addr(p_array, float, 0); - memcpy(dst, src, length); + const float *src = mono_array_addr(p_array, float, 0); + memcpy(dst, src, length * sizeof(float)); return ret; } @@ -1279,22 +1336,23 @@ MonoArray *PackedFloat64Array_to_mono_array(const PackedFloat64Array &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(double), length); - double *dst = (double *)mono_array_addr(ret, double, 0); - memcpy(dst, src, length); + double *dst = mono_array_addr(ret, double, 0); + memcpy(dst, src, length * sizeof(double)); return ret; } PackedFloat64Array mono_array_to_PackedFloat64Array(MonoArray *p_array) { PackedFloat64Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); double *dst = ret.ptrw(); - const double *src = (const double *)mono_array_addr(p_array, double, 0); - memcpy(dst, src, length); + const double *src = mono_array_addr(p_array, double, 0); + memcpy(dst, src, length * sizeof(double)); return ret; } @@ -1315,8 +1373,9 @@ MonoArray *PackedStringArray_to_mono_array(const PackedStringArray &p_array) { PackedStringArray mono_array_to_PackedStringArray(MonoArray *p_array) { PackedStringArray ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); String *w = ret.ptrw(); @@ -1336,8 +1395,8 @@ MonoArray *PackedColorArray_to_mono_array(const PackedColorArray &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(Color), length); if constexpr (InteropLayout::MATCHES_Color) { - Color *dst = (Color *)mono_array_addr(ret, Color, 0); - memcpy(dst, src, length); + Color *dst = mono_array_addr(ret, Color, 0); + memcpy(dst, src, length * sizeof(Color)); } else { for (int i = 0; i < length; i++) { M_Color *raw = (M_Color *)mono_array_addr_with_size(ret, sizeof(M_Color), i); @@ -1350,15 +1409,16 @@ MonoArray *PackedColorArray_to_mono_array(const PackedColorArray &p_array) { PackedColorArray mono_array_to_PackedColorArray(MonoArray *p_array) { PackedColorArray ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); Color *dst = ret.ptrw(); if constexpr (InteropLayout::MATCHES_Color) { - const Color *src = (const Color *)mono_array_addr(p_array, Color, 0); - memcpy(dst, src, length); + const Color *src = mono_array_addr(p_array, Color, 0); + memcpy(dst, src, length * sizeof(Color)); } else { for (int i = 0; i < length; i++) { dst[i] = MARSHALLED_IN(Color, (M_Color *)mono_array_addr_with_size(p_array, sizeof(M_Color), i)); @@ -1375,8 +1435,8 @@ MonoArray *PackedVector2Array_to_mono_array(const PackedVector2Array &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(Vector2), length); if constexpr (InteropLayout::MATCHES_Vector2) { - Vector2 *dst = (Vector2 *)mono_array_addr(ret, Vector2, 0); - memcpy(dst, src, length); + Vector2 *dst = mono_array_addr(ret, Vector2, 0); + memcpy(dst, src, length * sizeof(Vector2)); } else { for (int i = 0; i < length; i++) { M_Vector2 *raw = (M_Vector2 *)mono_array_addr_with_size(ret, sizeof(M_Vector2), i); @@ -1389,15 +1449,16 @@ MonoArray *PackedVector2Array_to_mono_array(const PackedVector2Array &p_array) { PackedVector2Array mono_array_to_PackedVector2Array(MonoArray *p_array) { PackedVector2Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); Vector2 *dst = ret.ptrw(); if constexpr (InteropLayout::MATCHES_Vector2) { - const Vector2 *src = (const Vector2 *)mono_array_addr(p_array, Vector2, 0); - memcpy(dst, src, length); + const Vector2 *src = mono_array_addr(p_array, Vector2, 0); + memcpy(dst, src, length * sizeof(Vector2)); } else { for (int i = 0; i < length; i++) { dst[i] = MARSHALLED_IN(Vector2, (M_Vector2 *)mono_array_addr_with_size(p_array, sizeof(M_Vector2), i)); @@ -1414,8 +1475,8 @@ MonoArray *PackedVector3Array_to_mono_array(const PackedVector3Array &p_array) { MonoArray *ret = mono_array_new(mono_domain_get(), CACHED_CLASS_RAW(Vector3), length); if constexpr (InteropLayout::MATCHES_Vector3) { - Vector3 *dst = (Vector3 *)mono_array_addr(ret, Vector3, 0); - memcpy(dst, src, length); + Vector3 *dst = mono_array_addr(ret, Vector3, 0); + memcpy(dst, src, length * sizeof(Vector3)); } else { for (int i = 0; i < length; i++) { M_Vector3 *raw = (M_Vector3 *)mono_array_addr_with_size(ret, sizeof(M_Vector3), i); @@ -1428,15 +1489,16 @@ MonoArray *PackedVector3Array_to_mono_array(const PackedVector3Array &p_array) { PackedVector3Array mono_array_to_PackedVector3Array(MonoArray *p_array) { PackedVector3Array ret; - if (!p_array) + if (!p_array) { return ret; + } int length = mono_array_length(p_array); ret.resize(length); Vector3 *dst = ret.ptrw(); if constexpr (InteropLayout::MATCHES_Vector3) { - const Vector3 *src = (const Vector3 *)mono_array_addr(p_array, Vector3, 0); - memcpy(dst, src, length); + const Vector3 *src = mono_array_addr(p_array, Vector3, 0); + memcpy(dst, src, length * sizeof(Vector3)); } else { for (int i = 0; i < length; i++) { dst[i] = MARSHALLED_IN(Vector3, (M_Vector3 *)mono_array_addr_with_size(p_array, sizeof(M_Vector3), i)); diff --git a/modules/mono/mono_gd/gd_mono_marshal.h b/modules/mono/mono_gd/gd_mono_marshal.h index f2d887e6d6..4ff330fd43 100644 --- a/modules/mono/mono_gd/gd_mono_marshal.h +++ b/modules/mono/mono_gd/gd_mono_marshal.h @@ -66,7 +66,6 @@ T *unbox_addr(MonoObject *p_obj) { Variant::Type managed_to_variant_type(const ManagedType &p_type, bool *r_nil_is_variant = nullptr); bool try_get_array_element_type(const ManagedType &p_array_type, ManagedType &r_elem_type); -bool try_get_dictionary_key_value_types(const ManagedType &p_dictionary_type, ManagedType &r_key_type, ManagedType &r_value_type); // String @@ -74,15 +73,17 @@ String mono_to_utf8_string(MonoString *p_mono_string); String mono_to_utf16_string(MonoString *p_mono_string); _FORCE_INLINE_ String mono_string_to_godot_not_null(MonoString *p_mono_string) { - if (sizeof(CharType) == 2) + if constexpr (sizeof(CharType) == 2) { return mono_to_utf16_string(p_mono_string); + } return mono_to_utf8_string(p_mono_string); } _FORCE_INLINE_ String mono_string_to_godot(MonoString *p_mono_string) { - if (p_mono_string == nullptr) + if (p_mono_string == nullptr) { return String(); + } return mono_string_to_godot_not_null(p_mono_string); } @@ -96,8 +97,9 @@ _FORCE_INLINE_ MonoString *mono_from_utf16_string(const String &p_string) { } _FORCE_INLINE_ MonoString *mono_string_from_godot(const String &p_string) { - if (sizeof(CharType) == 2) + if constexpr (sizeof(CharType) == 2) { return mono_from_utf16_string(p_string); + } return mono_from_utf8_string(p_string); } diff --git a/modules/mono/mono_gd/gd_mono_method.cpp b/modules/mono/mono_gd/gd_mono_method.cpp index 432aa74a6f..04f3b25a70 100644 --- a/modules/mono/mono_gd/gd_mono_method.cpp +++ b/modules/mono/mono_gd/gd_mono_method.cpp @@ -155,11 +155,13 @@ MonoObject *GDMonoMethod::invoke_raw(MonoObject *p_object, void **p_params, Mono bool GDMonoMethod::has_attribute(GDMonoClass *p_attr_class) { ERR_FAIL_NULL_V(p_attr_class, false); - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return false; + } return mono_custom_attrs_has_attr(attributes, p_attr_class->get_mono_ptr()); } @@ -167,11 +169,13 @@ bool GDMonoMethod::has_attribute(GDMonoClass *p_attr_class) { MonoObject *GDMonoMethod::get_attribute(GDMonoClass *p_attr_class) { ERR_FAIL_NULL_V(p_attr_class, nullptr); - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return nullptr; + } return mono_custom_attrs_get_attr(attributes, p_attr_class->get_mono_ptr()); } @@ -245,14 +249,14 @@ void GDMonoMethod::get_parameter_types(Vector<ManagedType> &types) const { } const MethodInfo &GDMonoMethod::get_method_info() { - if (!method_info_fetched) { method_info.name = name; bool nil_is_variant = false; method_info.return_val = PropertyInfo(GDMonoMarshal::managed_to_variant_type(return_type, &nil_is_variant), ""); - if (method_info.return_val.type == Variant::NIL && nil_is_variant) + if (method_info.return_val.type == Variant::NIL && nil_is_variant) { method_info.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + } Vector<StringName> names; get_parameter_names(names); @@ -260,8 +264,9 @@ const MethodInfo &GDMonoMethod::get_method_info() { for (int i = 0; i < params_count; ++i) { nil_is_variant = false; PropertyInfo arg_info = PropertyInfo(GDMonoMarshal::managed_to_variant_type(param_types[i], &nil_is_variant), names[i]); - if (arg_info.type == Variant::NIL && nil_is_variant) + if (arg_info.type == Variant::NIL && nil_is_variant) { arg_info.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + } method_info.arguments.push_back(arg_info); } diff --git a/modules/mono/mono_gd/gd_mono_method.h b/modules/mono/mono_gd/gd_mono_method.h index 54b2eba3e8..f78f57dca0 100644 --- a/modules/mono/mono_gd/gd_mono_method.h +++ b/modules/mono/mono_gd/gd_mono_method.h @@ -36,7 +36,6 @@ #include "i_mono_class_member.h" class GDMonoMethod : public IMonoClassMember { - StringName name; int params_count; diff --git a/modules/mono/mono_gd/gd_mono_method_thunk.h b/modules/mono/mono_gd/gd_mono_method_thunk.h index 0e05e974e9..01f3ae342a 100644 --- a/modules/mono/mono_gd/gd_mono_method_thunk.h +++ b/modules/mono/mono_gd/gd_mono_method_thunk.h @@ -47,10 +47,9 @@ template <class... ParamTypes> struct GDMonoMethodThunk { - typedef void(GD_MONO_STDCALL *M)(ParamTypes... p_args, MonoException **); - M mono_method_thunk; + M mono_method_thunk = nullptr; public: _FORCE_INLINE_ void invoke(ParamTypes... p_args, MonoException **r_exc) { @@ -81,9 +80,7 @@ public: mono_method_thunk = (M)mono_method_get_unmanaged_thunk(p_mono_method->get_mono_ptr()); } - GDMonoMethodThunk() : - mono_method_thunk(nullptr) { - } + GDMonoMethodThunk() {} explicit GDMonoMethodThunk(GDMonoMethod *p_mono_method) { set_from_method(p_mono_method); @@ -92,10 +89,9 @@ public: template <class R, class... ParamTypes> struct GDMonoMethodThunkR { - typedef R(GD_MONO_STDCALL *M)(ParamTypes... p_args, MonoException **); - M mono_method_thunk; + M mono_method_thunk = nullptr; public: _FORCE_INLINE_ R invoke(ParamTypes... p_args, MonoException **r_exc) { @@ -127,9 +123,7 @@ public: mono_method_thunk = (M)mono_method_get_unmanaged_thunk(p_mono_method->get_mono_ptr()); } - GDMonoMethodThunkR() : - mono_method_thunk(nullptr) { - } + GDMonoMethodThunkR() {} explicit GDMonoMethodThunkR(GDMonoMethod *p_mono_method) { #ifdef DEBUG_ENABLED @@ -247,8 +241,7 @@ struct VariadicInvokeMonoMethodR<1, R, P1> { template <class... ParamTypes> struct GDMonoMethodThunk { - - GDMonoMethod *mono_method; + GDMonoMethod *mono_method = nullptr; public: _FORCE_INLINE_ void invoke(ParamTypes... p_args, MonoException **r_exc) { @@ -277,9 +270,7 @@ public: mono_method = p_mono_method; } - GDMonoMethodThunk() : - mono_method(nullptr) { - } + GDMonoMethodThunk() {} explicit GDMonoMethodThunk(GDMonoMethod *p_mono_method) { set_from_method(p_mono_method); @@ -288,8 +279,7 @@ public: template <class R, class... ParamTypes> struct GDMonoMethodThunkR { - - GDMonoMethod *mono_method; + GDMonoMethod *mono_method = nullptr; public: _FORCE_INLINE_ R invoke(ParamTypes... p_args, MonoException **r_exc) { @@ -318,9 +308,7 @@ public: mono_method = p_mono_method; } - GDMonoMethodThunkR() : - mono_method(nullptr) { - } + GDMonoMethodThunkR() {} explicit GDMonoMethodThunkR(GDMonoMethod *p_mono_method) { set_from_method(p_mono_method); diff --git a/modules/mono/mono_gd/gd_mono_property.cpp b/modules/mono/mono_gd/gd_mono_property.cpp index c3e7598f2d..bc3be97102 100644 --- a/modules/mono/mono_gd/gd_mono_property.cpp +++ b/modules/mono/mono_gd/gd_mono_property.cpp @@ -77,15 +77,17 @@ GDMonoProperty::~GDMonoProperty() { bool GDMonoProperty::is_static() { MonoMethod *prop_method = mono_property_get_get_method(mono_property); - if (prop_method == nullptr) + if (prop_method == nullptr) { prop_method = mono_property_get_set_method(mono_property); + } return mono_method_get_flags(prop_method, nullptr) & MONO_METHOD_ATTR_STATIC; } IMonoClassMember::Visibility GDMonoProperty::get_visibility() { MonoMethod *prop_method = mono_property_get_get_method(mono_property); - if (prop_method == nullptr) + if (prop_method == nullptr) { prop_method = mono_property_get_set_method(mono_property); + } switch (mono_method_get_flags(prop_method, nullptr) & MONO_METHOD_ATTR_ACCESS_MASK) { case MONO_METHOD_ATTR_PRIVATE: @@ -106,11 +108,13 @@ IMonoClassMember::Visibility GDMonoProperty::get_visibility() { bool GDMonoProperty::has_attribute(GDMonoClass *p_attr_class) { ERR_FAIL_NULL_V(p_attr_class, false); - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return false; + } return mono_custom_attrs_has_attr(attributes, p_attr_class->get_mono_ptr()); } @@ -118,11 +122,13 @@ bool GDMonoProperty::has_attribute(GDMonoClass *p_attr_class) { MonoObject *GDMonoProperty::get_attribute(GDMonoClass *p_attr_class) { ERR_FAIL_NULL_V(p_attr_class, nullptr); - if (!attrs_fetched) + if (!attrs_fetched) { fetch_attributes(); + } - if (!attributes) + if (!attributes) { return nullptr; + } return mono_custom_attrs_get_attr(attributes, p_attr_class->get_mono_ptr()); } diff --git a/modules/mono/mono_gd/gd_mono_property.h b/modules/mono/mono_gd/gd_mono_property.h index 4653758a86..611ac293e4 100644 --- a/modules/mono/mono_gd/gd_mono_property.h +++ b/modules/mono/mono_gd/gd_mono_property.h @@ -36,7 +36,6 @@ #include "i_mono_class_member.h" class GDMonoProperty : public IMonoClassMember { - GDMonoClass *owner; MonoProperty *mono_property; diff --git a/modules/mono/mono_gd/gd_mono_utils.cpp b/modules/mono/mono_gd/gd_mono_utils.cpp index f9d492dabb..3f1155f430 100644 --- a/modules/mono/mono_gd/gd_mono_utils.cpp +++ b/modules/mono/mono_gd/gd_mono_utils.cpp @@ -38,7 +38,6 @@ #include "core/os/dir_access.h" #include "core/os/mutex.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "core/reference.h" #ifdef TOOLS_ENABLED @@ -51,14 +50,13 @@ #include "gd_mono_cache.h" #include "gd_mono_class.h" #include "gd_mono_marshal.h" -#include "gd_mono_method_thunk.h" namespace GDMonoUtils { MonoObject *unmanaged_get_managed(Object *unmanaged) { - - if (!unmanaged) + if (!unmanaged) { return nullptr; + } if (unmanaged->get_script_instance()) { CSharpInstance *cs_instance = CAST_CSHARP_INSTANCE(unmanaged->get_script_instance()); @@ -91,8 +89,9 @@ MonoObject *unmanaged_get_managed(Object *unmanaged) { MonoObject *target = gchandle.get_target(); - if (target) + if (target) { return target; + } CSharpLanguage::get_singleton()->release_script_gchandle(gchandle); @@ -197,8 +196,9 @@ GDMonoClass *get_object_class(MonoObject *p_object) { GDMonoClass *type_get_proxy_class(const StringName &p_type) { String class_name = p_type; - if (class_name[0] == '_') + if (class_name[0] == '_') { class_name = class_name.substr(1, class_name.length()); + } GDMonoClass *klass = GDMono::get_singleton()->get_core_api_assembly()->get_class(BINDINGS_NAMESPACE, class_name); @@ -221,11 +221,14 @@ GDMonoClass *get_class_native_base(GDMonoClass *p_class) { do { const GDMonoAssembly *assembly = klass->get_assembly(); - if (assembly == GDMono::get_singleton()->get_core_api_assembly()) + + if (assembly == GDMono::get_singleton()->get_core_api_assembly()) { return klass; + } #ifdef TOOLS_ENABLED - if (assembly == GDMono::get_singleton()->get_editor_api_assembly()) + if (assembly == GDMono::get_singleton()->get_editor_api_assembly()) { return klass; + } #endif } while ((klass = klass->get_parent_class()) != nullptr); @@ -386,14 +389,6 @@ String get_exception_name_and_message(MonoException *p_exc) { return res; } -void set_exception_message(MonoException *p_exc, String message) { - MonoClass *klass = mono_object_get_class((MonoObject *)p_exc); - MonoProperty *prop = mono_class_get_property_from_name(klass, "Message"); - MonoString *msg = GDMonoMarshal::mono_string_from_godot(message); - void *params[1] = { msg }; - property_set_value(prop, (MonoObject *)p_exc, params, nullptr); -} - void debug_print_unhandled_exception(MonoException *p_exc) { print_unhandled_exception(p_exc); debug_send_unhandled_exception_error(p_exc); @@ -411,8 +406,9 @@ void debug_send_unhandled_exception_error(MonoException *p_exc) { } static thread_local bool _recursion_flag_ = false; - if (_recursion_flag_) + if (_recursion_flag_) { return; + } _recursion_flag_ = true; SCOPE_EXIT { _recursion_flag_ = false; }; @@ -442,8 +438,9 @@ void debug_send_unhandled_exception_error(MonoException *p_exc) { Vector<ScriptLanguage::StackInfo> _si; if (stack_trace != nullptr) { _si = CSharpLanguage::get_singleton()->stack_trace_get_info(stack_trace); - for (int i = _si.size() - 1; i >= 0; i--) + for (int i = _si.size() - 1; i >= 0; i--) { si.insert(0, _si[i]); + } } exc_msg += (exc_msg.length() > 0 ? " ---> " : "") + GDMonoUtils::get_exception_name_and_message(p_exc); @@ -453,8 +450,9 @@ void debug_send_unhandled_exception_error(MonoException *p_exc) { CRASH_COND(inner_exc_prop == nullptr); MonoObject *inner_exc = inner_exc_prop->get_value((MonoObject *)p_exc); - if (inner_exc != nullptr) + if (inner_exc != nullptr) { si.insert(0, separator); + } p_exc = (MonoException *)inner_exc; } @@ -564,9 +562,10 @@ namespace Marshal { #ifdef MONO_GLUE_ENABLED #ifdef TOOLS_ENABLED -#define NO_GLUE_RET(m_ret) \ - { \ - if (!GDMonoCache::cached_data.godot_api_cache_updated) return m_ret; \ +#define NO_GLUE_RET(m_ret) \ + { \ + if (!GDMonoCache::cached_data.godot_api_cache_updated) \ + return m_ret; \ } #else #define NO_GLUE_RET(m_ret) \ @@ -663,8 +662,7 @@ GDMonoClass *make_generic_dictionary_type(MonoReflectionType *p_key_reftype, Mon } // namespace Marshal -ScopeThreadAttach::ScopeThreadAttach() : - mono_thread(nullptr) { +ScopeThreadAttach::ScopeThreadAttach() { if (likely(GDMono::get_singleton()->is_runtime_initialized()) && unlikely(!mono_domain_get())) { mono_thread = GDMonoUtils::attach_current_thread(); } diff --git a/modules/mono/mono_gd/gd_mono_utils.h b/modules/mono/mono_gd/gd_mono_utils.h index e3011ade5d..9db4a5f3f0 100644 --- a/modules/mono/mono_gd/gd_mono_utils.h +++ b/modules/mono/mono_gd/gd_mono_utils.h @@ -84,10 +84,6 @@ void detach_current_thread(MonoThread *p_mono_thread); MonoThread *get_current_thread(); bool is_thread_attached(); -_FORCE_INLINE_ bool is_main_thread() { - return mono_domain_get() != nullptr && mono_thread_get_main() == mono_thread_current(); -} - uint32_t new_strong_gchandle(MonoObject *p_object); uint32_t new_strong_gchandle_pinned(MonoObject *p_object); uint32_t new_weak_gchandle(MonoObject *p_object); @@ -115,7 +111,6 @@ String get_type_desc(MonoType *p_type); String get_type_desc(MonoReflectionType *p_reftype); String get_exception_name_and_message(MonoException *p_exc); -void set_exception_message(MonoException *p_exc, String message); void debug_print_unhandled_exception(MonoException *p_exc); void debug_send_unhandled_exception_error(MonoException *p_exc); @@ -134,6 +129,7 @@ extern thread_local int current_invoke_count; _FORCE_INLINE_ int get_runtime_invoke_count() { return current_invoke_count; } + _FORCE_INLINE_ int &get_runtime_invoke_count_ref() { return current_invoke_count; } @@ -155,7 +151,7 @@ struct ScopeThreadAttach { ~ScopeThreadAttach(); private: - MonoThread *mono_thread; + MonoThread *mono_thread = nullptr; }; StringName get_native_godot_class_name(GDMonoClass *p_class); diff --git a/modules/mono/mono_gd/managed_type.h b/modules/mono/mono_gd/managed_type.h index 84d1837853..491a2f3d20 100644 --- a/modules/mono/mono_gd/managed_type.h +++ b/modules/mono/mono_gd/managed_type.h @@ -36,18 +36,15 @@ #include "gd_mono_header.h" struct ManagedType { - int type_encoding; - GDMonoClass *type_class; + int type_encoding = 0; + GDMonoClass *type_class = nullptr; static ManagedType from_class(GDMonoClass *p_class); static ManagedType from_class(MonoClass *p_mono_class); static ManagedType from_type(MonoType *p_mono_type); static ManagedType from_reftype(MonoReflectionType *p_mono_reftype); - ManagedType() : - type_encoding(0), - type_class(nullptr) { - } + ManagedType() {} ManagedType(int p_type_encoding, GDMonoClass *p_type_class) : type_encoding(p_type_encoding), diff --git a/modules/mono/mono_gd/support/android_support.cpp b/modules/mono/mono_gd/support/android_support.cpp index 8bcdeec9dd..8bcdeec9dd 100755..100644 --- a/modules/mono/mono_gd/support/android_support.cpp +++ b/modules/mono/mono_gd/support/android_support.cpp diff --git a/modules/mono/mono_gd/support/ios_support.mm b/modules/mono/mono_gd/support/ios_support.mm index e3d1a647fd..e3d1a647fd 100755..100644 --- a/modules/mono/mono_gd/support/ios_support.mm +++ b/modules/mono/mono_gd/support/ios_support.mm diff --git a/modules/mono/register_types.cpp b/modules/mono/register_types.cpp index 94431e7c30..98c3ba1324 100644 --- a/modules/mono/register_types.cpp +++ b/modules/mono/register_types.cpp @@ -62,8 +62,9 @@ void register_mono_types() { void unregister_mono_types() { ScriptServer::unregister_language(script_language_cs); - if (script_language_cs) + if (script_language_cs) { memdelete(script_language_cs); + } ResourceLoader::remove_resource_format_loader(resource_loader_cs); resource_loader_cs.unref(); @@ -71,6 +72,7 @@ void unregister_mono_types() { ResourceSaver::remove_resource_format_saver(resource_saver_cs); resource_saver_cs.unref(); - if (_godotsharp) + if (_godotsharp) { memdelete(_godotsharp); + } } diff --git a/modules/mono/signal_awaiter_utils.cpp b/modules/mono/signal_awaiter_utils.cpp index e77a2e98f2..bd67b03c8e 100644 --- a/modules/mono/signal_awaiter_utils.cpp +++ b/modules/mono/signal_awaiter_utils.cpp @@ -51,18 +51,21 @@ bool SignalAwaiterCallable::compare_equal(const CallableCustom *p_a, const Calla const SignalAwaiterCallable *a = static_cast<const SignalAwaiterCallable *>(p_a); const SignalAwaiterCallable *b = static_cast<const SignalAwaiterCallable *>(p_b); - if (a->target_id != b->target_id) + if (a->target_id != b->target_id) { return false; + } - if (a->signal != b->signal) + if (a->signal != b->signal) { return false; + } return true; } bool SignalAwaiterCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) { - if (compare_equal(p_a, p_b)) + if (compare_equal(p_a, p_b)) { return false; + } return p_a < p_b; } @@ -77,7 +80,6 @@ String SignalAwaiterCallable::get_as_text() const { String class_name = base->get_class(); Ref<Script> script = base->get_script(); if (script.is_valid() && script->get_path().is_resource_file()) { - class_name += "(" + script->get_path().get_file() + ")"; } return class_name + "::SignalAwaiterMiddleman::" + String(signal); @@ -146,18 +148,21 @@ bool EventSignalCallable::compare_equal(const CallableCustom *p_a, const Callabl const EventSignalCallable *a = static_cast<const EventSignalCallable *>(p_a); const EventSignalCallable *b = static_cast<const EventSignalCallable *>(p_b); - if (a->owner != b->owner) + if (a->owner != b->owner) { return false; + } - if (a->event_signal != b->event_signal) + if (a->event_signal != b->event_signal) { return false; + } return true; } bool EventSignalCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) { - if (compare_equal(p_a, p_b)) + if (compare_equal(p_a, p_b)) { return false; + } return p_a < p_b; } diff --git a/modules/mono/utils/macros.h b/modules/mono/utils/macros.h index 8650d6cc09..dc542477f5 100644 --- a/modules/mono/utils/macros.h +++ b/modules/mono/utils/macros.h @@ -68,6 +68,6 @@ public: } // namespace gdmono #define SCOPE_EXIT \ - auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]() + auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]() -> void #endif // UTIL_MACROS_H diff --git a/modules/mono/utils/mono_reg_utils.cpp b/modules/mono/utils/mono_reg_utils.cpp index 8f0ad8ba5e..e0cf916a01 100644 --- a/modules/mono/utils/mono_reg_utils.cpp +++ b/modules/mono/utils/mono_reg_utils.cpp @@ -58,7 +58,6 @@ REGSAM _get_bitness_sam() { } LONG _RegOpenKey(HKEY hKey, LPCWSTR lpSubKey, PHKEY phkResult) { - LONG res = RegOpenKeyExW(hKey, lpSubKey, 0, KEY_READ, phkResult); if (res != ERROR_SUCCESS) @@ -68,7 +67,6 @@ LONG _RegOpenKey(HKEY hKey, LPCWSTR lpSubKey, PHKEY phkResult) { } LONG _RegKeyQueryString(HKEY hKey, const String &p_value_name, String &r_value) { - Vector<WCHAR> buffer; buffer.resize(512); DWORD dwBufferSize = buffer.size(); @@ -77,7 +75,6 @@ LONG _RegKeyQueryString(HKEY hKey, const String &p_value_name, String &r_value) if (res == ERROR_MORE_DATA) { // dwBufferSize now contains the actual size - Vector<WCHAR> buffer; buffer.resize(dwBufferSize); res = RegQueryValueExW(hKey, p_value_name.c_str(), 0, nullptr, (LPBYTE)buffer.ptr(), &dwBufferSize); } @@ -92,7 +89,6 @@ LONG _RegKeyQueryString(HKEY hKey, const String &p_value_name, String &r_value) } LONG _find_mono_in_reg(const String &p_subkey, MonoRegInfo &r_info, bool p_old_reg = false) { - HKEY hKey; LONG res = _RegOpenKey(HKEY_LOCAL_MACHINE, p_subkey.c_str(), &hKey); @@ -128,7 +124,6 @@ cleanup: } LONG _find_mono_in_reg_old(const String &p_subkey, MonoRegInfo &r_info) { - String default_clr; HKEY hKey; @@ -150,7 +145,6 @@ cleanup: } MonoRegInfo find_mono() { - MonoRegInfo info; if (_find_mono_in_reg("Software\\Mono", info) == ERROR_SUCCESS) @@ -163,7 +157,6 @@ MonoRegInfo find_mono() { } String find_msbuild_tools_path() { - String msbuild_tools_path; // Try to find 15.0 with vswhere @@ -232,6 +225,7 @@ cleanup: return msbuild_tools_path; } + } // namespace MonoRegUtils #endif // WINDOWS_ENABLED diff --git a/modules/mono/utils/mono_reg_utils.h b/modules/mono/utils/mono_reg_utils.h index f844a7233a..4ef876f2b6 100644 --- a/modules/mono/utils/mono_reg_utils.h +++ b/modules/mono/utils/mono_reg_utils.h @@ -36,7 +36,6 @@ #include "core/ustring.h" struct MonoRegInfo { - String version; String install_root_dir; String assembly_dir; diff --git a/modules/mono/utils/osx_utils.cpp b/modules/mono/utils/osx_utils.cpp index 8fadf3c109..e68466b1cf 100644 --- a/modules/mono/utils/osx_utils.cpp +++ b/modules/mono/utils/osx_utils.cpp @@ -38,25 +38,21 @@ #include <CoreServices/CoreServices.h> bool osx_is_app_bundle_installed(const String &p_bundle_id) { - - CFURLRef app_url = nullptr; CFStringRef bundle_id = CFStringCreateWithCString(nullptr, p_bundle_id.utf8(), kCFStringEncodingUTF8); - OSStatus result = LSFindApplicationForInfo(kLSUnknownCreator, bundle_id, nullptr, nullptr, &app_url); + CFArrayRef result = LSCopyApplicationURLsForBundleIdentifier(bundle_id, nullptr); CFRelease(bundle_id); - if (app_url) - CFRelease(app_url); - - switch (result) { - case noErr: + if (result) { + if (CFArrayGetCount(result) > 0) { + CFRelease(result); return true; - case kLSApplicationNotFoundErr: - break; - default: - break; + } else { + CFRelease(result); + return false; + } + } else { + return false; } - - return false; } #endif diff --git a/modules/mono/utils/path_utils.cpp b/modules/mono/utils/path_utils.cpp index 973375a471..ccfaf5aba7 100644 --- a/modules/mono/utils/path_utils.cpp +++ b/modules/mono/utils/path_utils.cpp @@ -50,34 +50,6 @@ namespace path { -String find_executable(const String &p_name) { -#ifdef WINDOWS_ENABLED - Vector<String> exts = OS::get_singleton()->get_environment("PATHEXT").split(ENV_PATH_SEP, false); -#endif - Vector<String> env_path = OS::get_singleton()->get_environment("PATH").split(ENV_PATH_SEP, false); - - if (env_path.empty()) - return String(); - - for (int i = 0; i < env_path.size(); i++) { - String p = path::join(env_path[i], p_name); - -#ifdef WINDOWS_ENABLED - for (int j = 0; j < exts.size(); j++) { - String p2 = p + exts[j].to_lower(); // lowercase to reduce risk of case mismatch warning - - if (FileAccess::exists(p2)) - return p2; - } -#else - if (FileAccess::exists(p)) - return p; -#endif - } - - return String(); -} - String cwd() { #ifdef WINDOWS_ENABLED const DWORD expected_size = ::GetCurrentDirectoryW(0, nullptr); @@ -90,12 +62,14 @@ String cwd() { return buffer.simplify_path(); #else char buffer[PATH_MAX]; - if (::getcwd(buffer, sizeof(buffer)) == nullptr) + if (::getcwd(buffer, sizeof(buffer)) == nullptr) { return "."; + } String result; - if (result.parse_utf8(buffer)) + if (result.parse_utf8(buffer)) { return "."; + } return result.simplify_path(); #endif @@ -135,23 +109,26 @@ String realpath(const String &p_path) { #elif UNIX_ENABLED char *resolved_path = ::realpath(p_path.utf8().get_data(), nullptr); - if (!resolved_path) + if (!resolved_path) { return p_path; + } String result; bool parse_ok = result.parse_utf8(resolved_path); ::free(resolved_path); - if (parse_ok) + if (parse_ok) { return p_path; + } return result.simplify_path(); #endif } String join(const String &p_a, const String &p_b) { - if (p_a.empty()) + if (p_a.empty()) { return p_b; + } const CharType a_last = p_a[p_a.length() - 1]; if ((a_last == '/' || a_last == '\\') || @@ -178,8 +155,9 @@ String relative_to_impl(const String &p_path, const String &p_relative_to) { } else { String base_dir = p_relative_to.get_base_dir(); - if (base_dir.length() <= 2 && (base_dir.empty() || base_dir.ends_with(":"))) + if (base_dir.length() <= 2 && (base_dir.empty() || base_dir.ends_with(":"))) { return p_path; + } return String("..").plus_file(relative_to_impl(p_path, base_dir)); } diff --git a/modules/mono/utils/path_utils.h b/modules/mono/utils/path_utils.h index 9965f58b0a..bcd8af8bb9 100644 --- a/modules/mono/utils/path_utils.h +++ b/modules/mono/utils/path_utils.h @@ -40,8 +40,6 @@ String join(const String &p_a, const String &p_b); String join(const String &p_a, const String &p_b, const String &p_c); String join(const String &p_a, const String &p_b, const String &p_c, const String &p_d); -String find_executable(const String &p_name); - /// Returns a normalized absolute path to the current working directory String cwd(); diff --git a/modules/mono/utils/string_utils.cpp b/modules/mono/utils/string_utils.cpp index 907811355f..f8d9804de4 100644 --- a/modules/mono/utils/string_utils.cpp +++ b/modules/mono/utils/string_utils.cpp @@ -38,14 +38,16 @@ namespace { int sfind(const String &p_text, int p_from) { - if (p_from < 0) + if (p_from < 0) { return -1; + } int src_len = 2; int len = p_text.length(); - if (len == 0) + if (len == 0) { return -1; + } const CharType *src = p_text.c_str(); @@ -75,17 +77,20 @@ int sfind(const String &p_text, int p_from) { } } - if (found) + if (found) { return i; + } } return -1; } + } // namespace String sformat(const String &p_text, const Variant &p1, const Variant &p2, const Variant &p3, const Variant &p4, const Variant &p5) { - if (p_text.length() < 2) + if (p_text.length() < 2) { return p_text; + } Array args; @@ -132,7 +137,6 @@ String sformat(const String &p_text, const Variant &p1, const Variant &p2, const #ifdef TOOLS_ENABLED bool is_csharp_keyword(const String &p_name) { - // Reserved keywords return p_name == "abstract" || p_name == "as" || p_name == "base" || p_name == "bool" || @@ -196,16 +200,6 @@ String str_format(const char *p_format, ...) { return res; } -// va_copy was defined in the C99, but not in C++ standards before C++11. -// When you compile C++ without --std=c++<XX> option, compilers still define -// va_copy, otherwise you have to use the internal version (__va_copy). -#if !defined(va_copy) -#if defined(__GNUC__) -#define va_copy(d, s) __va_copy((d), (s)) -#else -#define va_copy(d, s) ((d) = (s)) -#endif -#endif #if defined(MINGW_ENABLED) || defined(_MSC_VER) && _MSC_VER < 1900 #define gd_vsnprintf(m_buffer, m_count, m_format, m_args_copy) vsnprintf_s(m_buffer, m_count, _TRUNCATE, m_format, m_args_copy) |