diff options
10 files changed, 419 insertions, 138 deletions
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 18724ff6e1..e08491729b 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -558,42 +558,9 @@ bool CSharpLanguage::handles_global_class_type(const String &p_type) const { } String CSharpLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path) const { - Ref<CSharpScript> scr = ResourceLoader::load(p_path, get_type()); - // Always assign r_base_type and r_icon_path, even if the script - // is not a global one. In the case that it is not a global script, - // return an empty string AFTER assigning the return parameters. - // See GDScriptLanguage::get_global_class_name() in modules/gdscript/gdscript.cpp - - if (!scr.is_valid() || !scr->valid) { - // Invalid script. - return String(); - } - - if (r_icon_path) { - if (scr->icon_path.is_empty() || scr->icon_path.is_absolute_path()) { - *r_icon_path = scr->icon_path.simplify_path(); - } else if (scr->icon_path.is_relative_path()) { - *r_icon_path = p_path.get_base_dir().path_join(scr->icon_path).simplify_path(); - } - } - if (r_base_type) { - bool found_global_base_script = false; - const CSharpScript *top = scr->base_script.ptr(); - while (top != nullptr) { - if (top->global_class) { - *r_base_type = top->class_name; - found_global_base_script = true; - break; - } - - top = top->base_script.ptr(); - } - if (!found_global_base_script) { - *r_base_type = scr->get_instance_base_type(); - } - } - - return scr->global_class ? scr->class_name : String(); + String class_name; + GDMonoCache::managed_callbacks.ScriptManagerBridge_GetGlobalClassName(&p_path, r_base_type, r_icon_path, &class_name); + return class_name; } String CSharpLanguage::debug_get_error() const { @@ -697,25 +664,19 @@ struct CSharpScriptDepSort { // Shouldn't happen but just in case... return false; } - const CSharpScript *I = get_base_script(B.ptr()).ptr(); + const Script *I = B->get_base_script().ptr(); while (I) { if (I == A.ptr()) { // A is a base of B return true; } - I = get_base_script(I).ptr(); + I = I->get_base_script().ptr(); } // A isn't a base of B return false; } - - // Special fix for constructed generic types. - Ref<CSharpScript> get_base_script(const CSharpScript *p_script) const { - Ref<CSharpScript> base_script = p_script->base_script; - return base_script.is_valid() && !base_script->class_name.is_empty() ? base_script : nullptr; - } }; void CSharpLanguage::reload_all_scripts() { @@ -937,7 +898,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { obj->set_script(Ref<RefCounted>()); // Remove script and existing script instances (placeholder are not removed before domain reload) } - scr->was_tool_before_reload = scr->tool; + scr->was_tool_before_reload = scr->type_info.is_tool; scr->_clear(); } @@ -997,7 +958,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { scr->exports_invalidated = true; #endif - if (!scr->get_path().is_empty()) { + if (!scr->get_path().is_empty() && !scr->get_path().begins_with("csharp://")) { scr->reload(p_soft_reload); if (!scr->valid) { @@ -1839,6 +1800,7 @@ bool CSharpInstance::_internal_new_managed() { ERR_FAIL_NULL_V(owner, false); ERR_FAIL_COND_V(script.is_null(), false); + ERR_FAIL_COND_V(!script->can_instantiate(), false); bool ok = GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance( script.ptr(), owner, nullptr, 0); @@ -2161,7 +2123,7 @@ void GD_CLR_STDCALL CSharpScript::_add_property_info_list_callback(CSharpScript #ifdef TOOLS_ENABLED p_script->exported_members_cache.push_back(PropertyInfo( - Variant::NIL, *p_current_class_name, PROPERTY_HINT_NONE, + Variant::NIL, p_script->type_info.class_name, PROPERTY_HINT_NONE, p_script->get_path(), PROPERTY_USAGE_CATEGORY)); #endif @@ -2334,9 +2296,7 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) { // Extract information about the script using the mono class. void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { - bool tool = false; - bool global_class = false; - bool abstract_class = false; + TypeInfo type_info; // TODO: Use GDExtension godot_dictionary Array methods_array; @@ -2346,18 +2306,12 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { Dictionary signals_dict; signals_dict.~Dictionary(); - String class_name; - String icon_path; Ref<CSharpScript> base_script; GDMonoCache::managed_callbacks.ScriptManagerBridge_UpdateScriptClassInfo( - p_script.ptr(), &class_name, &tool, &global_class, &abstract_class, &icon_path, + p_script.ptr(), &type_info, &methods_array, &rpc_functions_dict, &signals_dict, &base_script); - p_script->class_name = class_name; - p_script->tool = tool; - p_script->global_class = global_class; - p_script->abstract_class = abstract_class; - p_script->icon_path = icon_path; + p_script->type_info = type_info; p_script->rpc_config.clear(); p_script->rpc_config = rpc_functions_dict; @@ -2436,7 +2390,7 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { bool CSharpScript::can_instantiate() const { #ifdef TOOLS_ENABLED - bool extra_cond = tool || ScriptServer::is_scripting_enabled(); + bool extra_cond = type_info.is_tool || ScriptServer::is_scripting_enabled(); #else bool extra_cond = true; #endif @@ -2445,10 +2399,10 @@ bool CSharpScript::can_instantiate() const { // 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 && !valid) { - ERR_FAIL_V_MSG(false, "Cannot instance script because the associated class could not be found. Script: '" + get_path() + "'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive)."); + ERR_FAIL_V_MSG(false, "Cannot instantiate C# script because the associated class could not be found. Script: '" + get_path() + "'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive)."); } - return valid && !abstract_class && extra_cond; + return valid && type_info.can_instantiate() && extra_cond; } StringName CSharpScript::get_instance_base_type() const { @@ -2458,6 +2412,8 @@ StringName CSharpScript::get_instance_base_type() const { } CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error) { + ERR_FAIL_COND_V_MSG(!type_info.can_instantiate(), nullptr, "Cannot instantiate C# script. Script: '" + get_path() + "'."); + /* STEP 1, CREATE */ Ref<RefCounted> ref; @@ -2772,11 +2728,11 @@ bool CSharpScript::inherits_script(const Ref<Script> &p_script) const { } Ref<Script> CSharpScript::get_base_script() const { - return base_script.is_valid() && !base_script->get_path().is_empty() ? base_script : nullptr; + return base_script; } StringName CSharpScript::get_global_name() const { - return global_class ? StringName(class_name) : StringName(); + return type_info.is_global_class ? StringName(type_info.class_name) : StringName(); } void CSharpScript::get_script_property_list(List<PropertyInfo> *r_list) const { @@ -2833,7 +2789,7 @@ Error CSharpScript::load_source_code(const String &p_path) { } void CSharpScript::_clear() { - tool = false; + type_info = TypeInfo(); valid = false; reload_invalidated = true; } @@ -2881,17 +2837,25 @@ Ref<Resource> ResourceFormatLoaderCSharpScript::load(const String &p_path, const // TODO ignore anything inside bin/ and obj/ in tools builds? + String real_path = p_path; + if (p_path.begins_with("csharp://")) { + // This is a virtual path used by generic types, extract the real path. + real_path = "res://" + p_path.trim_prefix("csharp://"); + real_path = real_path.substr(0, real_path.rfind(":")); + } + Ref<CSharpScript> scr; if (GDMonoCache::godot_api_cache_updated) { GDMonoCache::managed_callbacks.ScriptManagerBridge_GetOrCreateScriptBridgeForPath(&p_path, &scr); + ERR_FAIL_NULL_V_MSG(scr, Ref<Resource>(), "Could not create C# script '" + real_path + "'."); } else { scr = Ref<CSharpScript>(memnew(CSharpScript)); } #if defined(DEBUG_ENABLED) || defined(TOOLS_ENABLED) - Error err = scr->load_source_code(p_path); - ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load C# script file '" + p_path + "'."); + Error err = scr->load_source_code(real_path); + ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load C# script file '" + real_path + "'."); #endif // Only one instance of a C# script is allowed to exist. diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 918776a6ba..14a9dc2850 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -60,14 +60,88 @@ class CSharpScript : public Script { friend class CSharpInstance; friend class CSharpLanguage; - friend struct CSharpScriptDepSort; - bool tool = false; - bool global_class = false; - bool abstract_class = false; +public: + struct TypeInfo { + /** + * Name of the C# class. + */ + String class_name; + + /** + * Path to the icon that will be used for this class by the editor. + */ + String icon_path; + + /** + * Script is marked as tool and runs in the editor. + */ + bool is_tool = false; + + /** + * Script is marked as global class and will be registered in the editor. + * Registered classes can be created using certain editor dialogs and + * can be referenced by name from other languages that support the feature. + */ + bool is_global_class = false; + + /** + * Script is declared abstract. + */ + bool is_abstract = false; + + /** + * The C# type that corresponds to this script is a constructed generic type. + * E.g.: `Dictionary<int, string>` + */ + bool is_constructed_generic_type = false; + + /** + * The C# type that corresponds to this script is a generic type definition. + * E.g.: `Dictionary<,>` + */ + bool is_generic_type_definition = false; + + /** + * The C# type that corresponds to this script contains generic type parameters, + * regardless of whether the type parameters are bound or not. + */ + bool is_generic() const { + return is_constructed_generic_type || is_generic_type_definition; + } + + /** + * Check if the script can be instantiated. + * C# types can't be instantiated if they are abstract or contain generic + * type parameters, but a CSharpScript is still created for them. + */ + bool can_instantiate() const { + return !is_abstract && !is_generic_type_definition; + } + }; + +private: + /** + * Contains the C# type information for this script. + */ + TypeInfo type_info; + + /** + * Scripts are valid when the corresponding C# class is found and used + * to extract the script info using the [update_script_class_info] method. + */ bool valid = false; + /** + * Scripts extract info from the C# class in the reload methods but, + * if the reload is not invalidated, then the current extracted info + * is still valid and there's no need to reload again. + */ bool reload_invalidated = false; + /** + * Base script that this script derives from, or null if it derives from a + * native Godot class. + */ Ref<CSharpScript> base_script; HashSet<Object *> instances; @@ -88,9 +162,10 @@ class CSharpScript : public Script { HashSet<ObjectID> pending_replace_placeholders; #endif + /** + * Script source code. + */ String source; - String class_name; - String icon_path; SelfList<CSharpScript> script_list = this; @@ -167,7 +242,7 @@ public: return docs; } virtual String get_class_icon_path() const override { - return icon_path; + return type_info.icon_path; } #endif // TOOLS_ENABLED @@ -185,13 +260,13 @@ public: void get_members(HashSet<StringName> *p_members) override; bool is_tool() const override { - return tool; + return type_info.is_tool; } bool is_valid() const override { return valid; } bool is_abstract() const override { - return abstract_class; + return type_info.is_abstract; } bool inherits_script(const Ref<Script> &p_script) const override; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index 01aafe9c74..d6d8a93e03 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -54,9 +54,7 @@ namespace Godot.SourceGenerators ) .Where(x => // Ignore classes whose name is not the same as the file name - Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name && - // Ignore generic classes - !x.symbol.IsGenericType) + Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name) .GroupBy(x => x.symbol) .ToDictionary(g => g.Key, g => g.Select(x => x.cds)); @@ -160,6 +158,8 @@ namespace Godot.SourceGenerators first = false; sourceBuilder.Append("typeof("); sourceBuilder.Append(qualifiedName); + if (godotClass.Key.IsGenericType) + sourceBuilder.Append($"<{new string(',', godotClass.Key.TypeParameters.Count() - 1)}>"); sourceBuilder.Append(")"); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs index 323a0fb380..b86a2b8b24 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Godot; using GodotTools.Build; @@ -23,9 +24,19 @@ namespace GodotTools.Inspector { foreach (var script in EnumerateScripts(godotObject)) { - if (script is not CSharpScript) continue; + if (script is not CSharpScript) + continue; - if (File.GetLastWriteTime(script.ResourcePath) > BuildManager.LastValidBuildDateTime) + string scriptPath = script.ResourcePath; + if (scriptPath.StartsWith("csharp://")) + { + // This is a virtual path used by generic types, extract the real path. + var scriptPathSpan = scriptPath.AsSpan("csharp://".Length); + scriptPathSpan = scriptPathSpan[..scriptPathSpan.IndexOf(':')]; + scriptPath = $"res://{scriptPathSpan}"; + } + + if (File.GetLastWriteTime(scriptPath) > BuildManager.LastValidBuildDateTime) { AddCustomControl(new InspectorOutOfSyncWarning()); break; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index a78cb0bba9..6c34d7c29d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -18,6 +18,7 @@ namespace Godot.Bridge public delegate* unmanaged<godot_string_name*, IntPtr, IntPtr> ScriptManagerBridge_CreateManagedForGodotObjectBinding; public delegate* unmanaged<IntPtr, IntPtr, godot_variant**, int, godot_bool> ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; public delegate* unmanaged<IntPtr, godot_string_name*, void> ScriptManagerBridge_GetScriptNativeName; + public delegate* unmanaged<godot_string*, godot_string*, godot_string*, godot_string*, void> ScriptManagerBridge_GetGlobalClassName; public delegate* unmanaged<IntPtr, IntPtr, void> ScriptManagerBridge_SetGodotObjectPtr; public delegate* unmanaged<IntPtr, godot_string_name*, godot_variant**, int, godot_bool*, void> ScriptManagerBridge_RaiseEventSignal; public delegate* unmanaged<IntPtr, IntPtr, godot_bool> ScriptManagerBridge_ScriptIsOrInherits; @@ -25,7 +26,7 @@ namespace Godot.Bridge public delegate* unmanaged<godot_string*, godot_ref*, void> ScriptManagerBridge_GetOrCreateScriptBridgeForPath; public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_RemoveScriptBridge; public delegate* unmanaged<IntPtr, godot_bool> ScriptManagerBridge_TryReloadRegisteredScriptWithClass; - public delegate* unmanaged<IntPtr, godot_string*, godot_bool*, godot_bool*, godot_bool*, godot_string*, godot_array*, godot_dictionary*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo; + public delegate* unmanaged<IntPtr, godot_csharp_type_info*, godot_array*, godot_dictionary*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo; public delegate* unmanaged<IntPtr, IntPtr*, godot_bool, godot_bool> ScriptManagerBridge_SwapGCHandleForType; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, godot_string*, void*, int, void>, void> ScriptManagerBridge_GetPropertyInfoList; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, void*, int, void>, void> ScriptManagerBridge_GetPropertyDefaultValues; @@ -60,6 +61,7 @@ namespace Godot.Bridge ScriptManagerBridge_CreateManagedForGodotObjectBinding = &ScriptManagerBridge.CreateManagedForGodotObjectBinding, ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = &ScriptManagerBridge.CreateManagedForGodotObjectScriptInstance, ScriptManagerBridge_GetScriptNativeName = &ScriptManagerBridge.GetScriptNativeName, + ScriptManagerBridge_GetGlobalClassName = &ScriptManagerBridge.GetGlobalClassName, ScriptManagerBridge_SetGodotObjectPtr = &ScriptManagerBridge.SetGodotObjectPtr, ScriptManagerBridge_RaiseEventSignal = &ScriptManagerBridge.RaiseEventSignal, ScriptManagerBridge_ScriptIsOrInherits = &ScriptManagerBridge.ScriptIsOrInherits, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index e344dc84c7..0f271d6547 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Loader; using System.Runtime.Serialization; +using System.Text; using Godot.NativeInterop; namespace Godot.Bridge @@ -29,7 +30,7 @@ namespace Godot.Bridge foreach (var type in typesInAlc.Keys) { if (_scriptTypeBiMap.RemoveByScriptType(type, out IntPtr scriptPtr) && - !_pathTypeBiMap.TryGetScriptPath(type, out _)) + (!_pathTypeBiMap.TryGetScriptPath(type, out string? scriptPath) || scriptPath.StartsWith("csharp://"))) { // For scripts without a path, we need to keep the class qualified name for reloading _scriptDataForReload.TryAdd(scriptPtr, @@ -221,6 +222,71 @@ namespace Godot.Bridge } [UnmanagedCallersOnly] + internal static unsafe void GetGlobalClassName(godot_string* scriptPath, godot_string* outBaseType, godot_string* outIconPath, godot_string* outClassName) + { + // This method must always return the outBaseType for every script, even if the script is + // not a global class. But if the script is not a global class it must return an empty + // outClassName string since it should not have a name. + string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); + Debug.Assert(!string.IsNullOrEmpty(scriptPathStr), "Script path can't be empty."); + + if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) + { + // Script at the given path does not exist, or it's not a C# type. + // This is fine, it may be a path to a generic script and those can't be global classes. + *outClassName = default; + return; + } + + if (outIconPath != null) + { + var iconAttr = scriptType.GetCustomAttributes(inherit: false) + .OfType<IconAttribute>() + .FirstOrDefault(); + + *outIconPath = Marshaling.ConvertStringToNative(iconAttr?.Path); + } + + if (outBaseType != null) + { + bool foundGlobalBaseScript = false; + + Type native = GodotObject.InternalGetClassNativeBase(scriptType); + Type? top = scriptType.BaseType; + + while (top != null && top != native) + { + if (IsGlobalClass(top)) + { + *outBaseType = Marshaling.ConvertStringToNative(top.Name); + foundGlobalBaseScript = true; + break; + } + + top = top.BaseType; + } + if (!foundGlobalBaseScript) + { + *outBaseType = Marshaling.ConvertStringToNative(native.Name); + } + } + + if (!IsGlobalClass(scriptType)) + { + // Scripts that are not global classes should not have a name. + // Return an empty string to prevent the class from being registered + // as a global class in the editor. + *outClassName = default; + return; + } + + *outClassName = Marshaling.ConvertStringToNative(scriptType.Name); + + static bool IsGlobalClass(Type scriptType) => + scriptType.IsDefined(typeof(GlobalClassAttribute), inherit: false); + } + + [UnmanagedCallersOnly] internal static void SetGodotObjectPtr(IntPtr gcHandlePtr, IntPtr newPtr) { try @@ -333,7 +399,7 @@ namespace Godot.Bridge foreach (var type in assembly.GetTypes()) { - if (type.IsNested || type.IsGenericType) + if (type.IsNested) continue; if (!typeOfGodotObject.IsAssignableFrom(type)) @@ -352,9 +418,6 @@ namespace Godot.Bridge { foreach (var type in scriptTypes) { - if (type.IsGenericType) - continue; - LookupScriptForClass(type); } } @@ -422,20 +485,8 @@ namespace Godot.Bridge { try { - lock (_scriptTypeBiMap.ReadWriteLock) - { - if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) - { - string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); - - if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) - return godot_bool.False; - - _scriptTypeBiMap.Add(scriptPtr, scriptType); - } - } - - return godot_bool.True; + string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); + return AddScriptBridgeCore(scriptPtr, scriptPathStr).ToGodotBool(); } catch (Exception e) { @@ -444,6 +495,22 @@ namespace Godot.Bridge } } + private static unsafe bool AddScriptBridgeCore(IntPtr scriptPtr, string scriptPath) + { + lock (_scriptTypeBiMap.ReadWriteLock) + { + if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) + { + if (!_pathTypeBiMap.TryGetScriptType(scriptPath, out Type? scriptType)) + return false; + + _scriptTypeBiMap.Add(scriptPtr, scriptType); + } + } + + return true; + } + [UnmanagedCallersOnly] internal static unsafe void GetOrCreateScriptBridgeForPath(godot_string* scriptPath, godot_ref* outScript) { @@ -455,6 +522,8 @@ namespace Godot.Bridge return; } + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Cannot get or create script for a generic type definition '{scriptType.FullName}'. Path: '{scriptPathStr}'."); + GetOrCreateScriptBridgeForType(scriptType, outScript); } @@ -494,16 +563,51 @@ namespace Godot.Bridge if (_pathTypeBiMap.TryGetScriptPath(scriptType, out scriptPath)) return true; + if (scriptType.IsConstructedGenericType) + { + // If the script type is generic, also try looking for the path of the generic type definition + // since we can use it to create the script. + Type genericTypeDefinition = scriptType.GetGenericTypeDefinition(); + if (_pathTypeBiMap.TryGetGenericTypeDefinitionPath(genericTypeDefinition, out scriptPath)) + return true; + } + CreateScriptBridgeForType(scriptType, outScript); scriptPath = null; return false; } } + static string GetVirtualConstructedGenericTypeScriptPath(Type scriptType, string scriptPath) + { + // Constructed generic types all have the same path which is not allowed by Godot + // (every Resource must have a unique path). So we create a unique "virtual" path + // for each type. + + if (!scriptPath.StartsWith("res://")) + { + throw new ArgumentException("Script path must start with 'res://'.", nameof(scriptPath)); + } + + scriptPath = scriptPath.Substring("res://".Length); + return $"csharp://{scriptPath}:{scriptType}.cs"; + } + if (GetPathOtherwiseGetOrCreateScript(scriptType, outScript, out string? scriptPath)) { // This path is slower, but it's only executed for the first instantiation of the type + if (scriptType.IsConstructedGenericType && !scriptPath.StartsWith("csharp://")) + { + // If the script type is generic it can't be loaded using the real script path. + // Construct a virtual path unique to this constructed generic type and add it + // to the path bimap so they can be found later by their virtual path. + // IMPORTANT: The virtual path must be added to _pathTypeBiMap before the first + // load of the script, otherwise the loaded script won't be added to _scriptTypeBiMap. + scriptPath = GetVirtualConstructedGenericTypeScriptPath(scriptType, scriptPath); + _pathTypeBiMap.Add(scriptPath, scriptType); + } + // This must be done outside the read-write lock, as the script resource loading can lock it using godot_string scriptPathIn = Marshaling.ConvertStringToNative(scriptPath); if (!NativeFuncs.godotsharp_internal_script_load(scriptPathIn, outScript).ToBool()) @@ -514,11 +618,23 @@ namespace Godot.Bridge // with no path, as we do for types without an associated script file. GetOrCreateScriptBridgeForType(scriptType, outScript); } + + if (scriptType.IsConstructedGenericType) + { + // When reloading generic scripts they won't be added to the script bimap because their + // virtual path won't be in the path bimap yet. The current method executes when a derived type + // is trying to get or create the script for their base type. The code above has now added + // the virtual path to the path bimap and loading the script with that path should retrieve + // any existing script, so now we have a chance to make sure it's added to the script bimap. + AddScriptBridgeCore(outScript->Reference, scriptPath); + } } } private static unsafe void CreateScriptBridgeForType(Type scriptType, godot_ref* outScript) { + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}."); + NativeFuncs.godotsharp_internal_new_csharp_script(outScript); IntPtr scriptPtr = outScript->Reference; @@ -605,45 +721,82 @@ namespace Godot.Bridge } } - [UnmanagedCallersOnly] - internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_string* outClassName, - godot_bool* outTool, godot_bool* outGlobal, godot_bool* outAbstract, godot_string* outIconPath, - godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest, - godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript) + private static unsafe void GetScriptTypeInfo(Type scriptType, godot_csharp_type_info* outTypeInfo) { - try + Type native = GodotObject.InternalGetClassNativeBase(scriptType); + + string typeName = scriptType.Name; + if (scriptType.IsGenericType) { - // Performance is not critical here as this will be replaced with source generators. - var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); + var sb = new StringBuilder(); + AppendTypeName(sb, scriptType); + typeName = sb.ToString(); + } - *outClassName = Marshaling.ConvertStringToNative(scriptType.Name); + godot_string className = Marshaling.ConvertStringToNative(typeName); - *outTool = scriptType.GetCustomAttributes(inherit: false) - .OfType<ToolAttribute>() - .Any().ToGodotBool(); + bool isTool = scriptType.IsDefined(typeof(ToolAttribute), inherit: false); - if (!(*outTool).ToBool() && scriptType.IsNested) - { - *outTool = (scriptType.DeclaringType?.GetCustomAttributes(inherit: false) - .OfType<ToolAttribute>() - .Any() ?? false).ToGodotBool(); - } + // If the type is nested and the parent type is a tool script, + // consider the nested type a tool script as well. + if (!isTool && scriptType.IsNested) + { + isTool = scriptType.DeclaringType?.IsDefined(typeof(ToolAttribute), inherit: false) ?? false; + } - if (!(*outTool).ToBool() && scriptType.Assembly.GetName().Name == "GodotTools") - *outTool = godot_bool.True; + // Every script in the GodotTools assembly is a tool script. + if (!isTool && scriptType.Assembly.GetName().Name == "GodotTools") + { + isTool = true; + } - var globalAttr = scriptType.GetCustomAttributes(inherit: false) - .OfType<GlobalClassAttribute>() - .FirstOrDefault(); + bool isGlobalClass = scriptType.IsDefined(typeof(GlobalClassAttribute), inherit: false); - *outGlobal = (globalAttr != null).ToGodotBool(); + var iconAttr = scriptType.GetCustomAttributes(inherit: false) + .OfType<IconAttribute>() + .FirstOrDefault(); - var iconAttr = scriptType.GetCustomAttributes(inherit: false) - .OfType<IconAttribute>() - .FirstOrDefault(); - *outIconPath = Marshaling.ConvertStringToNative(iconAttr?.Path); + godot_string iconPath = Marshaling.ConvertStringToNative(iconAttr?.Path); - *outAbstract = scriptType.IsAbstract.ToGodotBool(); + outTypeInfo->ClassName = className; + outTypeInfo->IconPath = iconPath; + outTypeInfo->IsTool = isTool.ToGodotBool(); + outTypeInfo->IsGlobalClass = isGlobalClass.ToGodotBool(); + outTypeInfo->IsAbstract = scriptType.IsAbstract.ToGodotBool(); + outTypeInfo->IsGenericTypeDefinition = scriptType.IsGenericTypeDefinition.ToGodotBool(); + outTypeInfo->IsConstructedGenericType = scriptType.IsConstructedGenericType.ToGodotBool(); + + static void AppendTypeName(StringBuilder sb, Type type) + { + sb.Append(type.Name); + if (type.IsGenericType) + { + sb.Append('<'); + for (int i = 0; i < type.GenericTypeArguments.Length; i++) + { + Type typeArg = type.GenericTypeArguments[i]; + AppendTypeName(sb, typeArg); + if (i != type.GenericTypeArguments.Length - 1) + { + sb.Append(", "); + } + } + sb.Append('>'); + } + } + } + + [UnmanagedCallersOnly] + internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_csharp_type_info* outTypeInfo, + godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest, godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript) + { + try + { + // Performance is not critical here as this will be replaced with source generators. + var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}."); + + GetScriptTypeInfo(scriptType, outTypeInfo); // Methods @@ -820,11 +973,7 @@ namespace Godot.Bridge catch (Exception e) { ExceptionUtils.LogException(e); - *outClassName = default; - *outTool = godot_bool.False; - *outGlobal = godot_bool.False; - *outAbstract = godot_bool.False; - *outIconPath = default; + *outTypeInfo = default; *outMethodsDest = NativeFuncs.godotsharp_array_new(); *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new(); *outEventSignalsDest = NativeFuncs.godotsharp_dictionary_new(); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs index 7fa3498b92..1ec1a75516 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; @@ -19,6 +20,8 @@ public static partial class ScriptManagerBridge { // TODO: What if this is called while unloading a load context, but after we already did cleanup in preparation for unloading? + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"A generic type definition must never be added to the script type map. Type: {scriptType}."); + _scriptTypeMap.Add(scriptPtr, scriptType); _typeScriptMap.Add(scriptType, scriptPtr); @@ -85,10 +88,29 @@ public static partial class ScriptManagerBridge [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetScriptType(string scriptPath, [MaybeNullWhen(false)] out Type scriptType) => - _pathTypeMap.TryGetValue(scriptPath, out scriptType); + // This must never return true for a generic type definition, we only consider script types + // the types that can be attached to a Node/Resource (non-generic or constructed generic types). + _pathTypeMap.TryGetValue(scriptPath, out scriptType) && !scriptType.IsGenericTypeDefinition; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) + { + if (scriptType.IsGenericTypeDefinition) + { + // This must never return true for a generic type definition, we only consider script types + // the types that can be attached to a Node/Resource (non-generic or constructed generic types). + scriptPath = null; + return false; + } + + return _typePathMap.TryGetValue(scriptType, out scriptPath); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) => - _typePathMap.TryGetValue(scriptType, out scriptPath); + public bool TryGetGenericTypeDefinitionPath(Type genericTypeDefinition, [MaybeNullWhen(false)] out string scriptPath) + { + Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); + return _typePathMap.TryGetValue(genericTypeDefinition, out scriptPath); + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs index c806263edb..a1a011e0f0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs @@ -105,6 +105,61 @@ namespace Godot.NativeInterop } } + [StructLayout(LayoutKind.Sequential)] + // ReSharper disable once InconsistentNaming + public ref struct godot_csharp_type_info + { + private godot_string _className; + private godot_string _iconPath; + private godot_bool _isTool; + private godot_bool _isGlobalClass; + private godot_bool _isAbstract; + private godot_bool _isConstructedGenericType; + private godot_bool _isGenericTypeDefinition; + + public godot_string ClassName + { + readonly get => _className; + set => _className = value; + } + + public godot_string IconPath + { + readonly get => _iconPath; + set => _iconPath = value; + } + + public godot_bool IsTool + { + readonly get => _isTool; + set => _isTool = value; + } + + public godot_bool IsGlobalClass + { + readonly get => _isGlobalClass; + set => _isGlobalClass = value; + } + + public godot_bool IsAbstract + { + readonly get => _isAbstract; + set => _isAbstract = value; + } + + public godot_bool IsConstructedGenericType + { + readonly get => _isConstructedGenericType; + set => _isConstructedGenericType = value; + } + + public godot_bool IsGenericTypeDefinition + { + readonly get => _isGenericTypeDefinition; + set => _isGenericTypeDefinition = value; + } + } + [StructLayout(LayoutKind.Sequential, Pack = 8)] // ReSharper disable once InconsistentNaming public ref struct godot_variant diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp index ef4e32e4a7..145f4cee90 100644 --- a/modules/mono/mono_gd/gd_mono_cache.cpp +++ b/modules/mono/mono_gd/gd_mono_cache.cpp @@ -59,6 +59,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectBinding); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectScriptInstance); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetScriptNativeName); + CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetGlobalClassName); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, SetGodotObjectPtr); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, RaiseEventSignal); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, ScriptIsOrInherits); diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index dac8cdcaef..46e9ab10cb 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -84,6 +84,7 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_CreateManagedForGodotObjectBinding = GCHandleIntPtr(GD_CLR_STDCALL *)(const StringName *, Object *); using FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = bool(GD_CLR_STDCALL *)(const CSharpScript *, Object *, const Variant **, int32_t); using FuncScriptManagerBridge_GetScriptNativeName = void(GD_CLR_STDCALL *)(const CSharpScript *, StringName *); + using FuncScriptManagerBridge_GetGlobalClassName = void(GD_CLR_STDCALL *)(const String *, String *, String *, String *); using FuncScriptManagerBridge_SetGodotObjectPtr = void(GD_CLR_STDCALL *)(GCHandleIntPtr, Object *); using FuncScriptManagerBridge_RaiseEventSignal = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int32_t, bool *); using FuncScriptManagerBridge_ScriptIsOrInherits = bool(GD_CLR_STDCALL *)(const CSharpScript *, const CSharpScript *); @@ -91,7 +92,7 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_GetOrCreateScriptBridgeForPath = void(GD_CLR_STDCALL *)(const String *, Ref<CSharpScript> *); using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *); using FuncScriptManagerBridge_TryReloadRegisteredScriptWithClass = bool(GD_CLR_STDCALL *)(const CSharpScript *); - using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, String *, bool *, bool *, bool *, String *, Array *, Dictionary *, Dictionary *, Ref<CSharpScript> *); + using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, CSharpScript::TypeInfo *, Array *, Dictionary *, Dictionary *, Ref<CSharpScript> *); using FuncScriptManagerBridge_SwapGCHandleForType = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr *, bool); using FuncScriptManagerBridge_GetPropertyInfoList = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyInfoList_Add); using FuncScriptManagerBridge_GetPropertyDefaultValues = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add); @@ -120,6 +121,7 @@ struct ManagedCallbacks { FuncScriptManagerBridge_CreateManagedForGodotObjectBinding ScriptManagerBridge_CreateManagedForGodotObjectBinding; FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; FuncScriptManagerBridge_GetScriptNativeName ScriptManagerBridge_GetScriptNativeName; + FuncScriptManagerBridge_GetGlobalClassName ScriptManagerBridge_GetGlobalClassName; FuncScriptManagerBridge_SetGodotObjectPtr ScriptManagerBridge_SetGodotObjectPtr; FuncScriptManagerBridge_RaiseEventSignal ScriptManagerBridge_RaiseEventSignal; FuncScriptManagerBridge_ScriptIsOrInherits ScriptManagerBridge_ScriptIsOrInherits; |
