diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index 7ce92989f7f7..4fabe3ae7d98 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -885,6 +885,8 @@ Error ResourceLoaderBinary::load() { res->set_edited(false); #endif + res->notification(Object::NOTIFICATION_EXPORT_ASSIGNED); + if (progress) { *progress = (i + 1) / float(internal_resources.size()); } @@ -1252,6 +1254,7 @@ Ref ResourceFormatLoaderBinary::load(const String &p_path, const Strin if (err) { return Ref(); } + return loader.resource; } diff --git a/core/object/object.h b/core/object/object.h index 1fa64e68556d..1b4c6907a177 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -847,6 +847,8 @@ class Object { NOTIFICATION_EXTENSION_RELOADED = 2, // Internal notification to send after NOTIFICATION_PREDELETE, not bound to scripting. NOTIFICATION_PREDELETE_CLEANUP = 3, + // Notification sent when all of the exported variables have been assigned after resource load or scene instantiation. + NOTIFICATION_EXPORT_ASSIGNED = 4, }; /* TYPE API */ diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 0372ada17c6f..8d9151f74ffc 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -1950,6 +1950,11 @@ void CSharpInstance::notification(int p_notification, bool p_reversed) { gchandle.get_intptr(), /* okIfNull */ false); return; + } else if (p_notification == Object::NOTIFICATION_EXPORT_ASSIGNED) { + // When NOTIFICATION_EXPORT_ASSIGNED is sent, we take the chance to validate exported properties. + // This notification is sent after all the exported properties of a Resource or PackedScene instance + // have been assigned, which makes it a good point to all the validation logic. + GDMonoCache::managed_callbacks.CSharpInstanceBridge_ValidateExports(gchandle.get_intptr()); } _call_notification(p_notification, p_reversed); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index c80747695f7e..175336d8ab0f 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; namespace Godot.SourceGenerators { @@ -16,6 +17,12 @@ public static bool TryGetGlobalAnalyzerProperty( ) => context.AnalyzerConfigOptions.GlobalOptions .TryGetValue("build_property." + property, out value); + public static bool TryGetGlobalAnalyzerProperty( + this SuppressionAnalysisContext context, string property, out string? value + ) => context.Options.AnalyzerConfigOptionsProvider.GlobalOptions + .TryGetValue("build_property." + property, out value); + + public static bool AreGodotSourceGeneratorsDisabled(this GeneratorExecutionContext context) => context.TryGetGlobalAnalyzerProperty("GodotSourceGenerators", out string? toggle) && toggle != null && @@ -32,6 +39,16 @@ public static bool IsGodotSourceGeneratorDisabled(this GeneratorExecutionContext disabledGenerators != null && disabledGenerators.Split(';').Contains(generatorName)); + public static bool IsGodotEnableExportNullChecks(this GeneratorExecutionContext context) + => context.TryGetGlobalAnalyzerProperty("GodotEnableExportNullChecks", out string? toggle) && + toggle != null && + toggle.Equals("true", StringComparison.OrdinalIgnoreCase); + + public static bool IsGodotEnableExportNullChecks(this SuppressionAnalysisContext context) + => context.TryGetGlobalAnalyzerProperty("GodotEnableExportNullChecks", out string? toggle) && + toggle != null && + toggle.Equals("true", StringComparison.OrdinalIgnoreCase); + public static bool InheritsFrom(this ITypeSymbol? symbol, string assemblyName, string typeFullName) { while (symbol != null) diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props index 56c51159d086..1c1ef7746494 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.props @@ -6,5 +6,6 @@ + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index f5f51722b40c..9f99d04729cd 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -23,6 +23,74 @@ INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName) } } + /// + /// Checks if the given type symbol is a StringName type from GodotSharp. + /// + public static bool IsStringNameType(ITypeSymbol typeSymbol) + { + return typeSymbol is INamedTypeSymbol namedType && + namedType.ContainingAssembly?.Name == "GodotSharp" && + namedType.ContainingNamespace?.Name == "Godot" && + namedType.Name == "StringName"; + } + + /// + /// Checks if the given type symbol is a NodePath type from GodotSharp. + /// + public static bool IsNodePathType(ITypeSymbol typeSymbol) + { + return typeSymbol is INamedTypeSymbol namedType && + namedType.ContainingAssembly?.Name == "GodotSharp" && + namedType.ContainingNamespace?.Name == "Godot" && + namedType.Name == "NodePath"; + } + + /// + /// Checks if the given type symbol is a packed array (C# array of Godot-compatible primitive or struct types). + /// + public static bool IsPackedArrayType(ITypeSymbol typeSymbol) + { + if (typeSymbol.TypeKind != TypeKind.Array) + return false; + + var arrayType = (IArrayTypeSymbol)typeSymbol; + if (arrayType.Rank != 1) + return false; + + var elementType = arrayType.ElementType; + + // Check for primitive packed arrays + switch (elementType.SpecialType) + { + case SpecialType.System_Byte: + case SpecialType.System_Int32: + case SpecialType.System_Int64: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_String: + return true; + } + + // Check for Godot struct packed arrays + if (elementType.ContainingAssembly?.Name == "GodotSharp" && + elementType.ContainingNamespace?.Name == "Godot") + { + switch (elementType.Name) + { + case "Vector2": + case "Vector3": + case "Vector4": + case "Color": + case "StringName": + case "NodePath": + case "Rid": + return true; + } + } + + return false; + } + public static VariantType? ConvertMarshalTypeToVariantType(MarshalType marshalType) => marshalType switch { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/NullableUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/NullableUtils.cs new file mode 100644 index 000000000000..9b0b368426cb --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/NullableUtils.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + internal static class NullableUtils + { + internal static bool IsNullableContextEnabledForSymbol(ISymbol symbol, Func semanticModelProvider) + { + // Get the syntax reference for the symbol declaration + var syntaxReference = symbol.DeclaringSyntaxReferences.FirstOrDefault(); + if (syntaxReference == null) + return false; + + var syntaxTree = syntaxReference.SyntaxTree; + var syntaxNode = syntaxReference.GetSyntax(); + + // Get the nullable context options at the declaration location + var semanticModel = semanticModelProvider(syntaxTree); + var nullableContext = semanticModel.GetNullableContext(syntaxNode.SpanStart); + + // Check if nullable reference types are enabled (either as warnings or errors) + return (nullableContext & NullableContext.Enabled) != 0; + } + + internal static bool IsExportedNonNullableGodotType(ISymbol memberSymbol, ITypeSymbol memberType, Func semanticModelProvider, bool requireNullableContext = true) + { + // Check if the member has the [Export] attribute + bool isExported = memberSymbol.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false); + + if (!isExported) + return false; + + // Check if it's a reference type and check nullable annotation + if (!memberType.IsReferenceType) + return false; + + // Check if member has nullable context enabled (either via #nullable or project settings) + // This check can be skipped for the suppressor when we just need to identify exported Godot types + if (requireNullableContext && !IsNullableContextEnabledForSymbol(memberSymbol, semanticModelProvider)) + return false; + + // If the type is nullable annotated (e.g., Node?), skip it + if (memberType.NullableAnnotation == NullableAnnotation.Annotated) + return false; + + // Check for string type + if (memberType.SpecialType == SpecialType.System_String) + return true; + + // Check for packed arrays (System arrays of Godot-compatible types) + if (MarshalUtils.IsPackedArrayType(memberType)) + return true; + + // Check if the type is a Godot compatible class type (Node, Resource, or derived) + // This check must come after array check since arrays are not INamedTypeSymbol + if (memberType is not INamedTypeSymbol namedType) + return false; + + // Check if the type inherits from Node or Resource + bool isNodeOrResource = namedType.InheritsFrom("GodotSharp", GodotClasses.Node) || + namedType.InheritsFrom("GodotSharp", "Godot.Resource"); + + if (isNodeOrResource) + return true; + + // Check if the type is Godot.Collections.Array or Dictionary (including generic variations) + string fullTypeName = namedType.ConstructedFrom.ToString(); + bool isGodotCollection = fullTypeName == "Godot.Collections.Array" || + fullTypeName == "Godot.Collections.Array" || + fullTypeName == "Godot.Collections.Dictionary" || + fullTypeName == "Godot.Collections.Dictionary"; + + if (isGodotCollection) + return true; + + // Check for StringName and NodePath + if (MarshalUtils.IsStringNameType(namedType) || MarshalUtils.IsNodePathType(namedType)) + return true; + + return false; + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs index 7cbab34c5b5c..6543789034e0 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; namespace Godot.SourceGenerators @@ -19,6 +21,8 @@ public void Execute(GeneratorExecutionContext context) if (context.IsGodotSourceGeneratorDisabled("ScriptMethods")) return; + bool enableExportNullChecks = context.IsGodotEnableExportNullChecks(); + INamedTypeSymbol[] godotClasses = context .Compilation.SyntaxTrees .SelectMany(tree => @@ -50,7 +54,7 @@ public void Execute(GeneratorExecutionContext context) foreach (var godotClass in godotClasses) { - VisitGodotScriptClass(context, typeCache, godotClass); + VisitGodotScriptClass(context, typeCache, godotClass, enableExportNullChecks); } } } @@ -72,7 +76,8 @@ public int GetHashCode(GodotMethodData obj) private static void VisitGodotScriptClass( GeneratorExecutionContext context, MarshalUtils.TypeCache typeCache, - INamedTypeSymbol symbol + INamedTypeSymbol symbol, + bool enableExportNullChecks ) { INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; @@ -134,6 +139,39 @@ void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) .Distinct(new MethodOverloadEqualityComparer()) .ToArray(); + // Collect exported properties/fields for null checks if feature is enabled + List<(string Name, ITypeSymbol Type)>? exportedNonNullableGodotTypes = null; + if (enableExportNullChecks) + { + exportedNonNullableGodotTypes = new List<(string, ITypeSymbol)>(); + + // Collect exported properties + var propertySymbols = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) + .Cast(); + + foreach (var property in propertySymbols) + { + if (NullableUtils.IsExportedNonNullableGodotType(property, property.Type, syntaxTree => context.Compilation.GetSemanticModel(syntaxTree))) + { + exportedNonNullableGodotTypes.Add((property.Name, property.Type)); + } + } + + // Collect exported fields + var fieldSymbols = members + .Where(s => !s.IsStatic && s is { Kind: SymbolKind.Field, IsImplicitlyDeclared: false }) + .Cast(); + + foreach (var field in fieldSymbols) + { + if (NullableUtils.IsExportedNonNullableGodotType(field, field.Type, syntaxTree => context.Compilation.GetSemanticModel(syntaxTree))) + { + exportedNonNullableGodotTypes.Add((field.Name, field.Type)); + } + } + } + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); source.Append(" /// \n") @@ -203,6 +241,20 @@ void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) source.Append("#pragma warning restore CS0109\n"); + // Generate ValidateExportedProperties + + if (exportedNonNullableGodotTypes is { Count: > 0 }) + { + source.Append(" /// \n"); + source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n"); + source.Append(" protected override void ValidateExportedProperties()\n {\n"); + + GenerateNullChecksForValidation(source, exportedNonNullableGodotTypes); + + source.Append(" base.ValidateExportedProperties();\n"); + source.Append(" }\n"); + } + // Generate InvokeGodotClassMethod if (godotClassMethods.Length > 0) @@ -419,6 +471,50 @@ StringBuilder source source.Append(") {\n return true;\n }\n"); } + private static void GenerateNullChecksForValidation( + StringBuilder source, + List<(string Name, ITypeSymbol Type)> exportedNonNullableGodotTypes + ) + { + foreach (var (memberName, memberType) in exportedNonNullableGodotTypes) + { + // Check if this is a string type + bool isString = memberType.SpecialType == SpecialType.System_String; + + // Check if this is a StringName type + bool isStringName = MarshalUtils.IsStringNameType(memberType); + + // Check if this is a packed array type + bool isPackedArray = MarshalUtils.IsPackedArrayType(memberType); + + source.Append(" "); + + if (isString) + { + // For string, initialize with string.Empty instead of throwing + source.Append($"this.{memberName} ??= string.Empty;\n"); + } + else if (isStringName) + { + // For StringName, initialize with new StringName() instead of throwing + source.Append($"this.{memberName} ??= new global::Godot.StringName();\n"); + } + else if (isPackedArray) + { + // For packed arrays, initialize with Array.Empty instead of throwing + source.Append($"this.{memberName} ??= global::System.Array.Empty<"); + + var arrayType = (IArrayTypeSymbol)memberType; + source.Append(arrayType.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + source.Append(">();\n"); + } + else + { + // For other types (Node, Resource, NodePath, Collections), throw NullReferenceException + source.Append($"global::System.ArgumentNullException.ThrowIfNull(this.{memberName}, \"{memberName}\");\n"); + } + } + } private static void GenerateMethodInvoker( GodotMethodData method, StringBuilder source @@ -471,4 +567,117 @@ StringBuilder source source.Append(" }\n"); } } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class NullabilitySuppressor : DiagnosticSuppressor + { + private static readonly SuppressionDescriptor _suppressionMessage = + new( + id: "GDSPR001", + suppressedDiagnosticId: "CS8618", + justification: "Member's nullability has been checked by Godot when initialized." + ); + + public override ImmutableArray SupportedSuppressions => + ImmutableArray.Create(_suppressionMessage); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + // Check if the feature is enabled + var enableExportNullChecks = context.IsGodotEnableExportNullChecks(); + + if (!enableExportNullChecks) + return; + + foreach (var diagnostic in context.ReportedDiagnostics) + { + if (diagnostic.Id != "CS8618") + continue; + + // Get the symbol that triggered the diagnostic + var syntaxTree = diagnostic.Location.SourceTree; + if (syntaxTree == null) + continue; + + var semanticModel = context.GetSemanticModel(syntaxTree); + var root = syntaxTree.GetRoot(context.CancellationToken); + var node = root.FindNode(diagnostic.Location.SourceSpan); + + // Check if the diagnostic is on a constructor + if (node is ConstructorDeclarationSyntax ctorDecl) + { + var ctorSymbol = semanticModel.GetDeclaredSymbol(ctorDecl, context.CancellationToken); + if (ctorSymbol == null) + continue; + + var containingType = ctorSymbol.ContainingType; + if (containingType == null || !containingType.InheritsFrom("GodotSharp", GodotClasses.GodotObject)) + continue; + + // Check if the containing type has any exported non-nullable Godot types + // that would trigger CS8618 on the constructor + bool hasExportedNonNullableMembers = containingType.GetMembers() + .Where(m => m is IPropertySymbol or IFieldSymbol) + .Any(m => + { + ITypeSymbol? memberType = m switch + { + IPropertySymbol prop => prop.Type, + IFieldSymbol field => field.Type, + _ => null + }; + + return memberType != null && NullableUtils.IsExportedNonNullableGodotType(m, memberType, syntaxTree2 => context.GetSemanticModel(syntaxTree2), requireNullableContext: false); + }); + + if (hasExportedNonNullableMembers) + { + context.ReportSuppression(Suppression.Create(_suppressionMessage, diagnostic)); + } + + continue; + } + + // Try to get the member symbol (property or field) + ISymbol? memberSymbol = null; + + if (node is PropertyDeclarationSyntax propertyDecl) + { + memberSymbol = semanticModel.GetDeclaredSymbol(propertyDecl, context.CancellationToken); + } + else if (node is VariableDeclaratorSyntax variableDecl) + { + memberSymbol = semanticModel.GetDeclaredSymbol(variableDecl, context.CancellationToken); + } + + if (memberSymbol == null) + continue; + + // Get the type symbol + ITypeSymbol? memberType = null; + if (memberSymbol is IPropertySymbol propertySymbol) + { + memberType = propertySymbol.Type; + } + else if (memberSymbol is IFieldSymbol fieldSymbol) + { + memberType = fieldSymbol.Type; + } + + if (memberType == null) + continue; + + // Check if this is an exported non-nullable Godot type + if (NullableUtils.IsExportedNonNullableGodotType(memberSymbol, memberType, syntaxTree2 => context.GetSemanticModel(syntaxTree2), requireNullableContext: false)) + { + // Check if the containing type is a Godot script class + var containingType = memberSymbol.ContainingType; + if (containingType != null && containingType.InheritsFrom("GodotSharp", GodotClasses.GodotObject)) + { + context.ReportSuppression(Suppression.Create(_suppressionMessage, diagnostic)); + } + } + } + } + } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index d17b7327822c..0aa8eaf893bd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -275,5 +275,23 @@ internal static unsafe void DeserializeState( ExceptionUtils.LogException(e); } } + + [UnmanagedCallersOnly] + internal static void ValidateExports(IntPtr godotObjectGCHandle) + { + try + { + var godotObject = (GodotObject)GCHandle.FromIntPtr(godotObjectGCHandle).Target; + + if (godotObject == null) + return; + + godotObject.ValidateExportedProperties(); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index b0ff041fa49a..1d16a8a31cd5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -40,6 +40,7 @@ public unsafe struct ManagedCallbacks public delegate* unmanaged CSharpInstanceBridge_HasMethodUnknownParams; public delegate* unmanaged CSharpInstanceBridge_SerializeState; public delegate* unmanaged CSharpInstanceBridge_DeserializeState; + public delegate* unmanaged CSharpInstanceBridge_ValidateExports; public delegate* unmanaged GCHandleBridge_FreeGCHandle; public delegate* unmanaged GCHandleBridge_GCHandleIsTargetCollectible; public delegate* unmanaged DebuggingUtils_GetCurrentStackInfo; @@ -84,6 +85,7 @@ public static ManagedCallbacks Create() CSharpInstanceBridge_HasMethodUnknownParams = &CSharpInstanceBridge.HasMethodUnknownParams, CSharpInstanceBridge_SerializeState = &CSharpInstanceBridge.SerializeState, CSharpInstanceBridge_DeserializeState = &CSharpInstanceBridge.DeserializeState, + CSharpInstanceBridge_ValidateExports = &CSharpInstanceBridge.ValidateExports, GCHandleBridge_FreeGCHandle = &GCHandleBridge.FreeGCHandle, GCHandleBridge_GCHandleIsTargetCollectible = &GCHandleBridge.GCHandleIsTargetCollectible, DebuggingUtils_GetCurrentStackInfo = &DebuggingUtils.GetCurrentStackInfo, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs index 08aea77e84c7..7714c3f98264 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs @@ -349,5 +349,15 @@ protected internal virtual void SaveGodotObjectData(GodotSerializationInfo info) protected internal virtual void RestoreGodotObjectData(GodotSerializationInfo info) { } + + /// + /// Validates the exported properties of this instance. + /// This method is implemented by the Godot Source Generators for types that have not-null exported members + /// with Nullable Reference Types enabled. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected internal virtual void ValidateExportedProperties() + { + } } } diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp index 5292bcd1ead7..01cac754ab38 100644 --- a/modules/mono/mono_gd/gd_mono_cache.cpp +++ b/modules/mono/mono_gd/gd_mono_cache.cpp @@ -81,6 +81,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, HasMethodUnknownParams); CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, SerializeState); CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, DeserializeState); + CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, ValidateExports); CHECK_CALLBACK_NOT_NULL(GCHandleBridge, FreeGCHandle); CHECK_CALLBACK_NOT_NULL(GCHandleBridge, GCHandleIsTargetCollectible); CHECK_CALLBACK_NOT_NULL(DebuggingUtils, GetCurrentStackInfo); diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index bbf32e3c77d8..a267aba7a6dc 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -103,6 +103,7 @@ struct ManagedCallbacks { using FuncCSharpInstanceBridge_HasMethodUnknownParams = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *); using FuncCSharpInstanceBridge_SerializeState = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Dictionary *, const Dictionary *); using FuncCSharpInstanceBridge_DeserializeState = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Dictionary *, const Dictionary *); + using FuncCSharpInstanceBridge_ValidateExports = void(GD_CLR_STDCALL *)(GCHandleIntPtr); using FuncGCHandleBridge_FreeGCHandle = void(GD_CLR_STDCALL *)(GCHandleIntPtr); using FuncGCHandleBridge_GCHandleIsTargetCollectible = bool(GD_CLR_STDCALL *)(GCHandleIntPtr); using FuncDebuggingUtils_GetCurrentStackInfo = void(GD_CLR_STDCALL *)(Vector *); @@ -141,6 +142,7 @@ struct ManagedCallbacks { FuncCSharpInstanceBridge_HasMethodUnknownParams CSharpInstanceBridge_HasMethodUnknownParams; FuncCSharpInstanceBridge_SerializeState CSharpInstanceBridge_SerializeState; FuncCSharpInstanceBridge_DeserializeState CSharpInstanceBridge_DeserializeState; + FuncCSharpInstanceBridge_ValidateExports CSharpInstanceBridge_ValidateExports; FuncGCHandleBridge_FreeGCHandle GCHandleBridge_FreeGCHandle; FuncGCHandleBridge_GCHandleIsTargetCollectible GCHandleBridge_GCHandleIsTargetCollectible; FuncDebuggingUtils_GetCurrentStackInfo DebuggingUtils_GetCurrentStackInfo; diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index 5f34e86463c0..a7decfe08aec 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -34,6 +34,7 @@ #include "core/io/file_access.h" #include "core/io/missing_resource.h" #include "core/io/resource_loader.h" +#include "core/object/object.h" #include "core/object/script_language.h" #include "core/templates/local_vector.h" #include "core/variant/callable_bind.h" @@ -189,6 +190,8 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { LocalVector deferred_node_paths; + LocalVector pending_notify_nodes; + bool deep_search_warned = false; for (int i = 0; i < nc; i++) { @@ -356,6 +359,7 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { //properties int nprop_count = n.properties.size(); if (nprop_count) { + pending_notify_nodes.push_back(node); const NodeData::Property *nprops = &n.properties[0]; Dictionary missing_resource_properties; @@ -700,6 +704,10 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { } } + for (Node *node : pending_notify_nodes) { + node->notification(Node::NOTIFICATION_EXPORT_ASSIGNED); + } + return ret_nodes[0]; } diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index 4206d13ea975..d7de5d455377 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -33,6 +33,7 @@ #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/io/missing_resource.h" +#include "core/object/object.h" #include "core/object/script_language.h" #include "scene/property_utils.h" @@ -685,6 +686,8 @@ Error ResourceLoaderText::load() { if (!missing_resource_properties.is_empty()) { res->set_meta(META_MISSING_RESOURCES, missing_resource_properties); } + + res->notification(Object::NOTIFICATION_EXPORT_ASSIGNED); } while (true) { @@ -831,6 +834,7 @@ Error ResourceLoaderText::load() { resource->set_meta(META_MISSING_RESOURCES, missing_resource_properties); } + resource->notification(Object::NOTIFICATION_EXPORT_ASSIGNED); error = OK; return error; @@ -1436,7 +1440,8 @@ Ref ResourceFormatLoaderText::load(const String &p_path, const String *r_error = err; } if (err == OK) { - return loader.get_resource(); + Ref resource = loader.get_resource(); + return resource; } else { return Ref(); }