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

@@ -0,0 +1,366 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using System.Linq;
using Gamesmiths.Forge.Godot.Resources;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that binds a node input property to an activation data field. Uses a two-step selection: first
/// select the <see cref="IActivationDataProvider"/> implementation, then select a compatible field from that provider.
/// Providers are discovered via reflection.
/// </summary>
/// <remarks>
/// A graph supports only one activation data provider. Once any other node in the graph references a provider, the
/// provider dropdown is locked to that provider. The user only needs to clear the bindings on other nodes to unlock
/// the dropdown.
/// </remarks>
[Tool]
internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
{
private readonly List<string> _providerClassNames = [];
private readonly List<string> _fieldNames = [];
private StatescriptGraph? _graph;
private StatescriptNodeProperty? _currentProperty;
private OptionButton? _providerDropdown;
private OptionButton? _fieldDropdown;
private Action? _onChanged;
private Type _expectedType = typeof(Variant128);
private string _selectedProviderClassName = string.Empty;
private string _selectedFieldName = string.Empty;
private StatescriptVariableType _selectedFieldType = StatescriptVariableType.Int;
/// <inheritdoc/>
public override string DisplayName => "Activation Data";
/// <inheritdoc/>
public override string ResolverTypeId => "ActivationData";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return true;
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_onChanged = onChanged;
_expectedType = expectedType;
_graph = graph;
_currentProperty = property;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(vBox);
if (property?.Resolver is ActivationDataResolverResource activationRes)
{
_selectedProviderClassName = activationRes.ProviderClassName;
_selectedFieldName = activationRes.FieldName;
_selectedFieldType = activationRes.FieldType;
}
var providerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(providerRow);
providerRow.AddChild(new Label
{
Text = "Provider:",
CustomMinimumSize = new Vector2(75, 0),
HorizontalAlignment = HorizontalAlignment.Right,
});
_providerDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
PopulateProviderDropdown();
providerRow.AddChild(_providerDropdown);
// Re-scan the graph each time the dropdown opens to pick up changes from other editors.
_providerDropdown.GetPopup().AboutToPopup += PopulateProviderDropdown;
var fieldRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(fieldRow);
fieldRow.AddChild(new Label
{
Text = "Field:",
CustomMinimumSize = new Vector2(75, 0),
HorizontalAlignment = HorizontalAlignment.Right,
});
_fieldDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
PopulateFieldDropdown();
fieldRow.AddChild(_fieldDropdown);
_providerDropdown.ItemSelected += OnProviderDropdownItemSelected;
_fieldDropdown.ItemSelected += OnFieldDropdownItemSelected;
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new ActivationDataResolverResource
{
ProviderClassName = _selectedProviderClassName,
FieldName = _selectedFieldName,
FieldType = _selectedFieldType,
};
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
}
private static string FindExistingProvider(StatescriptGraph graph, StatescriptNodeProperty? currentProperty)
{
foreach (StatescriptNode node in graph.Nodes)
{
foreach (StatescriptNodeProperty binding in node.PropertyBindings)
{
// Skip the property we're currently editing — the user should be free to change it.
if (ReferenceEquals(binding, currentProperty))
{
continue;
}
if (binding.Resolver is ActivationDataResolverResource { ProviderClassName.Length: > 0 } resolver)
{
return resolver.ProviderClassName;
}
}
}
return string.Empty;
}
private static IActivationDataProvider? InstantiateProvider(string className)
{
if (string.IsNullOrEmpty(className))
{
return null;
}
Type? type = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.FirstOrDefault(
x => typeof(IActivationDataProvider).IsAssignableFrom(x)
&& !x.IsAbstract
&& !x.IsInterface
&& x.Name == className);
if (type is null)
{
return null;
}
return Activator.CreateInstance(type) as IActivationDataProvider;
}
private void OnProviderDropdownItemSelected(long index)
{
if (_providerDropdown is null)
{
return;
}
var idx = _providerDropdown.Selected;
_selectedProviderClassName = idx >= 0 && idx < _providerClassNames.Count
? _providerClassNames[idx]
: string.Empty;
_selectedFieldName = string.Empty;
_selectedFieldType = StatescriptVariableType.Int;
PopulateFieldDropdown();
_onChanged?.Invoke();
}
private void OnFieldDropdownItemSelected(long index)
{
if (_fieldDropdown is null)
{
return;
}
var dropdownIndex = _fieldDropdown.Selected;
if (dropdownIndex >= 0 && dropdownIndex < _fieldNames.Count)
{
_selectedFieldName = _fieldNames[dropdownIndex];
if (!string.IsNullOrEmpty(_selectedFieldName))
{
ResolveFieldType();
}
else
{
_selectedFieldType = StatescriptVariableType.Int;
}
}
else
{
_selectedFieldName = string.Empty;
_selectedFieldType = StatescriptVariableType.Int;
}
_onChanged?.Invoke();
}
private void PopulateProviderDropdown()
{
if (_providerDropdown is null)
{
return;
}
_providerDropdown.Clear();
_providerClassNames.Clear();
// Always add a (None) option to allow deselecting.
_providerDropdown.AddItem("(None)");
_providerClassNames.Add(string.Empty);
// Re-scan the graph each time to pick up changes from other editors.
var graphLockedProvider = _graph is not null
? FindExistingProvider(_graph, _currentProperty)
: string.Empty;
if (!string.IsNullOrEmpty(graphLockedProvider))
{
// Another node already uses a provider: only show that one (plus None).
_providerDropdown.AddItem(graphLockedProvider);
_providerClassNames.Add(graphLockedProvider);
}
else
{
foreach (var name in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(x => typeof(IActivationDataProvider).IsAssignableFrom(x)
&& !x.IsAbstract
&& !x.IsInterface)
.Select(x => x.Name))
{
_providerDropdown.AddItem(name);
_providerClassNames.Add(name);
}
}
// Restore selection.
if (!string.IsNullOrEmpty(_selectedProviderClassName))
{
for (var i = 0; i < _providerClassNames.Count; i++)
{
if (_providerClassNames[i] == _selectedProviderClassName)
{
_providerDropdown.Selected = i;
return;
}
}
}
// Default to (None).
_providerDropdown.Selected = 0;
_selectedProviderClassName = string.Empty;
}
private void PopulateFieldDropdown()
{
if (_fieldDropdown is null)
{
return;
}
_fieldDropdown.Clear();
_fieldNames.Clear();
// Always add a (None) option.
_fieldDropdown.AddItem("(None)");
_fieldNames.Add(string.Empty);
IActivationDataProvider? provider = InstantiateProvider(_selectedProviderClassName);
if (provider is not null)
{
foreach (ForgeActivationDataField field in provider.GetFields())
{
if (string.IsNullOrEmpty(field.FieldName))
{
continue;
}
if (_expectedType != typeof(Variant128)
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, field.FieldType))
{
continue;
}
_fieldDropdown.AddItem(field.FieldName);
_fieldNames.Add(field.FieldName);
}
}
// Restore selection.
if (!string.IsNullOrEmpty(_selectedFieldName))
{
for (var i = 0; i < _fieldNames.Count; i++)
{
if (_fieldNames[i] == _selectedFieldName)
{
_fieldDropdown.Selected = i;
return;
}
}
}
// Default to (None).
_fieldDropdown.Selected = 0;
_selectedFieldName = string.Empty;
}
private void ResolveFieldType()
{
if (string.IsNullOrEmpty(_selectedProviderClassName) || string.IsNullOrEmpty(_selectedFieldName))
{
_selectedFieldType = StatescriptVariableType.Int;
return;
}
IActivationDataProvider? provider = InstantiateProvider(_selectedProviderClassName);
if (provider is null)
{
_selectedFieldType = StatescriptVariableType.Int;
return;
}
foreach (ForgeActivationDataField field in provider.GetFields())
{
if (field.FieldName == _selectedFieldName)
{
_selectedFieldType = field.FieldType;
return;
}
}
_selectedFieldType = StatescriptVariableType.Int;
}
}
#endif

View File

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

View File

@@ -0,0 +1,197 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that reads a value from a Forge entity attribute. Shows attribute set and attribute dropdowns.
/// </summary>
[Tool]
internal sealed partial class AttributeResolverEditor : NodeEditorProperty
{
private OptionButton? _setDropdown;
private OptionButton? _attributeDropdown;
private string _selectedSetClass = string.Empty;
private string _selectedAttribute = string.Empty;
private Action? _onChanged;
/// <inheritdoc/>
public override string DisplayName => "Attribute";
/// <inheritdoc/>
public override string ResolverTypeId => "Attribute";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(int) || expectedType == typeof(Variant128);
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_onChanged = onChanged;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(vBox);
if (property?.Resolver is AttributeResolverResource attrRes)
{
_selectedSetClass = attrRes.AttributeSetClass;
_selectedAttribute = attrRes.AttributeName;
}
var setRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(setRow);
setRow.AddChild(new Label
{
Text = "Set:",
CustomMinimumSize = new Vector2(45, 0),
HorizontalAlignment = HorizontalAlignment.Right,
});
_setDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
PopulateSetDropdown();
setRow.AddChild(_setDropdown);
var attrRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(attrRow);
attrRow.AddChild(new Label
{
Text = "Attr:",
CustomMinimumSize = new Vector2(45, 0),
HorizontalAlignment = HorizontalAlignment.Right,
});
_attributeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
PopulateAttributeDropdown();
attrRow.AddChild(_attributeDropdown);
_setDropdown.ItemSelected += OnSetDropdownItemSelected;
_attributeDropdown.ItemSelected += OnAttributeDropdownItemSelected;
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new AttributeResolverResource
{
AttributeSetClass = _selectedSetClass,
AttributeName = _selectedAttribute,
};
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
}
private void OnSetDropdownItemSelected(long index)
{
if (_setDropdown is null)
{
return;
}
_selectedSetClass = _setDropdown.GetItemText(_setDropdown.Selected);
_selectedAttribute = string.Empty;
PopulateAttributeDropdown();
_onChanged?.Invoke();
}
private void OnAttributeDropdownItemSelected(long index)
{
if (_attributeDropdown is null)
{
return;
}
_selectedAttribute = _attributeDropdown.GetItemText(_attributeDropdown.Selected);
_onChanged?.Invoke();
}
private void PopulateSetDropdown()
{
if (_setDropdown is null)
{
return;
}
_setDropdown.Clear();
foreach (var option in EditorUtils.GetAttributeSetOptions())
{
_setDropdown.AddItem(option);
}
// Restore selection.
if (!string.IsNullOrEmpty(_selectedSetClass))
{
for (var i = 0; i < _setDropdown.GetItemCount(); i++)
{
if (_setDropdown.GetItemText(i) == _selectedSetClass)
{
_setDropdown.Selected = i;
return;
}
}
}
// Default to first if available.
if (_setDropdown.GetItemCount() > 0)
{
_setDropdown.Selected = 0;
_selectedSetClass = _setDropdown.GetItemText(0);
}
}
private void PopulateAttributeDropdown()
{
if (_attributeDropdown is null)
{
return;
}
_attributeDropdown.Clear();
foreach (var option in EditorUtils.GetAttributeOptions(_selectedSetClass))
{
_attributeDropdown.AddItem(option);
}
if (!string.IsNullOrEmpty(_selectedAttribute))
{
for (var i = 0; i < _attributeDropdown.GetItemCount(); i++)
{
if (_attributeDropdown.GetItemText(i) == _selectedAttribute)
{
_attributeDropdown.Selected = i;
return;
}
}
}
if (_attributeDropdown.GetItemCount() > 0)
{
_attributeDropdown.Selected = 0;
_selectedAttribute = _attributeDropdown.GetItemText(0);
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,286 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript.Properties;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that compares two nested numeric resolvers and produces a boolean result. Supports nesting any
/// numeric-compatible resolver as left/right operands, enabling powerful comparisons like "Attribute &gt; Constant".
/// </summary>
[Tool]
internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
{
private StatescriptGraph? _graph;
private Action? _onChanged;
private OptionButton? _operationDropdown;
private VBoxContainer? _leftContainer;
private VBoxContainer? _rightContainer;
private OptionButton? _leftResolverDropdown;
private OptionButton? _rightResolverDropdown;
private NodeEditorProperty? _leftEditor;
private NodeEditorProperty? _rightEditor;
private List<Func<NodeEditorProperty>> _numericFactories = [];
private ComparisonOperation _operation;
private VBoxContainer? _leftEditorContainer;
private VBoxContainer? _rightEditorContainer;
/// <inheritdoc/>
public override string DisplayName => "Comparison";
/// <inheritdoc/>
public override string ResolverTypeId => "Comparison";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(bool) || expectedType == typeof(ForgeVariant128);
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_graph = graph;
_onChanged = onChanged;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(vBox);
_numericFactories = StatescriptResolverRegistry.GetCompatibleFactories(typeof(ForgeVariant128));
_numericFactories.RemoveAll(x =>
{
using NodeEditorProperty temp = x();
return temp.ResolverTypeId == "Comparison";
});
var comparisonResolver = property?.Resolver as ComparisonResolverResource;
if (comparisonResolver is not null)
{
_operation = comparisonResolver.Operation;
}
var leftFoldable = new FoldableContainer { Title = "Left:" };
leftFoldable.FoldingChanged += OnFoldingChanged;
vBox.AddChild(leftFoldable);
_leftContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
leftFoldable.AddChild(_leftContainer);
_leftEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_leftResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Left);
_leftContainer.AddChild(_leftResolverDropdown);
_leftContainer.AddChild(_leftEditorContainer);
ShowNestedEditor(
GetSelectedIndex(comparisonResolver?.Left),
comparisonResolver?.Left,
_leftEditorContainer,
x => _leftEditor = x);
_leftResolverDropdown.ItemSelected += OnLeftResolverDropdownItemSelected;
var opRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(opRow);
opRow.AddChild(new Label { Text = "Op:" });
_operationDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_operationDropdown.AddItem("==");
_operationDropdown.AddItem("!=");
_operationDropdown.AddItem("<");
_operationDropdown.AddItem("<=");
_operationDropdown.AddItem(">");
_operationDropdown.AddItem(">=");
_operationDropdown.Selected = (int)_operation;
_operationDropdown.ItemSelected += OnOperationDropdownItemSelected;
opRow.AddChild(_operationDropdown);
var rightFoldable = new FoldableContainer { Title = "Right:" };
rightFoldable.FoldingChanged += OnFoldingChanged;
vBox.AddChild(rightFoldable);
_rightContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
rightFoldable.AddChild(_rightContainer);
_rightEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_rightResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Right);
_rightContainer.AddChild(_rightResolverDropdown);
_rightContainer.AddChild(_rightEditorContainer);
ShowNestedEditor(
GetSelectedIndex(comparisonResolver?.Right),
comparisonResolver?.Right,
_rightEditorContainer,
x => _rightEditor = x);
_rightResolverDropdown.ItemSelected += OnRightResolverDropdownItemSelected;
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
var comparisonResolver = new ComparisonResolverResource { Operation = _operation };
if (_leftEditor is not null)
{
var leftProperty = new StatescriptNodeProperty();
_leftEditor.SaveTo(leftProperty);
comparisonResolver.Left = leftProperty.Resolver;
}
if (_rightEditor is not null)
{
var rightProperty = new StatescriptNodeProperty();
_rightEditor.SaveTo(rightProperty);
comparisonResolver.Right = rightProperty.Resolver;
}
property.Resolver = comparisonResolver;
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
_leftEditor?.ClearCallbacks();
_rightEditor?.ClearCallbacks();
}
private void OnFoldingChanged(bool isFolded)
{
RaiseLayoutSizeChanged();
}
private void OnOperationDropdownItemSelected(long x)
{
_operation = (ComparisonOperation)(int)x;
_onChanged?.Invoke();
}
private void OnLeftResolverDropdownItemSelected(long x)
{
HandleResolverDropdownChanged((int)x, _leftEditorContainer, editor => _leftEditor = editor);
}
private void OnRightResolverDropdownItemSelected(long x)
{
HandleResolverDropdownChanged((int)x, _rightEditorContainer, editor => _rightEditor = editor);
}
private void HandleResolverDropdownChanged(
int selectedIndex,
VBoxContainer? editorContainer,
Action<NodeEditorProperty?> setEditor)
{
if (editorContainer is null)
{
return;
}
foreach (Node child in editorContainer.GetChildren())
{
editorContainer.RemoveChild(child);
child.Free();
}
setEditor(null);
ShowNestedEditor(selectedIndex, null, editorContainer, setEditor);
_onChanged?.Invoke();
RaiseLayoutSizeChanged();
}
private int GetSelectedIndex(StatescriptResolverResource? existingResolver)
{
var selectedIndex = 0;
if (existingResolver is not null)
{
var existingTypeId = existingResolver.ResolverTypeId;
for (var i = 0; i < _numericFactories.Count; i++)
{
using NodeEditorProperty temp = _numericFactories[i]();
if (temp.ResolverTypeId == existingTypeId)
{
selectedIndex = i;
break;
}
}
}
return selectedIndex;
}
private OptionButton CreateResolverDropdownControl(StatescriptResolverResource? existingResolver)
{
var dropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
foreach (Func<NodeEditorProperty> factory in _numericFactories)
{
using NodeEditorProperty temp = factory();
dropdown.AddItem(temp.DisplayName);
}
dropdown.Selected = GetSelectedIndex(existingResolver);
return dropdown;
}
private void ShowNestedEditor(
int factoryIndex,
StatescriptResolverResource? existingResolver,
VBoxContainer container,
Action<NodeEditorProperty?> setEditor)
{
if (_graph is null || factoryIndex < 0 || factoryIndex >= _numericFactories.Count)
{
return;
}
NodeEditorProperty editor = _numericFactories[factoryIndex]();
StatescriptNodeProperty? tempProperty = null;
if (existingResolver is not null)
{
tempProperty = new StatescriptNodeProperty { Resolver = existingResolver };
}
editor.Setup(_graph, tempProperty, typeof(ForgeVariant128), OnNestedEditorChanged, false);
editor.LayoutSizeChanged += RaiseLayoutSizeChanged;
container.AddChild(editor);
setEditor(editor);
}
private void OnNestedEditorChanged()
{
_onChanged?.Invoke();
}
}
#endif

View File

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

View File

@@ -0,0 +1,57 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor for the ability activation magnitude. No configuration is needed, it simply reads the magnitude from
/// the <see cref="Abilities.AbilityBehaviorContext"/> at runtime. Only compatible with <see langword="float"/> inputs.
/// </summary>
[Tool]
internal sealed partial class MagnitudeResolverEditor : NodeEditorProperty
{
/// <inheritdoc/>
public override string DisplayName => "Magnitude";
/// <inheritdoc/>
public override string ResolverTypeId => "Magnitude";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(float) || expectedType == typeof(Variant128);
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
SizeFlagsHorizontal = SizeFlags.ExpandFill;
var label = new Label
{
Text = "Ability Magnitude",
HorizontalAlignment = HorizontalAlignment.Center,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
AddChild(label);
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new MagnitudeResolverResource();
}
}
#endif

View File

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

View File

@@ -0,0 +1,319 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using Gamesmiths.Forge.Godot.Resources;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that binds a node input property to a shared variable on the owning entity. Uses a two-step
/// selection: first select the <see cref="ForgeSharedVariableSet"/> resource, then select a compatible variable from
/// that set. At runtime the value is read from the entity's <see cref="GraphContext.SharedVariables"/> bag.
/// </summary>
[Tool]
internal sealed partial class SharedVariableResolverEditor : NodeEditorProperty
{
private readonly List<string> _setPaths = [];
private readonly List<string> _setDisplayNames = [];
private readonly List<string> _variableNames = [];
private OptionButton? _setDropdown;
private OptionButton? _variableDropdown;
private Action? _onChanged;
private Type _expectedType = typeof(Variant128);
private string _selectedSetPath = string.Empty;
private string _selectedVariableName = string.Empty;
private StatescriptVariableType _selectedVariableType = StatescriptVariableType.Int;
/// <inheritdoc/>
public override string DisplayName => "Shared Variable";
/// <inheritdoc/>
public override string ResolverTypeId => "SharedVariable";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return true;
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_onChanged = onChanged;
_expectedType = expectedType;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(vBox);
if (property?.Resolver is SharedVariableResolverResource sharedRes)
{
_selectedSetPath = sharedRes.SharedVariableSetPath;
_selectedVariableName = sharedRes.VariableName;
_selectedVariableType = sharedRes.VariableType;
}
var setRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(setRow);
setRow.AddChild(new Label
{
Text = "Set:",
CustomMinimumSize = new Vector2(45, 0),
HorizontalAlignment = HorizontalAlignment.Right,
});
_setDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
PopulateSetDropdown();
setRow.AddChild(_setDropdown);
var varRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(varRow);
varRow.AddChild(new Label
{
Text = "Var:",
CustomMinimumSize = new Vector2(45, 0),
HorizontalAlignment = HorizontalAlignment.Right,
});
_variableDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
PopulateVariableDropdown();
varRow.AddChild(_variableDropdown);
_setDropdown.ItemSelected += OnSetDropdownItemSelected;
_variableDropdown.ItemSelected += OnVariableDropdownItemSelected;
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new SharedVariableResolverResource
{
SharedVariableSetPath = _selectedSetPath,
VariableName = _selectedVariableName,
VariableType = _selectedVariableType,
};
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
}
private static List<string> FindAllSharedVariableSetPaths()
{
var results = new List<string>();
EditorFileSystemDirectory root = EditorInterface.Singleton.GetResourceFilesystem().GetFilesystem();
ScanFilesystemDirectory(root, results);
return results;
}
private static void ScanFilesystemDirectory(EditorFileSystemDirectory dir, List<string> results)
{
for (var i = 0; i < dir.GetFileCount(); i++)
{
var path = dir.GetFilePath(i);
if (!path.EndsWith(".tres", StringComparison.InvariantCultureIgnoreCase)
&& !path.EndsWith(".res", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
Resource resource = ResourceLoader.Load(path);
if (resource is ForgeSharedVariableSet)
{
results.Add(path);
}
}
for (var i = 0; i < dir.GetSubdirCount(); i++)
{
ScanFilesystemDirectory(dir.GetSubdir(i), results);
}
}
private void OnSetDropdownItemSelected(long index)
{
if (_setDropdown is null)
{
return;
}
var idx = _setDropdown.Selected;
_selectedSetPath = idx >= 0 && idx < _setPaths.Count ? _setPaths[idx] : string.Empty;
_selectedVariableName = string.Empty;
_selectedVariableType = StatescriptVariableType.Int;
PopulateVariableDropdown();
_onChanged?.Invoke();
}
private void OnVariableDropdownItemSelected(long index)
{
if (_variableDropdown is null)
{
return;
}
var idx = _variableDropdown.Selected;
if (idx >= 0 && idx < _variableNames.Count)
{
_selectedVariableName = _variableNames[idx];
ResolveVariableType();
}
else
{
_selectedVariableName = string.Empty;
_selectedVariableType = StatescriptVariableType.Int;
}
_onChanged?.Invoke();
}
private void PopulateSetDropdown()
{
if (_setDropdown is null)
{
return;
}
_setDropdown.Clear();
_setPaths.Clear();
_setDisplayNames.Clear();
_setDropdown.AddItem("(None)");
_setPaths.Add(string.Empty);
_setDisplayNames.Add("(None)");
foreach (var path in FindAllSharedVariableSetPaths())
{
var displayName = path[(path.LastIndexOf('/') + 1)..];
if (displayName.EndsWith(".tres", StringComparison.OrdinalIgnoreCase))
{
displayName = displayName[..^5];
}
_setDropdown.AddItem(displayName);
_setPaths.Add(path);
_setDisplayNames.Add(displayName);
}
// Restore selection.
for (var i = 0; i < _setPaths.Count; i++)
{
if (_setPaths[i] == _selectedSetPath)
{
_setDropdown.Selected = i;
return;
}
}
_setDropdown.Selected = 0;
_selectedSetPath = string.Empty;
}
private void PopulateVariableDropdown()
{
if (_variableDropdown is null)
{
return;
}
_variableDropdown.Clear();
_variableNames.Clear();
_variableDropdown.AddItem("(None)");
_variableNames.Add(string.Empty);
if (!string.IsNullOrEmpty(_selectedSetPath) && ResourceLoader.Exists(_selectedSetPath))
{
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
if (set is not null)
{
foreach (ForgeSharedVariableDefinition def in set.Variables)
{
if (string.IsNullOrEmpty(def.VariableName))
{
continue;
}
if (_expectedType != typeof(Variant128)
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, def.VariableType))
{
continue;
}
var label = $"{def.VariableName}";
_variableDropdown.AddItem(label);
_variableNames.Add(def.VariableName);
}
}
}
// Restore selection.
for (var i = 0; i < _variableNames.Count; i++)
{
if (_variableNames[i] == _selectedVariableName)
{
_variableDropdown.Selected = i;
return;
}
}
_variableDropdown.Selected = 0;
_selectedVariableName = string.Empty;
}
private void ResolveVariableType()
{
if (string.IsNullOrEmpty(_selectedSetPath)
|| string.IsNullOrEmpty(_selectedVariableName)
|| !ResourceLoader.Exists(_selectedSetPath))
{
_selectedVariableType = StatescriptVariableType.Int;
return;
}
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
if (set is null)
{
_selectedVariableType = StatescriptVariableType.Int;
return;
}
foreach (ForgeSharedVariableDefinition def in set.Variables)
{
if (def.VariableName == _selectedVariableName)
{
_selectedVariableType = def.VariableType;
return;
}
}
_selectedVariableType = StatescriptVariableType.Int;
}
}
#endif

View File

@@ -0,0 +1 @@
uid://55ynvr5cbscp

View File

@@ -0,0 +1,198 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using Gamesmiths.Forge.Godot.Core;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript;
using Gamesmiths.Forge.Tags;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that selects a single tag. Reuses the tag tree UI pattern from <c>TagEditorProperty</c>.
/// </summary>
[Tool]
internal sealed partial class TagResolverEditor : NodeEditorProperty
{
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
private Button? _tagButton;
private ScrollContainer? _scroll;
private Tree? _tree;
private string _selectedTag = string.Empty;
private Texture2D? _checkedIcon;
private Texture2D? _uncheckedIcon;
private Action? _onChanged;
/// <inheritdoc/>
public override string DisplayName => "Tag";
/// <inheritdoc/>
public override string ResolverTypeId => "Tag";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(bool) || expectedType == typeof(Variant128);
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_onChanged = onChanged;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(vBox);
// Restore from existing binding.
if (property?.Resolver is TagResolverResource tagRes)
{
_selectedTag = tagRes.Tag;
}
_checkedIcon = EditorInterface.Singleton
.GetEditorTheme()
.GetIcon("GuiRadioChecked", "EditorIcons");
_uncheckedIcon = EditorInterface.Singleton
.GetEditorTheme()
.GetIcon("GuiRadioUnchecked", "EditorIcons");
_tagButton = new Button
{
ToggleMode = true,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
Text = string.IsNullOrEmpty(_selectedTag) ? "(select tag)" : _selectedTag,
};
_tagButton.Toggled += OnTagButtonToggled;
vBox.AddChild(_tagButton);
_scroll = new ScrollContainer
{
Visible = false,
CustomMinimumSize = new Vector2(0, 180),
SizeFlagsHorizontal = SizeFlags.ExpandFill,
SizeFlagsVertical = SizeFlags.ExpandFill,
};
_tree = new Tree
{
HideRoot = true,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
SizeFlagsVertical = SizeFlags.ExpandFill,
};
_scroll.AddChild(_tree);
vBox.AddChild(_scroll);
_tree.ButtonClicked += OnTreeButtonClicked;
RebuildTree();
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new TagResolverResource
{
Tag = _selectedTag,
};
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
}
private void OnTagButtonToggled(bool toggled)
{
if (_scroll is not null)
{
_scroll.Visible = toggled;
RaiseLayoutSizeChanged();
}
}
private void OnTreeButtonClicked(TreeItem item, long column, long id, long mouseButton)
{
if (mouseButton != 1 || id != 0)
{
return;
}
if (!_treeItemToNode.TryGetValue(item, out TagNode? tagNode))
{
return;
}
Forge.Core.StringKey newValue = tagNode.CompleteTagKey;
if (newValue == _selectedTag)
{
newValue = string.Empty;
}
_selectedTag = newValue;
if (_tagButton is not null)
{
_tagButton.Text = string.IsNullOrEmpty(_selectedTag) ? "(select tag)" : _selectedTag;
}
RebuildTree();
_onChanged?.Invoke();
}
private void RebuildTree()
{
if (_tree is null || _checkedIcon is null || _uncheckedIcon is null)
{
return;
}
_tree.Clear();
_treeItemToNode.Clear();
TreeItem root = _tree.CreateItem();
ForgeData forgePluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
var tagsManager = new TagsManager([.. forgePluginData.RegisteredTags]);
BuildTreeRecursive(root, tagsManager.RootNode);
}
private void BuildTreeRecursive(TreeItem parent, TagNode node)
{
if (_tree is null || _checkedIcon is null || _uncheckedIcon is null)
{
return;
}
foreach (TagNode child in node.ChildTags)
{
TreeItem item = _tree.CreateItem(parent);
item.SetText(0, child.TagKey);
var selected = _selectedTag == child.CompleteTagKey;
item.AddButton(0, selected ? _checkedIcon : _uncheckedIcon);
_treeItemToNode[item] = child;
BuildTreeRecursive(item, child);
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,149 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that binds an input property to a graph variable. Only variables whose type is compatible with the
/// expected type are shown in the dropdown.
/// </summary>
[Tool]
internal sealed partial class VariableResolverEditor : NodeEditorProperty
{
private readonly List<string> _variableNames = [];
private OptionButton? _dropdown;
private string _selectedVariableName = string.Empty;
private Action? _onChanged;
/// <inheritdoc/>
public override string DisplayName => "Variable";
/// <inheritdoc/>
public override string ResolverTypeId => "Variable";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return true;
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_onChanged = onChanged;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
CustomMinimumSize = new Vector2(200, 25);
_dropdown = new OptionButton
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
CustomMinimumSize = new Vector2(100, 0),
};
_dropdown.SetMeta("is_variable_dropdown", true);
PopulateDropdown(graph, expectedType);
if (property?.Resolver is VariableResolverResource varRes
&& !string.IsNullOrEmpty(varRes.VariableName))
{
_selectedVariableName = varRes.VariableName;
SelectByName(varRes.VariableName);
}
_dropdown.ItemSelected += OnDropdownItemSelected;
AddChild(_dropdown);
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new VariableResolverResource
{
VariableName = _selectedVariableName,
};
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
}
private void OnDropdownItemSelected(long index)
{
if (_dropdown is null)
{
return;
}
var idx = _dropdown.Selected;
_selectedVariableName = idx >= 0 && idx < _variableNames.Count ? _variableNames[idx] : string.Empty;
_onChanged?.Invoke();
}
private void PopulateDropdown(StatescriptGraph graph, Type expectedType)
{
if (_dropdown is null)
{
return;
}
_dropdown.Clear();
_variableNames.Clear();
_dropdown.AddItem("(None)");
_variableNames.Add(string.Empty);
foreach (StatescriptGraphVariable variable in graph.Variables)
{
if (string.IsNullOrEmpty(variable.VariableName))
{
continue;
}
if (!StatescriptVariableTypeConverter.IsCompatible(expectedType, variable.VariableType))
{
continue;
}
_dropdown.AddItem(variable.VariableName);
_variableNames.Add(variable.VariableName);
}
}
private void SelectByName(string name)
{
if (_dropdown is null || string.IsNullOrEmpty(name))
{
return;
}
for (var i = 0; i < _variableNames.Count; i++)
{
if (_variableNames[i] == name)
{
_dropdown.Selected = i;
return;
}
}
_selectedVariableName = string.Empty;
}
}
#endif

View File

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

View File

@@ -0,0 +1,388 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using Godot.Collections;
using GodotVariant = Godot.Variant;
using GodotVector2 = Godot.Vector2;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
/// <summary>
/// Resolver editor that holds a constant (inline) value. The user edits the value directly in the node.
/// </summary>
[Tool]
internal sealed partial class VariantResolverEditor : NodeEditorProperty
{
private StatescriptVariableType _valueType;
private bool _isArray;
private bool _isArrayExpanded;
private GodotVariant _currentValue;
private Array<GodotVariant> _arrayValues = [];
private Action? _onChanged;
private Button? _toggleButton;
private VBoxContainer? _elementsContainer;
/// <inheritdoc/>
public override string DisplayName => "Constant";
/// <inheritdoc/>
public override string ResolverTypeId => "Variant";
/// <inheritdoc/>
public override bool IsCompatibleWith(Type expectedType)
{
return true;
}
/// <inheritdoc/>
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_isArray = isArray;
_onChanged = onChanged;
if (!StatescriptVariableTypeConverter.TryFromSystemType(expectedType, out _valueType))
{
_valueType = StatescriptVariableType.Int;
}
if (property?.Resolver is VariantResolverResource variantRes)
{
_valueType = variantRes.ValueType;
if (_isArray)
{
_arrayValues = [.. variantRes.ArrayValues];
_isArrayExpanded = variantRes.IsArrayExpanded;
}
else
{
_currentValue = variantRes.Value;
}
}
else if (_isArray)
{
_arrayValues = [];
}
else
{
_currentValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType);
}
CustomMinimumSize = new GodotVector2(200, 40);
if (_isArray)
{
VBoxContainer arrayEditor = CreateArrayEditor();
AddChild(arrayEditor);
}
else
{
Control valueEditor = CreateValueEditor();
AddChild(valueEditor);
}
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
if (_isArray)
{
property.Resolver = new VariantResolverResource
{
ValueType = _valueType,
IsArray = true,
ArrayValues = [.. _arrayValues],
IsArrayExpanded = _isArrayExpanded,
};
}
else
{
property.Resolver = new VariantResolverResource
{
Value = _currentValue,
ValueType = _valueType,
};
}
}
/// <inheritdoc/>
public override void ClearCallbacks()
{
base.ClearCallbacks();
_onChanged = null;
}
private Control CreateValueEditor()
{
if (_valueType == StatescriptVariableType.Bool)
{
return StatescriptEditorControls.CreateBoolEditor(_currentValue.AsBool(), OnBoolValueChanged);
}
if (StatescriptEditorControls.IsIntegerType(_valueType)
|| StatescriptEditorControls.IsFloatType(_valueType))
{
return StatescriptEditorControls.CreateNumericSpinSlider(
_valueType,
_currentValue.AsDouble(),
OnNumericValueChanged);
}
if (StatescriptEditorControls.IsVectorType(_valueType))
{
return StatescriptEditorControls.CreateVectorEditor(
_valueType,
x => StatescriptEditorControls.GetVectorComponent(_currentValue, _valueType, x),
OnVectorValueChanged);
}
return new Label { Text = _valueType.ToString() };
}
private void OnBoolValueChanged(bool x)
{
_currentValue = GodotVariant.From(x);
_onChanged?.Invoke();
}
private void OnNumericValueChanged(double x)
{
_currentValue = GodotVariant.From(x);
_onChanged?.Invoke();
}
private void OnVectorValueChanged(double[] x)
{
_currentValue = StatescriptEditorControls.BuildVectorVariant(_valueType, x);
_onChanged?.Invoke();
}
private VBoxContainer CreateArrayEditor()
{
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_elementsContainer = new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
Visible = _isArrayExpanded,
};
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
vBox.AddChild(headerRow);
_toggleButton = new Button
{
Text = $"Array (size {_arrayValues.Count})",
SizeFlagsHorizontal = SizeFlags.ExpandFill,
ToggleMode = true,
ButtonPressed = _isArrayExpanded,
};
_toggleButton.Toggled += OnArrayToggled;
headerRow.AddChild(_toggleButton);
Texture2D addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
var addButton = new Button
{
Icon = addIcon,
Flat = true,
TooltipText = "Add Element",
CustomMinimumSize = new GodotVector2(24, 24),
};
addButton.Pressed += OnAddElementPressed;
headerRow.AddChild(addButton);
vBox.AddChild(_elementsContainer);
RebuildArrayElements();
return vBox;
}
private void OnArrayToggled(bool toggled)
{
if (_elementsContainer is not null)
{
_elementsContainer.Visible = toggled;
}
_isArrayExpanded = toggled;
_onChanged?.Invoke();
RaiseLayoutSizeChanged();
}
private void OnAddElementPressed()
{
GodotVariant defaultValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType);
_arrayValues.Add(defaultValue);
_onChanged?.Invoke();
if (_elementsContainer is not null)
{
_elementsContainer.Visible = true;
}
_isArrayExpanded = true;
RebuildArrayElements();
RaiseLayoutSizeChanged();
}
private void RebuildArrayElements()
{
if (_elementsContainer is null || _toggleButton is null)
{
return;
}
foreach (Node child in _elementsContainer.GetChildren())
{
_elementsContainer.RemoveChild(child);
child.Free();
}
_toggleButton.Text = $"Array (size {_arrayValues.Count})";
Texture2D removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
for (var i = 0; i < _arrayValues.Count; i++)
{
var capturedIndex = i;
if (StatescriptEditorControls.IsVectorType(_valueType))
{
var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_elementsContainer.AddChild(elementVBox);
var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
elementVBox.AddChild(labelRow);
labelRow.AddChild(new Label
{
Text = $"[{i}]",
SizeFlagsHorizontal = SizeFlags.ExpandFill,
});
AddArrayRemoveButton(labelRow, removeIcon, capturedIndex);
VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor(
_valueType,
x =>
{
return StatescriptEditorControls.GetVectorComponent(
_arrayValues[capturedIndex],
_valueType,
x);
},
x =>
{
_arrayValues[capturedIndex] =
StatescriptEditorControls.BuildVectorVariant(_valueType, x);
_onChanged?.Invoke();
});
elementVBox.AddChild(vectorEditor);
}
else
{
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_elementsContainer.AddChild(elementRow);
elementRow.AddChild(new Label { Text = $"[{i}]" });
if (_valueType == StatescriptVariableType.Bool)
{
elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor(
_arrayValues[capturedIndex].AsBool(),
x =>
{
_arrayValues[capturedIndex] = GodotVariant.From(x);
_onChanged?.Invoke();
}));
}
else
{
EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider(
_valueType,
_arrayValues[capturedIndex].AsDouble(),
x =>
{
_arrayValues[capturedIndex] = GodotVariant.From(x);
_onChanged?.Invoke();
});
elementRow.AddChild(spin);
}
AddArrayRemoveButton(elementRow, removeIcon, capturedIndex);
}
}
}
private void AddArrayRemoveButton(
HBoxContainer row,
Texture2D removeIcon,
int elementIndex)
{
var removeButton = new Button
{
Icon = removeIcon,
Flat = true,
TooltipText = "Remove Element",
CustomMinimumSize = new GodotVector2(24, 24),
};
var handler = new ArrayRemoveHandler(this, elementIndex);
removeButton.AddChild(handler);
removeButton.Pressed += handler.HandlePressed;
row.AddChild(removeButton);
}
private void OnRemoveElement(int elementIndex)
{
_arrayValues.RemoveAt(elementIndex);
_onChanged?.Invoke();
RebuildArrayElements();
RaiseLayoutSizeChanged();
}
/// <summary>
/// Godot-compatible signal handler for array element remove buttons. Holds the element index and a reference to the
/// owning editor so the <c>Pressed</c> signal can be handled without a lambda.
/// </summary>
[Tool]
private sealed partial class ArrayRemoveHandler : Node
{
private readonly VariantResolverEditor _editor;
private readonly int _elementIndex;
public ArrayRemoveHandler()
{
_editor = null!;
}
public ArrayRemoveHandler(VariantResolverEditor editor, int elementIndex)
{
_editor = editor;
_elementIndex = elementIndex;
}
public void HandlePressed()
{
_editor.OnRemoveElement(_elementIndex);
}
}
}
#endif

View File

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