From c702d7b2ab67286fbbcd0b27fa38c1441291e1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20St=C4=99pie=C5=84?= Date: Sat, 22 Aug 2026 15:55:09 +0200 Subject: [PATCH] feat: add reusable node export validation --- .../MissingRequiredExportException.cs | 17 +++++ .../MissingRequiredExportException.cs.uid | 1 + .../NodeExportValidationExtensions.cs | 73 +++++++++++++++++++ .../NodeExportValidationExtensions.cs.uid | 1 + 4 files changed, 92 insertions(+) create mode 100644 src/Godot/Exceptions/MissingRequiredExportException.cs create mode 100644 src/Godot/Exceptions/MissingRequiredExportException.cs.uid create mode 100644 src/Godot/Extensions/NodeExportValidationExtensions.cs create mode 100644 src/Godot/Extensions/NodeExportValidationExtensions.cs.uid diff --git a/src/Godot/Exceptions/MissingRequiredExportException.cs b/src/Godot/Exceptions/MissingRequiredExportException.cs new file mode 100644 index 0000000..36888dd --- /dev/null +++ b/src/Godot/Exceptions/MissingRequiredExportException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Game.Godot.Exceptions; + +/// +/// Thrown when a node's [Export]ed Node reference is unassigned. +/// +public sealed class MissingRequiredExportException(string nodeName, IReadOnlyList missingExports) + : Exception(BuildMessage(nodeName, missingExports)) +{ + public string NodeName { get; } = nodeName; + public IReadOnlyList MissingExports { get; } = missingExports; + + private static string BuildMessage(string nodeName, IReadOnlyList missingExports) + => $"{nodeName}: missing required exports:\n- {string.Join("\n- ", missingExports)}\nAssign them in the editor Inspector."; +} diff --git a/src/Godot/Exceptions/MissingRequiredExportException.cs.uid b/src/Godot/Exceptions/MissingRequiredExportException.cs.uid new file mode 100644 index 0000000..1fb3a80 --- /dev/null +++ b/src/Godot/Exceptions/MissingRequiredExportException.cs.uid @@ -0,0 +1 @@ +uid://diqdaysdyp3jf diff --git a/src/Godot/Extensions/NodeExportValidationExtensions.cs b/src/Godot/Extensions/NodeExportValidationExtensions.cs new file mode 100644 index 0000000..ad3286f --- /dev/null +++ b/src/Godot/Extensions/NodeExportValidationExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Game.Godot.Exceptions; +using Godot; + +namespace Game.Godot.Extensions; + +/// +/// Validates that a node's [Export] fields/properties pointing to other nodes are actually assigned. +/// +public static class NodeExportValidationExtensions +{ + // Tells which members to include when scanning for [Export] fields/properties + private const BindingFlags MemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + private static void DisableProcessing(Node node) + { + node.SetProcess(false); + node.SetPhysicsProcess(false); + node.SetProcessInput(false); + node.SetProcessUnhandledInput(false); + } + + private static string[] GetMissingExportWarnings(Node node) + { + Type nodeType = node.GetType(); + + IEnumerable missingFields = nodeType + .GetFields(MemberFlags) + .Where(field => field.GetCustomAttribute() is not null) + .Where(field => typeof(Node).IsAssignableFrom(field.FieldType)) + .Where(field => field.GetValue(node) is null) + .Select(field => $"{field.Name} ({field.FieldType.Name}) is not assigned."); + + IEnumerable missingProperties = nodeType + .GetProperties(MemberFlags) + .Where(property => property.GetCustomAttribute() is not null) + .Where(property => typeof(Node).IsAssignableFrom(property.PropertyType)) + .Where(property => property.GetValue(node) is null) + .Select(property => $"{property.Name} ({property.PropertyType.Name}) is not assigned."); + + return missingFields.Concat(missingProperties).ToArray(); + } + + /// + /// Throws if any [Export]ed Node field on this node is unassigned. + /// + /// Node to check. + /// One or more exports are missing. + /// + /// + /// public override void _Ready() + /// { + /// this.AssertRequiredExports(); + /// // rest of initialization + /// } + /// + /// + public static void AssertRequiredExports(this Node node) + { + List missing = GetMissingExportWarnings(node).ToList(); + if (missing.Count == 0) + { + return; + } + + DisableProcessing(node); + + throw new MissingRequiredExportException(node.Name, missing); + } +} diff --git a/src/Godot/Extensions/NodeExportValidationExtensions.cs.uid b/src/Godot/Extensions/NodeExportValidationExtensions.cs.uid new file mode 100644 index 0000000..d664e2b --- /dev/null +++ b/src/Godot/Extensions/NodeExportValidationExtensions.cs.uid @@ -0,0 +1 @@ +uid://d3twapljpp1ln