diff options
-rw-r--r-- | modules/mono/csharp_script.cpp | 134 | ||||
-rw-r--r-- | modules/mono/csharp_script.h | 6 | ||||
-rw-r--r-- | modules/mono/editor/GodotSharpTools/Project/ProjectUtils.cs | 58 | ||||
-rw-r--r-- | modules/mono/editor/csharp_project.cpp | 109 | ||||
-rw-r--r-- | modules/mono/editor/csharp_project.h | 3 | ||||
-rw-r--r-- | modules/mono/editor/godotsharp_builds.cpp | 17 | ||||
-rw-r--r-- | modules/mono/editor/godotsharp_export.cpp | 13 | ||||
-rw-r--r-- | modules/mono/editor/godotsharp_export.h | 2 | ||||
-rw-r--r-- | modules/mono/editor/mono_bottom_panel.cpp | 6 | ||||
-rw-r--r-- | modules/mono/editor/script_class_parser.cpp | 486 | ||||
-rw-r--r-- | modules/mono/editor/script_class_parser.h | 74 | ||||
-rw-r--r-- | modules/mono/mono_gd/gd_mono.cpp | 2 | ||||
-rw-r--r-- | modules/mono/utils/string_utils.cpp | 28 | ||||
-rw-r--r-- | modules/mono/utils/string_utils.h | 2 |
14 files changed, 889 insertions, 51 deletions
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 5160d42367..72c0c09b23 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -32,6 +32,7 @@ #include <mono/metadata/threads.h> +#include "core/io/json.h" #include "core/os/file_access.h" #include "core/os/os.h" #include "core/os/thread.h" @@ -42,7 +43,6 @@ #include "editor/csharp_project.h" #include "editor/editor_node.h" #include "editor/godotsharp_editor.h" -#include "utils/string_utils.h" #endif #include "godotsharp_dirs.h" @@ -50,6 +50,7 @@ #include "mono_gd/gd_mono_marshal.h" #include "signal_awaiter_utils.h" #include "utils/macros.h" +#include "utils/string_utils.h" #include "utils/thread_local.h" #define CACHED_STRING_NAME(m_var) (CSharpLanguage::get_singleton()->get_string_names().m_var) @@ -370,6 +371,7 @@ 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()) @@ -432,8 +434,7 @@ static String variant_type_to_managed_name(const String &p_var_type_name) { return p_var_type_name; } -String CSharpLanguage::make_function(const String &p_class, const String &p_name, const PoolStringArray &p_args) const { -#ifdef TOOLS_ENABLED +String CSharpLanguage::make_function(const String &, const String &p_name, const PoolStringArray &p_args) const { // FIXME // - Due to Godot's API limitation this just appends the function to the end of the file // - Use fully qualified name if there is ambiguity @@ -449,10 +450,12 @@ String CSharpLanguage::make_function(const String &p_class, const String &p_name s += ")\n{\n // Replace with function body.\n}\n"; return s; +} #else +String CSharpLanguage::make_function(const String &, const String &, const PoolStringArray &) const { return String(); -#endif } +#endif String CSharpLanguage::_get_indentation() const { #ifdef TOOLS_ENABLED @@ -822,6 +825,49 @@ void CSharpLanguage::reload_assemblies_if_needed(bool p_soft_reload) { } #endif +void CSharpLanguage::project_assembly_loaded() { + + scripts_metadata.clear(); + + String scripts_metadata_filename = "scripts_metadata."; + +#ifdef TOOLS_ENABLED + scripts_metadata_filename += Engine::get_singleton()->is_editor_hint() ? "editor" : "editor_player"; +#else +#ifdef DEBUG_ENABLED + scripts_metadata_filename += "debug"; +#else + scripts_metadata_filename += "release"; +#endif +#endif + + String scripts_metadata_path = GodotSharpDirs::get_res_metadata_dir().plus_file(scripts_metadata_filename); + + if (FileAccess::exists(scripts_metadata_path)) { + String old_json; + + Error ferr = read_all_file_utf8(scripts_metadata_path, old_json); + ERR_FAIL_COND(ferr != OK); + + Variant old_dict_var; + String err_str; + int err_line; + Error json_err = JSON::parse(old_json, old_dict_var, err_str, err_line); + if (json_err != OK) { + ERR_PRINTS("Failed to parse metadata file: '" + err_str + "' (" + String::num_int64(err_line) + ")"); + return; + } + + scripts_metadata = old_dict_var.operator Dictionary(); + + print_verbose("Successfully loaded scripts metadata"); + } else { + if (!Engine::get_singleton()->is_editor_hint()) { + ERR_PRINT("Missing scripts metadata file"); + } + } +} + void CSharpLanguage::get_recognized_extensions(List<String> *p_extensions) const { p_extensions->push_back("cs"); @@ -2208,10 +2254,27 @@ bool CSharpScript::can_instance() const { #endif #ifdef TOOLS_ENABLED - return valid && (tool || ScriptServer::is_scripting_enabled()); + bool extra_cond = tool || ScriptServer::is_scripting_enabled(); #else - return valid; + bool extra_cond = true; #endif + + // FIXME Need to think this through better. + // For tool scripts, this will never fire if the class is not found. That's because we + // don't know if it's a tool script if we can't find the class to access the attributes. + if (extra_cond && !script_class) { + if (GDMono::get_singleton()->get_project_assembly() == NULL) { + // The project assembly is not loaded + ERR_EXPLAIN("Cannot instance script because the project assembly is not loaded. Script: " + get_path()); + ERR_FAIL_V(NULL); + } else { + // The project assembly is loaded, but the class could not found + ERR_EXPLAIN("Cannot instance script because the class '" + name + "' could not be found. Script: " + get_path()); + ERR_FAIL_V(NULL); + } + } + + return valid && extra_cond; } StringName CSharpScript::get_instance_base_type() const { @@ -2317,20 +2380,6 @@ ScriptInstance *CSharpScript::instance_create(Object *p_this) { CRASH_COND(!valid); #endif - if (!script_class) { - if (GDMono::get_singleton()->get_project_assembly() == NULL) { - // The project assembly is not loaded - ERR_EXPLAIN("Cannot instance script because the project assembly is not loaded. Script: " + get_path()); - ERR_FAIL_V(NULL); - } else { - // The project assembly is loaded, but the class could not found - ERR_EXPLAIN("Cannot instance script because the class '" + name + "' could not be found. Script: " + get_path()); - ERR_FAIL_V(NULL); - } - } - - ERR_FAIL_COND_V(!valid, NULL); - if (native) { String native_name = native->get_name(); if (!ClassDB::is_parent_class(p_this->get_class_name(), native_name)) { @@ -2418,7 +2467,23 @@ Error CSharpScript::reload(bool p_keep_state) { GDMonoAssembly *project_assembly = GDMono::get_singleton()->get_project_assembly(); if (project_assembly) { - script_class = project_assembly->get_object_derived_class(name); + const Variant *script_metadata_var = CSharpLanguage::get_singleton()->get_scripts_metadata().getptr(get_path()); + if (script_metadata_var) { + Dictionary script_metadata = script_metadata_var->operator Dictionary()["class"]; + const Variant *namespace_ = script_metadata.getptr("namespace"); + const Variant *class_name = script_metadata.getptr("class_name"); + 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); + script_class = klass; + } + } else { + // Missing script metadata. Fallback to legacy method + script_class = project_assembly->get_object_derived_class(name); + } valid = script_class != NULL; @@ -2546,29 +2611,14 @@ int CSharpScript::get_member_line(const StringName &p_member) const { Error CSharpScript::load_source_code(const String &p_path) { - PoolVector<uint8_t> sourcef; - Error err; - FileAccess *f = FileAccess::open(p_path, FileAccess::READ, &err); - ERR_FAIL_COND_V(err != OK, err); - - int len = f->get_len(); - sourcef.resize(len + 1); - PoolVector<uint8_t>::Write w = sourcef.write(); - int r = f->get_buffer(w.ptr(), len); - f->close(); - memdelete(f); - ERR_FAIL_COND_V(r != len, ERR_CANT_OPEN); - w[len] = 0; - - String s; - if (s.parse_utf8((const char *)w.ptr())) { - - ERR_EXPLAIN("Script '" + p_path + "' contains invalid unicode (utf-8), so it was not loaded. Please ensure that scripts are saved in valid utf-8 unicode."); - ERR_FAIL_V(ERR_INVALID_DATA); + Error ferr = read_all_file_utf8(p_path, source); + if (ferr != OK) { + if (ferr == ERR_INVALID_DATA) { + ERR_EXPLAIN("Script '" + p_path + "' contains invalid unicode (utf-8), so it was not loaded. Please ensure that scripts are saved in valid utf-8 unicode."); + } + ERR_FAIL_V(ferr); } - source = s; - #ifdef TOOLS_ENABLED source_changed_cache = true; #endif diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 2636ffbcc8..fc604df2a0 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -275,6 +275,8 @@ class CSharpLanguage : public ScriptLanguage { int lang_idx; + Dictionary scripts_metadata; + public: StringNameCache string_names; @@ -295,6 +297,10 @@ public: void reload_assemblies_if_needed(bool p_soft_reload); #endif + void project_assembly_loaded(); + + _FORCE_INLINE_ const Dictionary &get_scripts_metadata() { return scripts_metadata; } + virtual String get_name() const; /* LANGUAGE FUNCTIONS */ diff --git a/modules/mono/editor/GodotSharpTools/Project/ProjectUtils.cs b/modules/mono/editor/GodotSharpTools/Project/ProjectUtils.cs index 6889ea715f..a13f4fd6ef 100644 --- a/modules/mono/editor/GodotSharpTools/Project/ProjectUtils.cs +++ b/modules/mono/editor/GodotSharpTools/Project/ProjectUtils.cs @@ -1,5 +1,6 @@ -using System; +using System.Collections.Generic; using System.IO; +using DotNet.Globbing; using Microsoft.Build.Construction; namespace GodotSharpTools.Project @@ -10,8 +11,61 @@ namespace GodotSharpTools.Project { var dir = Directory.GetParent(projectPath).FullName; var root = ProjectRootElement.Open(projectPath); - if (root.AddItemChecked(itemType, include.RelativeToPath(dir).Replace("/", "\\"))) + var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\"); + + if (root.AddItemChecked(itemType, normalizedInclude)) root.Save(); } + + private static string[] GetAllFilesRecursive(string rootDirectory, string mask) + { + string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories); + + // We want relative paths + for (int i = 0; i < files.Length; i++) { + files[i] = files[i].RelativeToPath(rootDirectory); + } + + return files; + } + + public static string[] GetIncludeFiles(string projectPath, string itemType) + { + var result = new List<string>(); + var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); + + GlobOptions globOptions = new GlobOptions(); + globOptions.Evaluation.CaseInsensitive = false; + + var root = ProjectRootElement.Open(projectPath); + + foreach (var itemGroup in root.ItemGroups) + { + if (itemGroup.Condition.Length != 0) + continue; + + foreach (var item in itemGroup.Items) + { + if (item.ItemType != itemType) + continue; + + string normalizedInclude = item.Include.NormalizePath(); + + var glob = Glob.Parse(normalizedInclude, globOptions); + + // TODO Check somehow if path has no blog to avoid the following loop... + + foreach (var existingFile in existingFiles) + { + if (glob.IsMatch(existingFile)) + { + result.Add(existingFile); + } + } + } + } + + return result.ToArray(); + } } } diff --git a/modules/mono/editor/csharp_project.cpp b/modules/mono/editor/csharp_project.cpp index 6f223b75a3..03db765c2e 100644 --- a/modules/mono/editor/csharp_project.cpp +++ b/modules/mono/editor/csharp_project.cpp @@ -30,11 +30,15 @@ #include "csharp_project.h" +#include "core/io/json.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 { @@ -118,4 +122,109 @@ void add_item(const String &p_project_path, const String &p_item_type, const Str ERR_FAIL(); } } + +Error generate_scripts_metadata(const String &p_project_path, const String &p_output_path) { + + _GDMONO_SCOPE_DOMAIN_(TOOLS_DOMAIN) + + GDMonoClass *project_utils = GDMono::get_singleton()->get_editor_tools_assembly()->get_class("GodotSharpTools.Project", "ProjectUtils"); + + void *args[2] = { + GDMonoMarshal::mono_string_from_godot(p_project_path), + GDMonoMarshal::mono_string_from_godot("Compile") + }; + + MonoException *exc = NULL; + MonoArray *ret = (MonoArray *)project_utils->get_method("GetIncludeFiles", 2)->invoke_raw(NULL, args, &exc); + + if (exc) { + GDMonoUtils::debug_print_unhandled_exception(exc); + ERR_FAIL_V(FAILED); + } + + PoolStringArray project_files = GDMonoMarshal::mono_array_to_PoolStringArray(ret); + PoolStringArray::Read r = project_files.read(); + + Dictionary old_dict = CSharpLanguage::get_singleton()->get_scripts_metadata(); + Dictionary new_dict; + + for (int i = 0; i < project_files.size(); i++) { + const String &project_file = ("res://" + r[i]).simplify_path(); + + uint64_t modified_time = FileAccess::get_modified_time(project_file); + + const Variant *old_file_var = old_dict.getptr(project_file); + if (old_file_var) { + Dictionary old_file_dict = old_file_var->operator Dictionary(); + + if (old_file_dict["modified_time"].operator uint64_t() == modified_time) { + // No changes so no need to parse again + new_dict[project_file] = old_file_dict; + continue; + } + } + + ScriptClassParser scp; + Error err = scp.parse_file(project_file); + if (err != OK) { + ERR_EXPLAIN("Failed to determine namespace and class for script: " + project_file); + ERR_FAIL_V(err); + } + + Vector<ScriptClassParser::ClassDecl> classes = scp.get_classes(); + + bool found = false; + Dictionary class_dict; + + String search_name = project_file.get_file().get_basename(); + + for (int j = 0; j < classes.size(); j++) { + const ScriptClassParser::ClassDecl &class_decl = classes[j]; + + if (class_decl.base.size() == 0) + continue; // Does not inherit nor implement anything, so it can't be a script class + + String class_cmp; + + if (class_decl.nested) { + class_cmp = class_decl.name.get_slice(".", class_decl.name.get_slice_count(".") - 1); + } else { + class_cmp = class_decl.name; + } + + if (class_cmp != search_name) + continue; + + class_dict["namespace"] = class_decl.namespace_; + class_dict["class_name"] = class_decl.name; + class_dict["nested"] = class_decl.nested; + + found = true; + break; + } + + if (found) { + Dictionary file_dict; + file_dict["modified_time"] = modified_time; + file_dict["class"] = class_dict; + new_dict[project_file] = file_dict; + } + } + + if (new_dict.size()) { + String json = JSON::print(new_dict, "", false); + + Error ferr; + FileAccess *f = FileAccess::open(p_output_path, FileAccess::WRITE, &ferr); + ERR_EXPLAIN("Cannot open file for writing: " + p_output_path); + ERR_FAIL_COND_V(ferr != OK, ferr); + f->store_string(json); + f->flush(); + f->close(); + memdelete(f); + } + + return OK; +} + } // namespace CSharpProject diff --git a/modules/mono/editor/csharp_project.h b/modules/mono/editor/csharp_project.h index d852139de0..aeeeff50f0 100644 --- a/modules/mono/editor/csharp_project.h +++ b/modules/mono/editor/csharp_project.h @@ -40,6 +40,9 @@ String generate_editor_api_project(const String &p_dir, const String &p_core_dll String generate_game_project(const String &p_dir, const String &p_name, const Vector<String> &p_files = Vector<String>()); void add_item(const String &p_project_path, const String &p_item_type, const String &p_include); + +Error generate_scripts_metadata(const String &p_project_path, const String &p_output_path); + } // namespace CSharpProject #endif // CSHARP_PROJECT_H diff --git a/modules/mono/editor/godotsharp_builds.cpp b/modules/mono/editor/godotsharp_builds.cpp index b504cfe712..6216213716 100644 --- a/modules/mono/editor/godotsharp_builds.cpp +++ b/modules/mono/editor/godotsharp_builds.cpp @@ -39,6 +39,7 @@ #include "../mono_gd/gd_mono_marshal.h" #include "../utils/path_utils.h" #include "bindings_generator.h" +#include "csharp_project.h" #include "godotsharp_editor.h" #define PROP_NAME_MSBUILD_MONO "MSBuild (Mono)" @@ -268,7 +269,7 @@ bool GodotSharpBuilds::copy_api_assembly(const String &p_src_dir, const String & if (!FileAccess::exists(assembly_dst) || FileAccess::get_modified_time(assembly_src) > FileAccess::get_modified_time(assembly_dst) || GDMono::get_singleton()->metadata_is_api_assembly_invalidated(p_api_type)) { - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); String xml_file = p_assembly_name + ".xml"; if (da->copy(p_src_dir.plus_file(xml_file), p_dst_dir.plus_file(xml_file)) != OK) @@ -280,8 +281,6 @@ bool GodotSharpBuilds::copy_api_assembly(const String &p_src_dir, const String & Error err = da->copy(assembly_src, assembly_dst); - memdelete(da); - if (err != OK) { show_build_error_dialog("Failed to copy " + assembly_file); return false; @@ -398,6 +397,18 @@ bool GodotSharpBuilds::build_project_blocking(const String &p_config) { bool GodotSharpBuilds::editor_build_callback() { + String scripts_metadata_path_editor = GodotSharpDirs::get_res_metadata_dir().plus_file("scripts_metadata.editor"); + String scripts_metadata_path_player = GodotSharpDirs::get_res_metadata_dir().plus_file("scripts_metadata.editor_player"); + + Error metadata_err = CSharpProject::generate_scripts_metadata(GodotSharpDirs::get_project_csproj_path(), scripts_metadata_path_editor); + ERR_FAIL_COND_V(metadata_err != OK, false); + + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Error copy_err = da->copy(scripts_metadata_path_editor, scripts_metadata_path_player); + + ERR_EXPLAIN("Failed to copy scripts metadata file"); + ERR_FAIL_COND_V(copy_err != OK, false); + return build_project_blocking("Tools"); } diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp index 1e43bcf518..509ec961fb 100644 --- a/modules/mono/editor/godotsharp_export.cpp +++ b/modules/mono/editor/godotsharp_export.cpp @@ -37,6 +37,7 @@ #include "../godotsharp_dirs.h" #include "../mono_gd/gd_mono_class.h" #include "../mono_gd/gd_mono_marshal.h" +#include "csharp_project.h" #include "godotsharp_builds.h" static MonoString *godot_icall_GodotSharpExport_GetTemplatesDir() { @@ -86,6 +87,12 @@ void GodotSharpExport::_export_begin(const Set<String> &p_features, bool p_debug String build_config = p_debug ? "Debug" : "Release"; + String scripts_metadata_path = GodotSharpDirs::get_res_metadata_dir().plus_file("scripts_metadata." + String(p_debug ? "debug" : "release")); + Error metadata_err = CSharpProject::generate_scripts_metadata(GodotSharpDirs::get_project_csproj_path(), scripts_metadata_path); + ERR_FAIL_COND(metadata_err != OK); + + ERR_FAIL_COND(!_add_file(scripts_metadata_path, scripts_metadata_path)); + ERR_FAIL_COND(!GodotSharpBuilds::build_project_blocking(build_config)); // Add dependency assemblies @@ -124,7 +131,7 @@ void GodotSharpExport::_export_begin(const Set<String> &p_features, bool p_debug for (Map<String, String>::Element *E = dependencies.front(); E; E = E->next()) { String depend_src_path = E->value(); String depend_dst_path = GodotSharpDirs::get_res_assemblies_dir().plus_file(depend_src_path.get_file()); - ERR_FAIL_COND(!_add_assembly(depend_src_path, depend_dst_path)); + ERR_FAIL_COND(!_add_file(depend_src_path, depend_dst_path)); } // Mono specific export template extras (data dir) @@ -155,7 +162,7 @@ void GodotSharpExport::_export_begin(const Set<String> &p_features, bool p_debug } } -bool GodotSharpExport::_add_assembly(const String &p_src_path, const String &p_dst_path) { +bool GodotSharpExport::_add_file(const String &p_src_path, const String &p_dst_path, bool p_remap) { FileAccessRef f = FileAccess::open(p_src_path, FileAccess::READ); ERR_FAIL_COND_V(!f, false); @@ -164,7 +171,7 @@ bool GodotSharpExport::_add_assembly(const String &p_src_path, const String &p_d data.resize(f->get_len()); f->get_buffer(data.ptrw(), data.size()); - add_file(p_dst_path, data, false); + add_file(p_dst_path, data, p_remap); return true; } diff --git a/modules/mono/editor/godotsharp_export.h b/modules/mono/editor/godotsharp_export.h index f007016578..10d4375567 100644 --- a/modules/mono/editor/godotsharp_export.h +++ b/modules/mono/editor/godotsharp_export.h @@ -41,7 +41,7 @@ class GodotSharpExport : public EditorExportPlugin { MonoAssemblyName *aname_prealloc; - bool _add_assembly(const String &p_src_path, const String &p_dst_path); + bool _add_file(const String &p_src_path, const String &p_dst_path, bool p_remap = false); Error _get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Map<String, String> &r_dependencies); diff --git a/modules/mono/editor/mono_bottom_panel.cpp b/modules/mono/editor/mono_bottom_panel.cpp index 0ac59a1be8..d7bfa54aba 100644 --- a/modules/mono/editor/mono_bottom_panel.cpp +++ b/modules/mono/editor/mono_bottom_panel.cpp @@ -31,6 +31,8 @@ #include "mono_bottom_panel.h" #include "../csharp_script.h" +#include "../godotsharp_dirs.h" +#include "csharp_project.h" #include "godotsharp_editor.h" MonoBottomPanel *MonoBottomPanel::singleton = NULL; @@ -148,6 +150,10 @@ void MonoBottomPanel::_errors_toggled(bool p_pressed) { void MonoBottomPanel::_build_project_pressed() { + String scripts_metadata_path = GodotSharpDirs::get_res_metadata_dir().plus_file("scripts_metadata.editor"); + Error metadata_err = CSharpProject::generate_scripts_metadata(GodotSharpDirs::get_project_csproj_path(), scripts_metadata_path); + ERR_FAIL_COND(metadata_err != OK); + GodotSharpBuilds::get_singleton()->build_project_blocking("Tools"); MonoReloadNode::get_singleton()->restart_reload_timer(); diff --git a/modules/mono/editor/script_class_parser.cpp b/modules/mono/editor/script_class_parser.cpp new file mode 100644 index 0000000000..43de8df2b2 --- /dev/null +++ b/modules/mono/editor/script_class_parser.cpp @@ -0,0 +1,486 @@ +#include "script_class_parser.h" + +#include "core/map.h" +#include "core/os/os.h" + +#include "../utils/string_utils.h" + +ScriptClassParser::Token ScriptClassParser::get_token() { + + while (true) { + switch (code[idx]) { + case '\n': { + line++; + idx++; + break; + }; + case 0: { + return TK_EOF; + } break; + case '{': { + idx++; + return TK_CURLY_BRACKET_OPEN; + }; + case '}': { + idx++; + return TK_CURLY_BRACKET_CLOSE; + }; + case '[': { + idx++; + return TK_BRACKET_OPEN; + }; + case ']': { + idx++; + return TK_BRACKET_CLOSE; + }; + case '<': { + idx++; + return TK_OP_LESS; + }; + case '>': { + idx++; + return TK_OP_GREATER; + }; + case ':': { + idx++; + return TK_COLON; + }; + case ',': { + idx++; + return TK_COMMA; + }; + case '.': { + idx++; + return TK_PERIOD; + }; + case '#': { + //compiler directive + while (code[idx] != '\n' && code[idx] != 0) { + idx++; + } + continue; + } break; + case '/': { + switch (code[idx + 1]) { + case '*': { // block comment + idx += 2; + while (true) { + if (code[idx] == 0) { + error_str = "Unterminated comment"; + error = true; + return TK_ERROR; + } else if (code[idx] == '*' && code[idx + 1] == '/') { + idx += 2; + break; + } else if (code[idx] == '\n') { + line++; + } + + idx++; + } + + } break; + case '/': { // line comment skip + while (code[idx] != '\n' && code[idx] != 0) { + idx++; + } + + } break; + default: { + value = "/"; + idx++; + return TK_SYMBOL; + } + } + + continue; // a comment + } break; + case '\'': + case '"': { + bool verbatim = idx != 0 && code[idx - 1] == '@'; + + CharType begin_str = code[idx]; + idx++; + String tk_string = String(); + while (true) { + if (code[idx] == 0) { + error_str = "Unterminated String"; + error = true; + return TK_ERROR; + } else if (code[idx] == begin_str) { + if (verbatim && code[idx + 1] == '"') { // `""` is verbatim string's `\"` + idx += 2; // skip next `"` as well + continue; + } + + idx += 1; + break; + } else if (code[idx] == '\\' && !verbatim) { + //escaped characters... + idx++; + CharType next = code[idx]; + if (next == 0) { + error_str = "Unterminated String"; + error = true; + return TK_ERROR; + } + 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 'r': + res = 13; + break; + case '\"': res = '\"'; break; + case '\\': + res = '\\'; + break; + default: { + res = next; + } break; + } + + tk_string += res; + + } else { + if (code[idx] == '\n') + line++; + tk_string += code[idx]; + } + idx++; + } + + value = tk_string; + + return TK_STRING; + } break; + default: { + if (code[idx] <= 32) { + idx++; + break; + } + + if ((code[idx] >= 33 && code[idx] <= 47) || (code[idx] >= 58 && code[idx] <= 63) || (code[idx] >= 91 && code[idx] <= 94) || code[idx] == 96 || (code[idx] >= 123 && code[idx] <= 127)) { + value = String::chr(code[idx]); + idx++; + return TK_SYMBOL; + } + + if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) { + //a number + const CharType *rptr; + double number = String::to_double(&code[idx], &rptr); + idx += (rptr - &code[idx]); + value = number; + return TK_NUMBER; + + } else if ((code[idx] == '@' && code[idx + 1] != '"') || code[idx] == '_' || (code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) { + String id; + + id += code[idx]; + idx++; + + while (code[idx] == '_' || (code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || (code[idx] >= '0' && code[idx] <= '9') || code[idx] > 127) { + id += code[idx]; + idx++; + } + + value = id; + return TK_IDENTIFIER; + } else if (code[idx] == '@' && code[idx + 1] == '"') { + // begin of verbatim string + idx++; + } else { + error_str = "Unexpected character."; + error = true; + return TK_ERROR; + } + } + } + } +} + +Error ScriptClassParser::_skip_type_parameters() { + + Token tk; + + while (true) { + tk = get_token(); + + if (tk == TK_IDENTIFIER) { + tk = get_token(); + + if (tk == TK_OP_LESS) { + Error err = _skip_type_parameters(); + if (err) + return err; + continue; + } else if (tk != TK_COMMA) { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } + } else if (tk == TK_OP_LESS) { + error_str = "Expected identifier before `<`."; + error = true; + return ERR_PARSE_ERROR; + } else if (tk == TK_OP_GREATER) { + return OK; + } else { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } + } +} + +Error ScriptClassParser::_parse_class_base(Vector<String> &r_base) { + + Token tk; + + while (true) { + tk = get_token(); + + if (tk == TK_IDENTIFIER) { + bool generic = false; + + String name = value; + + tk = get_token(); + + if (tk == TK_OP_LESS) { + generic = true; + Error err = _skip_type_parameters(); + if (err) + return err; + } else if (tk == TK_COMMA) { + Error err = _parse_class_base(r_base); + if (err) + return err; + } else if (tk != TK_CURLY_BRACKET_OPEN) { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } + + r_base.push_back(!generic ? name : String()); // no generics, please + + return OK; + } else { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } + } +} + +Error ScriptClassParser::_parse_namespace_name(String &r_name, int &r_curly_stack) { + + Token tk = get_token(); + + if (tk == TK_IDENTIFIER) { + r_name += String(value); + } else { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } + + tk = get_token(); + + if (tk == TK_PERIOD) { + r_name += "."; + return _parse_namespace_name(r_name, r_curly_stack); + } else if (tk == TK_CURLY_BRACKET_OPEN) { + r_curly_stack++; + return OK; + } else { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } +} + +Error ScriptClassParser::parse(const String &p_code) { + + code = p_code; + idx = 0; + line = 0; + error_str = String(); + error = false; + value = Variant(); + classes.clear(); + + Token tk = get_token(); + + Map<int, NameDecl> name_stack; + int curly_stack = 0; + int type_curly_stack = 0; + + while (!error && tk != TK_EOF) { + if (tk == TK_IDENTIFIER && String(value) == "class") { + tk = get_token(); + + if (tk == TK_IDENTIFIER) { + String name = value; + int at_level = type_curly_stack; + + ClassDecl class_decl; + + for (Map<int, NameDecl>::Element *E = name_stack.front(); E; E = E->next()) { + const NameDecl &name_decl = E->value(); + + if (name_decl.type == NameDecl::NAMESPACE_DECL) { + if (E != name_stack.front()) + class_decl.namespace_ += "."; + class_decl.namespace_ += name_decl.name; + } else { + class_decl.name += name_decl.name + "."; + } + } + + class_decl.name += name; + class_decl.nested = type_curly_stack > 0; + + bool generic = false; + + while (true) { + tk = get_token(); + + if (tk == TK_COLON) { + Error err = _parse_class_base(class_decl.base); + if (err) + return err; + + curly_stack++; + type_curly_stack++; + + break; + } else if (tk == TK_CURLY_BRACKET_OPEN) { + curly_stack++; + type_curly_stack++; + break; + } else if (tk == TK_OP_LESS && !generic) { + generic = true; + + Error err = _skip_type_parameters(); + if (err) + return err; + } else { + error_str = "Unexpected token: " + itos(tk); + error = true; + return ERR_PARSE_ERROR; + } + } + + NameDecl name_decl; + name_decl.name = name; + name_decl.type = NameDecl::CLASS_DECL; + name_stack[at_level] = name_decl; + + if (!generic) { // no generics, thanks + classes.push_back(class_decl); + } else if (OS::get_singleton()->is_stdout_verbose()) { + String full_name = class_decl.namespace_; + if (full_name.length()) + full_name += "."; + full_name += class_decl.name; + OS::get_singleton()->print(String("Ignoring generic class declaration: " + class_decl.name).utf8()); + } + } + } else if (tk == TK_IDENTIFIER && String(value) == "struct") { + String name; + int at_level = type_curly_stack; + while (true) { + tk = get_token(); + if (tk == TK_IDENTIFIER && name.empty()) { + name = String(value); + } else if (tk == TK_CURLY_BRACKET_OPEN) { + if (name.empty()) { + error_str = "Expected identifier after keyword `struct`. Found `{`."; + error = true; + return ERR_PARSE_ERROR; + } + + curly_stack++; + type_curly_stack++; + break; + } else if (tk == TK_EOF) { + error_str = "Expected `{` after struct decl. Found `EOF`."; + error = true; + return ERR_PARSE_ERROR; + } + } + + NameDecl name_decl; + name_decl.name = name; + name_decl.type = NameDecl::STRUCT_DECL; + name_stack[at_level] = name_decl; + } else if (tk == TK_IDENTIFIER && String(value) == "namespace") { + if (type_curly_stack > 0) { + error_str = "Found namespace nested inside type."; + error = true; + return ERR_PARSE_ERROR; + } + + String name; + int at_level = curly_stack; + + Error err = _parse_namespace_name(name, curly_stack); + if (err) + return err; + + NameDecl name_decl; + name_decl.name = name; + name_decl.type = NameDecl::NAMESPACE_DECL; + name_stack[at_level] = name_decl; + } else if (tk == TK_CURLY_BRACKET_OPEN) { + curly_stack++; + } else if (tk == TK_CURLY_BRACKET_CLOSE) { + curly_stack--; + if (name_stack.has(curly_stack)) { + if (name_stack[curly_stack].type != NameDecl::NAMESPACE_DECL) + type_curly_stack--; + name_stack.erase(curly_stack); + } + } + + tk = get_token(); + } + + if (!error && tk == TK_EOF && curly_stack > 0) { + error_str = "Reached EOF with missing close curly brackets."; + error = true; + } + + if (error) + return ERR_PARSE_ERROR; + + return OK; +} + +Error ScriptClassParser::parse_file(const String &p_filepath) { + + String source; + + Error ferr = read_all_file_utf8(p_filepath, source); + if (ferr != OK) { + if (ferr == ERR_INVALID_DATA) { + ERR_EXPLAIN("File '" + p_filepath + "' contains invalid unicode (utf-8), so it was not loaded. Please ensure that scripts are saved in valid utf-8 unicode."); + } + ERR_FAIL_V(ferr); + } + + return parse(source); +} + +String ScriptClassParser::get_error() { + return error_str; +} + +Vector<ScriptClassParser::ClassDecl> ScriptClassParser::get_classes() { + return classes; +} diff --git a/modules/mono/editor/script_class_parser.h b/modules/mono/editor/script_class_parser.h new file mode 100644 index 0000000000..11cf1853e2 --- /dev/null +++ b/modules/mono/editor/script_class_parser.h @@ -0,0 +1,74 @@ +#ifndef SCRIPT_CLASS_PARSER_H +#define SCRIPT_CLASS_PARSER_H + +#include "core/ustring.h" +#include "core/variant.h" +#include "core/vector.h" + +class ScriptClassParser { + +public: + struct NameDecl { + enum Type { + NAMESPACE_DECL, + CLASS_DECL, + STRUCT_DECL + }; + + String name; + Type type; + }; + + struct ClassDecl { + String name; + String namespace_; + Vector<String> base; + bool nested; + bool has_script_attr; + }; + +private: + String code; + int idx; + int line; + String error_str; + bool error; + Variant value; + + Vector<ClassDecl> classes; + + enum Token { + TK_BRACKET_OPEN, + TK_BRACKET_CLOSE, + TK_CURLY_BRACKET_OPEN, + TK_CURLY_BRACKET_CLOSE, + TK_PERIOD, + TK_COLON, + TK_COMMA, + TK_SYMBOL, + TK_IDENTIFIER, + TK_STRING, + TK_NUMBER, + TK_OP_LESS, + TK_OP_GREATER, + TK_EOF, + TK_ERROR + }; + + Token get_token(); + + Error _skip_type_parameters(); + + Error _parse_class_base(Vector<String> &r_base); + Error _parse_namespace_name(String &r_name, int &r_curly_stack); + +public: + Error parse(const String &p_code); + Error parse_file(const String &p_filepath); + + String get_error(); + + Vector<ClassDecl> get_classes(); +}; + +#endif // SCRIPT_CLASS_PARSER_H diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index d0638d61ed..c64f13b49c 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -582,6 +582,8 @@ bool GDMono::_load_project_assembly() { if (success) { mono_assembly_set_main(project_assembly->get_assembly()); + + CSharpLanguage::get_singleton()->project_assembly_loaded(); } else { if (OS::get_singleton()->is_stdout_verbose()) print_error("Mono: Failed to load project assembly"); diff --git a/modules/mono/utils/string_utils.cpp b/modules/mono/utils/string_utils.cpp index 8691932f9a..6900866725 100644 --- a/modules/mono/utils/string_utils.cpp +++ b/modules/mono/utils/string_utils.cpp @@ -30,6 +30,8 @@ #include "string_utils.h" +#include "core/os/file_access.h" + namespace { int sfind(const String &p_text, int p_from) { @@ -128,6 +130,7 @@ String sformat(const String &p_text, const Variant &p1, const Variant &p2, const return new_string; } +#ifdef TOOLS_ENABLED bool is_csharp_keyword(const String &p_name) { // Reserved keywords @@ -156,3 +159,28 @@ bool is_csharp_keyword(const String &p_name) { String escape_csharp_keyword(const String &p_name) { return is_csharp_keyword(p_name) ? "@" + p_name : p_name; } +#endif + +Error read_all_file_utf8(const String &p_path, String &r_content) { + PoolVector<uint8_t> sourcef; + Error err; + FileAccess *f = FileAccess::open(p_path, FileAccess::READ, &err); + ERR_FAIL_COND_V(err != OK, err); + + int len = f->get_len(); + sourcef.resize(len + 1); + PoolVector<uint8_t>::Write w = sourcef.write(); + int r = f->get_buffer(w.ptr(), len); + f->close(); + memdelete(f); + ERR_FAIL_COND_V(r != len, ERR_CANT_OPEN); + w[len] = 0; + + String source; + if (source.parse_utf8((const char *)w.ptr())) { + ERR_FAIL_V(ERR_INVALID_DATA); + } + + r_content = source; + return OK; +} diff --git a/modules/mono/utils/string_utils.h b/modules/mono/utils/string_utils.h index f2df2340ae..ee803bd720 100644 --- a/modules/mono/utils/string_utils.h +++ b/modules/mono/utils/string_utils.h @@ -42,4 +42,6 @@ bool is_csharp_keyword(const String &p_name); String escape_csharp_keyword(const String &p_name); #endif +Error read_all_file_utf8(const String &p_path, String &r_content); + #endif // STRING_FORMAT_H |