// 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