Browse Source

Merge pull request #87890 from raulsntos/dotnet/generics

Improve handling of generic C# types
Rémi Verschelde 1 year ago
parent
commit
efcb23fb6b

+ 30 - 66
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.

+ 84 - 9
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;

+ 3 - 3
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(")");
             }
 

+ 13 - 2
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;

+ 3 - 1
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,

+ 202 - 53
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,
@@ -220,6 +221,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)
         {
@@ -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();

+ 25 - 3
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);
+        }
     }
 }

+ 55 - 0
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

+ 1 - 0
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);

+ 3 - 1
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;