feat: add reusable node export validation

This commit is contained in:
2026-08-22 15:55:09 +02:00
parent ef6b34bcbd
commit c702d7b2ab
4 changed files with 92 additions and 0 deletions
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace Game.Godot.Exceptions;
/// <summary>
/// Thrown when a node's [Export]ed Node reference is unassigned.
/// </summary>
public sealed class MissingRequiredExportException(string nodeName, IReadOnlyList<string> missingExports)
: Exception(BuildMessage(nodeName, missingExports))
{
public string NodeName { get; } = nodeName;
public IReadOnlyList<string> MissingExports { get; } = missingExports;
private static string BuildMessage(string nodeName, IReadOnlyList<string> missingExports)
=> $"{nodeName}: missing required exports:\n- {string.Join("\n- ", missingExports)}\nAssign them in the editor Inspector.";
}
@@ -0,0 +1 @@
uid://diqdaysdyp3jf
@@ -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;
/// <summary>
/// Validates that a node's [Export] fields/properties pointing to other nodes are actually assigned.
/// </summary>
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<string> missingFields = nodeType
.GetFields(MemberFlags)
.Where(field => field.GetCustomAttribute<ExportAttribute>() 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<string> missingProperties = nodeType
.GetProperties(MemberFlags)
.Where(property => property.GetCustomAttribute<ExportAttribute>() 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();
}
/// <summary>
/// Throws if any [Export]ed Node field on this node is unassigned.
/// </summary>
/// <param name="node">Node to check.</param>
/// <exception cref="MissingRequiredExportException">One or more exports are missing.</exception>
/// <example>
/// <code>
/// public override void _Ready()
/// {
/// this.AssertRequiredExports();
/// // rest of initialization
/// }
/// </code>
/// </example>
public static void AssertRequiredExports(this Node node)
{
List<string> missing = GetMissingExportWarnings(node).ToList();
if (missing.Count == 0)
{
return;
}
DisableProcessing(node);
throw new MissingRequiredExportException(node.Name, missing);
}
}
@@ -0,0 +1 @@
uid://d3twapljpp1ln