Files
MovementTests/addons/forge/editor/statescript/StatescriptNodeDiscovery.cs
Minimata 1d856fd937
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 26s
Create tag and build when new code gets to main / Export (push) Successful in 5m42s
Replicated the weapon flying tick setup using resources
2026-04-07 16:32:26 +02:00

541 lines
14 KiB
C#

// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Statescript;
using Gamesmiths.Forge.Statescript.Nodes;
using Gamesmiths.Forge.Statescript.Ports;
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
/// <summary>
/// Discovers concrete Statescript node types from loaded assemblies using reflection.
/// </summary>
/// <remarks>
/// Provides port layout information for the editor without requiring node instantiation.
/// </remarks>
internal static class StatescriptNodeDiscovery
{
private static List<NodeTypeInfo>? _cachedNodeTypes;
/// <summary>
/// Gets all discovered concrete node types. Results are cached after first discovery.
/// </summary>
/// <returns>A read-only list of node type info.</returns>
internal static IReadOnlyList<NodeTypeInfo> GetDiscoveredNodeTypes()
{
_cachedNodeTypes ??= DiscoverNodeTypes();
return _cachedNodeTypes;
}
/// <summary>
/// Clears the cached discovery results, forcing re-discovery on next access.
/// </summary>
internal static void InvalidateCache()
{
_cachedNodeTypes = null;
}
/// <summary>
/// Finds the <see cref="NodeTypeInfo"/> for the given runtime type name.
/// </summary>
/// <param name="runtimeTypeName">The full type name stored in the resource.</param>
/// <returns>The matching node type info, or null if not found.</returns>
internal static NodeTypeInfo? FindByRuntimeTypeName(string runtimeTypeName)
{
IReadOnlyList<NodeTypeInfo> types = GetDiscoveredNodeTypes();
for (var i = 0; i < types.Count; i++)
{
if (types[i].RuntimeTypeName == runtimeTypeName)
{
return types[i];
}
}
return null;
}
private static List<NodeTypeInfo> DiscoverNodeTypes()
{
var results = new List<NodeTypeInfo>();
Type actionNodeType = typeof(ActionNode);
Type conditionNodeType = typeof(ConditionNode);
Type stateNodeOpenType = typeof(StateNode<>);
// Scan all loaded assemblies for concrete node types.
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(x => x is not null).ToArray()!;
}
foreach (Type type in types)
{
if (type.IsAbstract || type.IsGenericTypeDefinition)
{
continue;
}
// Skip the built-in Entry/Exit nodes — they are handled separately.
if (type == typeof(EntryNode) || type == typeof(ExitNode))
{
continue;
}
if (actionNodeType.IsAssignableFrom(type))
{
results.Add(BuildNodeTypeInfo(type, StatescriptNodeType.Action));
}
else if (conditionNodeType.IsAssignableFrom(type))
{
results.Add(BuildNodeTypeInfo(type, StatescriptNodeType.Condition));
}
else if (IsConcreteStateNode(type, stateNodeOpenType))
{
results.Add(BuildNodeTypeInfo(type, StatescriptNodeType.State));
}
}
}
results.Sort((a, b) => string.CompareOrdinal(a.DisplayName, b.DisplayName));
return results;
}
private static bool IsConcreteStateNode(Type type, Type stateNodeOpenType)
{
Type? current = type.BaseType;
while (current is not null)
{
if (current.IsGenericType && current.GetGenericTypeDefinition() == stateNodeOpenType)
{
return true;
}
current = current.BaseType;
}
return false;
}
private static NodeTypeInfo BuildNodeTypeInfo(Type type, StatescriptNodeType nodeType)
{
var displayName = FormatDisplayName(type.Name);
var runtimeTypeName = type.FullName!;
// Get constructor parameter names.
var constructorParamNames = GetConstructorParameterNames(type);
// Determine ports and description by instantiating a temporary node.
string[] inputLabels;
string[] outputLabels;
bool[] isSubgraph;
string description;
InputPropertyInfo[] inputPropertiesInfo;
OutputVariableInfo[] outputVariablesInfo;
try
{
Node tempNode = CreateTemporaryNode(type);
inputLabels = GetInputPortLabels(tempNode, nodeType);
outputLabels = GetOutputPortLabels(tempNode, nodeType);
isSubgraph = GetSubgraphFlags(tempNode);
description = tempNode.Description;
inputPropertiesInfo = GetInputPropertiesInfo(tempNode);
outputVariablesInfo = GetOutputVariablesInfo(tempNode);
}
catch
{
// Fallback to default port layout based on base type.
PortLayout[] portLayouts = GetDefaultPortLayout(nodeType);
inputLabels = [.. portLayouts.Select(x => x.InputLabel)];
outputLabels = [.. portLayouts.Select(x => x.OutputLabel)];
isSubgraph = [.. portLayouts.Select(x => x.IsSubgraph)];
description = $"{displayName} node.";
inputPropertiesInfo = [];
outputVariablesInfo = [];
}
return new NodeTypeInfo(
displayName,
runtimeTypeName,
nodeType,
inputLabels,
outputLabels,
isSubgraph,
constructorParamNames,
description,
inputPropertiesInfo,
outputVariablesInfo);
}
private static Node CreateTemporaryNode(Type type)
{
// Try to find the primary constructor or the one with the fewest parameters.
ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (constructors.Length == 0)
{
return (Node)Activator.CreateInstance(type)!;
}
// Sort by parameter count, prefer the fewest.
ConstructorInfo constructor = constructors.OrderBy(x => x.GetParameters().Length).First();
ParameterInfo[] parameters = constructor.GetParameters();
var args = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
Type paramType = parameters[i].ParameterType;
if (paramType == typeof(Forge.Core.StringKey))
{
args[i] = new Forge.Core.StringKey("_placeholder_");
}
else if (paramType == typeof(string))
{
args[i] = string.Empty;
}
else if (paramType.IsValueType)
{
args[i] = Activator.CreateInstance(paramType)!;
}
else
{
args[i] = null!;
}
}
return (Node)constructor.Invoke(args);
}
private static string[] GetConstructorParameterNames(Type type)
{
ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (constructors.Length == 0)
{
return [];
}
// Use the constructor with the most parameters (primary constructor).
ConstructorInfo constructor = constructors.OrderByDescending(x => x.GetParameters().Length).First();
return [.. constructor.GetParameters().Select(x => x.Name ?? string.Empty)];
}
private static string[] GetInputPortLabels(Node node, StatescriptNodeType nodeType)
{
var count = node.InputPorts.Length;
var labels = new string[count];
switch (nodeType)
{
case StatescriptNodeType.Action:
if (count >= 1)
{
labels[0] = "Execute";
}
break;
case StatescriptNodeType.Condition:
if (count >= 1)
{
labels[0] = "Condition";
}
break;
case StatescriptNodeType.State:
if (count >= 1)
{
labels[0] = "Begin";
}
if (count >= 2)
{
labels[1] = "Abort";
}
for (var i = 2; i < count; i++)
{
labels[i] = $"Input {i}";
}
break;
default:
for (var i = 0; i < count; i++)
{
labels[i] = $"Input {i}";
}
break;
}
return labels;
}
private static string[] GetOutputPortLabels(Node node, StatescriptNodeType nodeType)
{
var count = node.OutputPorts.Length;
var labels = new string[count];
switch (nodeType)
{
case StatescriptNodeType.Action:
if (count >= 1)
{
labels[0] = "Done";
}
break;
case StatescriptNodeType.Condition:
if (count >= 1)
{
labels[0] = "True";
}
if (count >= 2)
{
labels[1] = "False";
}
break;
case StatescriptNodeType.State:
if (count >= 1)
{
labels[0] = "OnActivate";
}
if (count >= 2)
{
labels[1] = "OnDeactivate";
}
if (count >= 3)
{
labels[2] = "OnAbort";
}
if (count >= 4)
{
labels[3] = "Subgraph";
}
for (var i = 4; i < count; i++)
{
labels[i] = $"Event {i}";
}
break;
default:
for (var i = 0; i < count; i++)
{
labels[i] = $"Output {i}";
}
break;
}
return labels;
}
private static bool[] GetSubgraphFlags(Node node)
{
var count = node.OutputPorts.Length;
var flags = new bool[count];
for (var i = 0; i < count; i++)
{
flags[i] = node.OutputPorts[i] is SubgraphPort;
}
return flags;
}
private static InputPropertyInfo[] GetInputPropertiesInfo(Node node)
{
var propertiesInfo = new InputPropertyInfo[node.InputProperties.Length];
for (var i = 0; i < node.InputProperties.Length; i++)
{
propertiesInfo[i] = new InputPropertyInfo(
node.InputProperties[i].Label,
node.InputProperties[i].ExpectedType);
}
return propertiesInfo;
}
private static OutputVariableInfo[] GetOutputVariablesInfo(Node node)
{
var variablesInfo = new OutputVariableInfo[node.OutputVariables.Length];
for (var i = 0; i < node.OutputVariables.Length; i++)
{
variablesInfo[i] = new OutputVariableInfo(
node.OutputVariables[i].Label,
node.OutputVariables[i].ValueType,
node.OutputVariables[i].Scope);
}
return variablesInfo;
}
private static PortLayout[] GetDefaultPortLayout(
StatescriptNodeType nodeType)
{
return nodeType switch
{
StatescriptNodeType.Action => [new PortLayout("Execute", "Done", false)],
StatescriptNodeType.Condition => [
new PortLayout("Condition", "True", false),
new PortLayout(string.Empty, "False", false)],
StatescriptNodeType.State => [
new PortLayout("Begin", "OnActivate", false),
new PortLayout("Abort", "OnDeactivate", false),
new PortLayout(string.Empty, "OnAbort", false),
new PortLayout(string.Empty, "Subgraph", true)],
StatescriptNodeType.Entry => throw new NotImplementedException(),
StatescriptNodeType.Exit => throw new NotImplementedException(),
_ => [new PortLayout("Input", "Output", false)],
};
}
private static string FormatDisplayName(string typeName)
{
// Remove common suffixes.
if (typeName.EndsWith("Node", StringComparison.Ordinal))
{
typeName = typeName[..^4];
}
// Insert spaces before capital letters for camelCase names.
var result = new System.Text.StringBuilder();
for (var i = 0; i < typeName.Length; i++)
{
if (i > 0 && char.IsUpper(typeName[i]) && !char.IsUpper(typeName[i - 1]))
{
result.Append(' ');
}
result.Append(typeName[i]);
}
return result.ToString();
}
/// <summary>
/// Describes a discovered concrete node type and its port layout.
/// </summary>
internal sealed class NodeTypeInfo
{
/// <summary>
/// Gets the display name for this node type (e.g., "Timer", "Set Variable", "Expression").
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets the CLR type name used for serialization (typically the type's full name).
/// </summary>
public string RuntimeTypeName { get; }
/// <summary>
/// Gets the node category (Action, Condition, State).
/// </summary>
public StatescriptNodeType NodeType { get; }
/// <summary>
/// Gets the input port labels for this node type.
/// </summary>
public string[] InputPortLabels { get; }
/// <summary>
/// Gets the output port labels for this node type.
/// </summary>
public string[] OutputPortLabels { get; }
/// <summary>
/// Gets whether each output port is a subgraph port.
/// </summary>
public bool[] IsSubgraphPort { get; }
/// <summary>
/// Gets the constructor parameter names for this node type.
/// </summary>
public string[] ConstructorParameterNames { get; }
/// <summary>
/// Gets a brief description for this node type, shown in the Add Node dialog.
/// Read from the <see cref="Node.Description"/> property at discovery time.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the input property declarations for this node type.
/// </summary>
public InputPropertyInfo[] InputPropertiesInfo { get; }
/// <summary>
/// Gets the output variable declarations for this node type.
/// </summary>
public OutputVariableInfo[] OutputVariablesInfo { get; }
public NodeTypeInfo(
string displayName,
string runtimeTypeName,
StatescriptNodeType nodeType,
string[] inputPortLabels,
string[] outputPortLabels,
bool[] isSubgraphPort,
string[] constructorParameterNames,
string description,
InputPropertyInfo[] inputPropertiesInfo,
OutputVariableInfo[] outputVariablesInfo)
{
DisplayName = displayName;
RuntimeTypeName = runtimeTypeName;
NodeType = nodeType;
InputPortLabels = inputPortLabels;
OutputPortLabels = outputPortLabels;
IsSubgraphPort = isSubgraphPort;
ConstructorParameterNames = constructorParameterNames;
Description = description;
InputPropertiesInfo = inputPropertiesInfo;
OutputVariablesInfo = outputVariablesInfo;
}
}
/// <summary>
/// Describes an input property declared by a node type.
/// </summary>
/// <param name="Label">The human-readable label for this input property.</param>
/// <param name="ExpectedType">The type the node expects to read.</param>
/// <param name="IsArray">Whether the input expects an array of values.</param>
internal readonly record struct InputPropertyInfo(string Label, Type ExpectedType, bool IsArray = false);
/// <summary>
/// Describes an output variable declared by a node type.
/// </summary>
/// <param name="Label">The human-readable label for this output variable.</param>
/// <param name="ValueType">The type the node writes.</param>
/// <param name="Scope">The default scope for this output variable.</param>
internal readonly record struct OutputVariableInfo(string Label, Type ValueType, VariableScope Scope);
private record struct PortLayout(string InputLabel, string OutputLabel, bool IsSubgraph);
}
#endif