Replicated the weapon flying tick setup using resources
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

This commit is contained in:
2026-04-07 16:32:26 +02:00
parent cc7cb90041
commit 1d856fd937
145 changed files with 12943 additions and 109 deletions

View File

@@ -8,7 +8,7 @@ public partial class ForgeBootstrap : Node
{
public override void _Ready()
{
ForgeData pluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
_ = new ForgeManagers(pluginData);
}
}

View File

@@ -8,6 +8,8 @@ namespace Gamesmiths.Forge.Godot.Core;
[Tool]
public partial class ForgeData : Resource
{
public const string ForgeDataResourcePath = "res://forge/forge_data.tres";
[Export]
public Array<string> RegisteredTags { get; set; } = [];
}

View File

@@ -0,0 +1,352 @@
// 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!;
}
}

View File

@@ -0,0 +1 @@
uid://btkf3jeisyh8j

View File

@@ -0,0 +1,86 @@
// Copyright © Gamesmiths Guild.
using Gamesmiths.Forge.Core;
using Gamesmiths.Forge.Statescript;
using GodotPlane = Godot.Plane;
using GodotQuaternion = Godot.Quaternion;
using GodotVector2 = Godot.Vector2;
using GodotVector3 = Godot.Vector3;
using GodotVector4 = Godot.Vector4;
using SysPlane = System.Numerics.Plane;
using SysQuaternion = System.Numerics.Quaternion;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Core;
/// <summary>
/// Extension methods for <see cref="Variables"/> that provide seamless support for Godot types. These methods
/// automatically convert Godot math types (e.g., <see cref="GodotVector3"/>) to their System.Numerics equivalents
/// before storing them in the variable bag.
/// </summary>
/// <remarks>
/// Use these overloads in data binder delegates (e.g., when implementing
/// <see cref="Resources.IActivationDataProvider.CreateBehavior"/>) to avoid manual Godot-to-System.Numerics
/// conversions.
/// </remarks>
public static class VariablesExtensions
{
/// <summary>
/// Sets a variable from a <see cref="GodotVector2"/> value, converting it to <see cref="SysVector2"/>.
/// </summary>
/// <param name="variables">The variables bag.</param>
/// <param name="name">The name of the variable to set.</param>
/// <param name="value">The Godot Vector2 value to store.</param>
public static void SetGodotVar(this Variables variables, StringKey name, GodotVector2 value)
{
variables.SetVariant(name, new Variant128(new SysVector2(value.X, value.Y)));
}
/// <summary>
/// Sets a variable from a <see cref="GodotVector3"/> value, converting it to <see cref="SysVector3"/>.
/// </summary>
/// <param name="variables">The variables bag.</param>
/// <param name="name">The name of the variable to set.</param>
/// <param name="value">The Godot Vector3 value to store.</param>
public static void SetGodotVar(this Variables variables, StringKey name, GodotVector3 value)
{
variables.SetVariant(name, new Variant128(new SysVector3(value.X, value.Y, value.Z)));
}
/// <summary>
/// Sets a variable from a <see cref="GodotVector4"/> value, converting it to <see cref="SysVector4"/>.
/// </summary>
/// <param name="variables">The variables bag.</param>
/// <param name="name">The name of the variable to set.</param>
/// <param name="value">The Godot Vector4 value to store.</param>
public static void SetGodotVar(this Variables variables, StringKey name, GodotVector4 value)
{
variables.SetVariant(name, new Variant128(new SysVector4(value.X, value.Y, value.Z, value.W)));
}
/// <summary>
/// Sets a variable from a <see cref="GodotPlane"/> value, converting it to <see cref="SysPlane"/>.
/// </summary>
/// <param name="variables">The variables bag.</param>
/// <param name="name">The name of the variable to set.</param>
/// <param name="value">The Godot Plane value to store.</param>
public static void SetGodotVar(this Variables variables, StringKey name, GodotPlane value)
{
variables.SetVariant(
name,
new Variant128(new SysPlane(value.Normal.X, value.Normal.Y, value.Normal.Z, value.D)));
}
/// <summary>
/// Sets a variable from a <see cref="GodotQuaternion"/> value, converting it to <see cref="SysQuaternion"/>.
/// </summary>
/// <param name="variables">The variables bag.</param>
/// <param name="name">The name of the variable to set.</param>
/// <param name="value">The Godot Quaternion value to store.</param>
public static void SetGodotVar(this Variables variables, StringKey name, GodotQuaternion value)
{
variables.SetVariant(name, new Variant128(new SysQuaternion(value.X, value.Y, value.Z, value.W)));
}
}

View File

@@ -0,0 +1 @@
uid://tkifxnyfxgrp

View File

@@ -4,4 +4,4 @@
[resource]
script = ExtResource("1_x0pne")
RegisteredTags = Array[String](["character.player", "character.enemy", "weapon", "status.stunned", "status.burning", "status.frozen", "abilities.weapon.land", "abilities.weapon.flying", "abilities.weapon.left", "events.combat.damage", "events.combat.hit", "events.weapon.flyingTick", "events.weapon.startedFlying", "events.weapon.stoppedFlying", "events.weapon.handToFlying", "events.weapon.flyingToHand", "events.weapon.plantedToHand", "events.weapon.plantedToFlying", "events.weapon.planted", "cooldown.empoweredAction", "cooldown.empoweredSwordThrow", "cues.resources.mana", "events.player.empowered_action_used", "character.player.mana", "character.player.mana.regen", "character.player.mana.regen.inhibited"])
RegisteredTags = Array[String](["effect.fire", "effect.wet", "cue.floating.text", "cue.vfx.fire", "cue.vfx.wet", "cue.vfx.regen", "cooldown.enemy.attack", "set_by_caller.damage", "event.damage", "cooldown", "cooldown.skill.projectile", "cooldown.skill.shield", "cooldown.skill.dash", "movement.block", "immunity.damage", "effect.mana_shield", "cue.vfx.shield", "event.damage.taken", "event.damage.dealt", "event", "set_by_caller", "trait.flammable", "trait.healable", "trait.damageable", "trait.wettable", "cue.vfx.reflect", "cue.vfx", "cooldown.skill", "cooldown.skill.reflect", "test"])