feat: add reusable node export validation
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user