// 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;
///
/// Resolver editor that binds a node input property to an activation data field. Uses a two-step selection: first
/// select the implementation, then select a compatible field from that provider.
/// Providers are discovered via reflection.
///
///
/// 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.
///
[Tool]
internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
{
private readonly List _providerClassNames = [];
private readonly List _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;
///
public override string DisplayName => "Activation Data";
///
public override string ResolverTypeId => "ActivationData";
///
public override bool IsCompatibleWith(Type expectedType)
{
return true;
}
///
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;
}
///
public override void SaveTo(StatescriptNodeProperty property)
{
property.Resolver = new ActivationDataResolverResource
{
ProviderClassName = _selectedProviderClassName,
FieldName = _selectedFieldName,
FieldType = _selectedFieldType,
};
}
///
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