Files
MovementTests/addons/forge/core/StatescriptGraphBuilder.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

353 lines
9.0 KiB
C#

// Copyright © Gamesmiths Guild.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Gamesmiths.Forge.Core;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript;
using Gamesmiths.Forge.Statescript.Nodes;
using Godot;
using ForgeNode = Gamesmiths.Forge.Statescript.Node;
using GodotVariant = Godot.Variant;
namespace Gamesmiths.Forge.Godot.Core;
/// <summary>
/// Builds a runtime <see cref="Graph"/> from a serialized <see cref="StatescriptGraph"/> resource.
/// Resolves concrete node types from the Forge DLL and other assemblies using reflection and recreates all connections.
/// </summary>
public static class StatescriptGraphBuilder
{
/// <summary>
/// Builds a runtime <see cref="Graph"/> from the given <see cref="StatescriptGraph"/> resource.
/// </summary>
/// <param name="graphResource">The serialized graph resource.</param>
/// <returns>A fully constructed runtime graph ready for execution.</returns>
/// <exception cref="InvalidOperationException">Thrown when a node type cannot be resolved or instantiated.
/// </exception>
public static Graph Build(StatescriptGraph graphResource)
{
var graph = new Graph();
var nodeMap = new Dictionary<string, ForgeNode>();
foreach (StatescriptNode nodeResource in graphResource.Nodes)
{
switch (nodeResource.NodeType)
{
case StatescriptNodeType.Entry:
nodeMap[nodeResource.NodeId] = graph.EntryNode;
break;
case StatescriptNodeType.Exit:
var exitNode = new ExitNode();
graph.AddNode(exitNode);
nodeMap[nodeResource.NodeId] = exitNode;
break;
default:
ForgeNode runtimeNode = InstantiateNode(nodeResource);
graph.AddNode(runtimeNode);
nodeMap[nodeResource.NodeId] = runtimeNode;
break;
}
}
foreach (StatescriptConnection connectionResource in graphResource.Connections)
{
if (!nodeMap.TryGetValue(connectionResource.FromNode, out ForgeNode? fromNode))
{
GD.PushWarning(
$"Statescript: Connection references unknown source node '{connectionResource.FromNode}'.");
continue;
}
if (!nodeMap.TryGetValue(connectionResource.ToNode, out ForgeNode? toNode))
{
GD.PushWarning(
$"Statescript: Connection references unknown target node '{connectionResource.ToNode}'.");
continue;
}
var outputPortIndex = connectionResource.OutputPort;
var inputPortIndex = connectionResource.InputPort;
if (outputPortIndex < 0 || outputPortIndex >= fromNode.OutputPorts.Length)
{
GD.PushWarning(
$"Statescript: Output port index {outputPortIndex} out of range on node " +
$"'{connectionResource.FromNode}'.");
continue;
}
if (inputPortIndex < 0 || inputPortIndex >= toNode.InputPorts.Length)
{
GD.PushWarning(
$"Statescript: Input port index {inputPortIndex} out of range on node " +
$"'{connectionResource.ToNode}'.");
continue;
}
var connection = new Connection(
fromNode.OutputPorts[outputPortIndex],
toNode.InputPorts[inputPortIndex]);
graph.AddConnection(connection);
}
RegisterGraphVariables(graph, graphResource);
BindNodeProperties(graph, graphResource, nodeMap);
ValidateActivationDataProviders(graphResource);
return graph;
}
private static void RegisterGraphVariables(Graph graph, StatescriptGraph graphResource)
{
foreach (StatescriptGraphVariable variable in graphResource.Variables)
{
if (string.IsNullOrEmpty(variable.VariableName))
{
continue;
}
Type clrType = StatescriptVariableTypeConverter.ToSystemType(variable.VariableType);
if (variable.IsArray)
{
var initialValues = new Variant128[variable.InitialArrayValues.Count];
for (var i = 0; i < variable.InitialArrayValues.Count; i++)
{
initialValues[i] = StatescriptVariableTypeConverter.GodotVariantToForge(
variable.InitialArrayValues[i],
variable.VariableType);
}
graph.VariableDefinitions.ArrayVariableDefinitions.Add(
new ArrayVariableDefinition(
new StringKey(variable.VariableName),
initialValues,
clrType));
}
else
{
Variant128 initialValue = StatescriptVariableTypeConverter.GodotVariantToForge(
variable.InitialValue,
variable.VariableType);
graph.VariableDefinitions.VariableDefinitions.Add(
new VariableDefinition(
new StringKey(variable.VariableName),
initialValue,
clrType));
}
}
}
private static void BindNodeProperties(
Graph graph,
StatescriptGraph graphResource,
Dictionary<string, ForgeNode> nodeMap)
{
foreach (StatescriptNode nodeResource in graphResource.Nodes)
{
if (!nodeMap.TryGetValue(nodeResource.NodeId, out ForgeNode? runtimeNode))
{
continue;
}
foreach (StatescriptNodeProperty binding in nodeResource.PropertyBindings)
{
if (binding.Resolver is null)
{
continue;
}
var index = (byte)binding.PropertyIndex;
if (binding.Direction == StatescriptPropertyDirection.Input)
{
if (index >= runtimeNode.InputProperties.Length)
{
GD.PushWarning(
$"Statescript: Input property index {index} out of range on node " +
$"'{nodeResource.NodeId}'.");
continue;
}
binding.Resolver.BindInput(graph, runtimeNode, nodeResource.NodeId, index);
}
else
{
if (index >= runtimeNode.OutputVariables.Length)
{
GD.PushWarning(
$"Statescript: Output variable index {index} out of range on node " +
$"'{nodeResource.NodeId}'.");
continue;
}
binding.Resolver.BindOutput(runtimeNode, index);
}
}
}
}
private static void ValidateActivationDataProviders(StatescriptGraph graphResource)
{
string? firstProvider = null;
foreach (StatescriptNode node in graphResource.Nodes)
{
foreach (StatescriptNodeProperty binding in node.PropertyBindings)
{
if (binding.Resolver is ActivationDataResolverResource { ProviderClassName.Length: > 0 } resolver)
{
if (firstProvider is null)
{
firstProvider = resolver.ProviderClassName;
}
else if (resolver.ProviderClassName != firstProvider)
{
GD.PushError(
"Statescript: Graph uses multiple activation data providers " +
$"('{firstProvider}' and '{resolver.ProviderClassName}'). " +
"A graph supports only one activation data provider at a time. " +
"Combine the data into a single provider.");
}
}
}
}
}
private static ForgeNode InstantiateNode(StatescriptNode nodeResource)
{
if (string.IsNullOrEmpty(nodeResource.RuntimeTypeName))
{
throw new InvalidOperationException(
$"Node '{nodeResource.NodeId}' of type {nodeResource.NodeType} has no RuntimeTypeName set.");
}
Type? nodeType = ResolveType(nodeResource.RuntimeTypeName);
if (nodeType is null)
{
throw new InvalidOperationException(
$"Could not resolve runtime type '{nodeResource.RuntimeTypeName}' for node " +
$"'{nodeResource.NodeId}'.");
}
ConstructorInfo[] constructors = nodeType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (constructors.Length == 0)
{
return (ForgeNode)Activator.CreateInstance(nodeType)!;
}
ConstructorInfo constructor = constructors.OrderByDescending(x => x.GetParameters().Length).First();
ParameterInfo[] parameters = constructor.GetParameters();
var args = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
ParameterInfo param = parameters[i];
var paramName = param.Name ?? string.Empty;
if (nodeResource.CustomData.TryGetValue(paramName, out GodotVariant value))
{
args[i] = ConvertParameter(value, param.ParameterType);
}
else
{
args[i] = GetDefaultValue(param.ParameterType);
}
}
return (ForgeNode)constructor.Invoke(args);
}
private static Type? ResolveType(string typeName)
{
var type = Type.GetType(typeName);
if (type is not null)
{
return type;
}
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeName);
if (type is not null)
{
return type;
}
}
return null;
}
private static object ConvertParameter(GodotVariant value, Type targetType)
{
if (targetType == typeof(StringKey))
{
return new StringKey(value.AsString());
}
if (targetType == typeof(string))
{
return value.AsString();
}
if (targetType == typeof(int))
{
return value.AsInt32();
}
if (targetType == typeof(float))
{
return value.AsSingle();
}
if (targetType == typeof(double))
{
return value.AsDouble();
}
if (targetType == typeof(bool))
{
return value.AsBool();
}
if (targetType == typeof(long))
{
return value.AsInt64();
}
return Convert.ChangeType(value.AsString(), targetType, CultureInfo.InvariantCulture);
}
private static object GetDefaultValue(Type type)
{
if (type == typeof(StringKey))
{
return new StringKey("_default_");
}
if (type == typeof(string))
{
return string.Empty;
}
if (type.IsValueType)
{
return Activator.CreateInstance(type)!;
}
return null!;
}
}