diff --git a/addons/forge-godot-main.zip b/addons/forge-godot-main.zip new file mode 100644 index 00000000..022f4e83 Binary files /dev/null and b/addons/forge-godot-main.zip differ diff --git a/addons/forge/Forge.props b/addons/forge/Forge.props index 6aec39f0..5e94772b 100644 --- a/addons/forge/Forge.props +++ b/addons/forge/Forge.props @@ -3,7 +3,6 @@ enable - + - diff --git a/addons/forge/ForgePluginLoader.cs b/addons/forge/ForgePluginLoader.cs index 2d3155da..170181fd 100644 --- a/addons/forge/ForgePluginLoader.cs +++ b/addons/forge/ForgePluginLoader.cs @@ -2,7 +2,6 @@ #if TOOLS using System; -using System.Diagnostics; using Gamesmiths.Forge.Core; using Gamesmiths.Forge.Godot.Core; using Gamesmiths.Forge.Godot.Editor; @@ -69,31 +68,35 @@ public partial class ForgePluginLoader : EditorPlugin public override void _ExitTree() { - Debug.Assert( - _tagsEditorDock is not null, - $"{nameof(_tagsEditorDock)} should have been initialized on _Ready()."); - Debug.Assert( - _statescriptGraphEditorDock is not null, - $"{nameof(_statescriptGraphEditorDock)} should have been initialized on _Ready()."); - if (_fileSystem?.IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _resourcesReimportedCallable) == true) { _fileSystem.Disconnect(EditorFileSystem.SignalName.ResourcesReimported, _resourcesReimportedCallable); } - RemoveDock(_tagsEditorDock); - _tagsEditorDock.Free(); + if (_tagsEditorDock is not null) + { + RemoveDock(_tagsEditorDock); + _tagsEditorDock.Free(); + _tagsEditorDock = null; + } - RemoveInspectorPlugin(_tagContainerInspectorPlugin); - RemoveInspectorPlugin(_tagInspectorPlugin); - RemoveInspectorPlugin(_attributeSetInspectorPlugin); - RemoveInspectorPlugin(_cueHandlerInspectorPlugin); - RemoveInspectorPlugin(_attributeEditorPlugin); - RemoveInspectorPlugin(_sharedVariableSetInspectorPlugin); + RemoveInspectorPluginAndRelease(ref _tagContainerInspectorPlugin); + RemoveInspectorPluginAndRelease(ref _tagInspectorPlugin); + RemoveInspectorPluginAndRelease(ref _attributeSetInspectorPlugin); + RemoveInspectorPluginAndRelease(ref _cueHandlerInspectorPlugin); + RemoveInspectorPluginAndRelease(ref _attributeEditorPlugin); + RemoveInspectorPluginAndRelease(ref _sharedVariableSetInspectorPlugin); - RemoveDock(_statescriptGraphEditorDock); - _statescriptGraphEditorDock.Free(); + if (_statescriptGraphEditorDock is not null) + { + RemoveDock(_statescriptGraphEditorDock); + _statescriptGraphEditorDock.Free(); + _statescriptGraphEditorDock = null; + } + + _fileSystem = null; + _resourcesReimportedCallable = default; RemoveToolMenuItem("Repair assets tags"); } @@ -132,7 +135,7 @@ public partial class ForgePluginLoader : EditorPlugin EnsureForgeDataExists(); - var config = ProjectSettings.LoadResourcePack(AutoloadPath); + bool config = ProjectSettings.LoadResourcePack(AutoloadPath); if (config) { @@ -173,7 +176,7 @@ public partial class ForgePluginLoader : EditorPlugin return; } - var paths = _statescriptGraphEditorDock.GetOpenResourcePaths(); + string[] paths = _statescriptGraphEditorDock.GetOpenResourcePaths(); if (paths.Length == 0) { @@ -183,7 +186,7 @@ public partial class ForgePluginLoader : EditorPlugin configuration.SetValue("Forge", "open_tabs", string.Join(";", paths)); configuration.SetValue("Forge", "active_tab", _statescriptGraphEditorDock.GetActiveTabIndex()); - var varStates = _statescriptGraphEditorDock.GetVariablesPanelStates(); + bool[] varStates = _statescriptGraphEditorDock.GetVariablesPanelStates(); configuration.SetValue("Forge", "variables_states", string.Join(";", varStates)); } @@ -197,26 +200,26 @@ public partial class ForgePluginLoader : EditorPlugin Variant tabsValue = configuration.GetValue("Forge", "open_tabs", string.Empty); Variant active = configuration.GetValue("Forge", "active_tab", -1); - var tabsString = tabsValue.AsString(); + string tabsString = tabsValue.AsString(); if (string.IsNullOrEmpty(tabsString)) { return; } - var paths = tabsString.Split(';', StringSplitOptions.RemoveEmptyEntries); - var activeIndex = active.AsInt32(); + string[] paths = tabsString.Split(';', StringSplitOptions.RemoveEmptyEntries); + int activeIndex = active.AsInt32(); bool[]? variablesStates = null; Variant varStatesValue = configuration.GetValue("Forge", "variables_states", string.Empty); - var varString = varStatesValue.AsString(); + string varString = varStatesValue.AsString(); if (!string.IsNullOrEmpty(varString)) { - var parts = varString.Split(';'); + string[] parts = varString.Split(';'); variablesStates = new bool[parts.Length]; - for (var i = 0; i < parts.Length; i++) + for (int i = 0; i < parts.Length; i++) { - variablesStates[i] = bool.TryParse(parts[i], out var v) && v; + variablesStates[i] = bool.TryParse(parts[i], out bool v) && v; } } @@ -248,16 +251,28 @@ public partial class ForgePluginLoader : EditorPlugin AssetRepairTool.RepairAllAssetsTags(); } + private void RemoveInspectorPluginAndRelease(ref TPlugin? plugin) + where TPlugin : EditorInspectorPlugin + { + if (plugin is null) + { + return; + } + + RemoveInspectorPlugin(plugin); + plugin = null; + } + private void OnResourcesReimported(string[] resources) { - foreach (var path in resources) + foreach (string path in resources) { if (!ResourceLoader.Exists(path)) { continue; } - var fileType = EditorInterface.Singleton.GetResourceFilesystem().GetFileType(path); + string fileType = EditorInterface.Singleton.GetResourceFilesystem().GetFileType(path); if (fileType != "StatescriptGraph" && fileType != "Resource") { continue; diff --git a/addons/forge/core/ForgeRandom.cs b/addons/forge/core/ForgeRandom.cs index 08ee3d6d..87151535 100644 --- a/addons/forge/core/ForgeRandom.cs +++ b/addons/forge/core/ForgeRandom.cs @@ -18,7 +18,7 @@ public class ForgeRandom : IRandom, IDisposable public void NextBytes(byte[] buffer) { - for (var i = 0; i < buffer.Length; i++) + for (int i = 0; i < buffer.Length; i++) { buffer[i] = (byte)_randomNumberGenerator.RandiRange(0, 255); } @@ -26,13 +26,25 @@ public class ForgeRandom : IRandom, IDisposable public void NextBytes(Span buffer) { - for (var i = 0; i < buffer.Length; i++) + for (int i = 0; i < buffer.Length; i++) { buffer[i] = (byte)_randomNumberGenerator.RandiRange(0, 255); } } public double NextDouble() + { + double value; + do + { + value = _randomNumberGenerator.Randf(); + } + while (value >= 1.0d); + + return value; + } + + public double NextDoubleInclusive() { return _randomNumberGenerator.Randf(); } @@ -52,12 +64,17 @@ public class ForgeRandom : IRandom, IDisposable return _randomNumberGenerator.RandiRange(minValue, maxValue - 1); } + public int NextIntInclusive(int minValue, int maxValue) + { + return _randomNumberGenerator.RandiRange(minValue, maxValue); + } + public long NextInt64() { unchecked { - var high = _randomNumberGenerator.Randi(); - var low = _randomNumberGenerator.Randi(); + uint high = _randomNumberGenerator.Randi(); + uint low = _randomNumberGenerator.Randi(); return ((long)high << 32) | low; } } @@ -74,13 +91,47 @@ public class ForgeRandom : IRandom, IDisposable throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be less than maxValue."); } - var range = (ulong)(maxValue - minValue); - var rand = (ulong)NextInt64(); + ulong range = (ulong)(maxValue - minValue); + ulong rand = (ulong)NextInt64(); return (long)(rand % range) + minValue; } + public long NextInt64Inclusive(long minValue, long maxValue) + { + if (minValue > maxValue) + { + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be less than or equal to maxValue."); + } + + if (minValue == maxValue) + { + return minValue; + } + + if (maxValue == long.MaxValue) + { + ulong inclusiveRange = (ulong)(maxValue - minValue) + 1UL; + ulong rand = (ulong)NextInt64(); + return (long)(rand % inclusiveRange) + minValue; + } + + return NextInt64(minValue, maxValue + 1); + } + public float NextSingle() + { + float value; + do + { + value = _randomNumberGenerator.Randf(); + } + while (value >= 1.0f); + + return value; + } + + public float NextSingleInclusive() { return _randomNumberGenerator.Randf(); } diff --git a/addons/forge/core/StatescriptGraphBuilder.cs b/addons/forge/core/StatescriptGraphBuilder.cs index 17c29eee..15acbb17 100644 --- a/addons/forge/core/StatescriptGraphBuilder.cs +++ b/addons/forge/core/StatescriptGraphBuilder.cs @@ -73,8 +73,8 @@ public static class StatescriptGraphBuilder continue; } - var outputPortIndex = connectionResource.OutputPort; - var inputPortIndex = connectionResource.InputPort; + int outputPortIndex = connectionResource.OutputPort; + int inputPortIndex = connectionResource.InputPort; if (outputPortIndex < 0 || outputPortIndex >= fromNode.OutputPorts.Length) { @@ -120,7 +120,7 @@ public static class StatescriptGraphBuilder if (variable.IsArray) { var initialValues = new Variant128[variable.InitialArrayValues.Count]; - for (var i = 0; i < variable.InitialArrayValues.Count; i++) + for (int i = 0; i < variable.InitialArrayValues.Count; i++) { initialValues[i] = StatescriptVariableTypeConverter.GodotVariantToForge( variable.InitialArrayValues[i], @@ -167,7 +167,7 @@ public static class StatescriptGraphBuilder continue; } - var index = (byte)binding.PropertyIndex; + byte index = (byte)binding.PropertyIndex; if (binding.Direction == StatescriptPropertyDirection.Input) { @@ -251,11 +251,11 @@ public static class StatescriptGraphBuilder 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++) + object[] args = new object[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) { ParameterInfo param = parameters[i]; - var paramName = param.Name ?? string.Empty; + string paramName = param.Name ?? string.Empty; if (nodeResource.CustomData.TryGetValue(paramName, out GodotVariant value)) { @@ -292,6 +292,20 @@ public static class StatescriptGraphBuilder private static object ConvertParameter(GodotVariant value, Type targetType) { + if (targetType.IsEnum) + { + if (value.VariantType == GodotVariant.Type.Int || value.VariantType == GodotVariant.Type.Float) + { + return Enum.ToObject(targetType, value.AsInt32()); + } + + string enumText = value.AsString(); + if (!string.IsNullOrEmpty(enumText)) + { + return Enum.Parse(targetType, enumText, ignoreCase: true); + } + } + if (targetType == typeof(StringKey)) { return new StringKey(value.AsString()); diff --git a/addons/forge/core/statescript/nodes/action/DebugNode.cs b/addons/forge/core/statescript/nodes/action/DebugNode.cs new file mode 100644 index 00000000..2dc59d23 --- /dev/null +++ b/addons/forge/core/statescript/nodes/action/DebugNode.cs @@ -0,0 +1,83 @@ +// Copyright © Gamesmiths Guild. + +using System; +using System.Collections.Generic; +using Gamesmiths.Forge.Godot.Resources.Statescript; +using Godot; + +namespace Gamesmiths.Forge.Statescript.Nodes.Action; + +/// +/// Action node that resolves an input value of any supported type and prints it through +/// . +/// Useful for validating resolver chains while testing Statescript graphs in the editor. +/// +public sealed class DebugNode : ActionNode +{ + private readonly StatescriptVariableType _valueType; + + /// + public override string Description => "Prints the resolved input value to the Godot console for debugging."; + + public DebugNode(StatescriptVariableType valueType = StatescriptVariableType.Int) + { + _valueType = valueType; + } + + /// + protected override void DefineParameters( + List inputProperties, + List outputVariables) + { + inputProperties.Add(new InputProperty("Value", StatescriptVariableTypeConverter.ToSystemType(_valueType))); + } + + /// + protected override void Execute(GraphContext graphContext) + { + if (!graphContext.TryResolveVariant(InputProperties[0].BoundName, out Variant128 value)) + { + GD.Print("[Statescript Debug] "); + return; + } + + GD.Print("[Statescript Debug] ", FormatValue(value)); + } + + private string FormatValue(Variant128 value) + { + return _valueType switch + { + StatescriptVariableType.Bool => value.AsBool().ToString(), + StatescriptVariableType.Byte => value.AsByte().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.SByte => value.AsSByte().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Char => value.AsChar().ToString(), + StatescriptVariableType.Decimal => value.AsDecimal().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Double => value.AsDouble().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Float => value.AsFloat().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Int => value.AsInt().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.UInt => value.AsUInt().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Long => value.AsLong().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.ULong => value.AsULong().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Short => value.AsShort().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.UShort => value.AsUShort().ToString( + System.Globalization.CultureInfo.InvariantCulture), + StatescriptVariableType.Vector2 => value.AsVector2().ToString(), + StatescriptVariableType.Vector3 => value.AsVector3().ToString(), + StatescriptVariableType.Vector4 => value.AsVector4().ToString(), + StatescriptVariableType.Plane => value.AsPlane().ToString(), + StatescriptVariableType.Quaternion => value.AsQuaternion().ToString(), + _ => Convert.ToHexString(value.ToBytes()), + }; + } +} diff --git a/addons/forge/core/statescript/nodes/action/DebugNode.cs.uid b/addons/forge/core/statescript/nodes/action/DebugNode.cs.uid new file mode 100644 index 00000000..637c2256 --- /dev/null +++ b/addons/forge/core/statescript/nodes/action/DebugNode.cs.uid @@ -0,0 +1 @@ +uid://byp7r8mspi3df diff --git a/addons/forge/editor/AssetRepairTool.cs b/addons/forge/editor/AssetRepairTool.cs index f9ca01f5..47637a50 100644 --- a/addons/forge/editor/AssetRepairTool.cs +++ b/addons/forge/editor/AssetRepairTool.cs @@ -23,12 +23,12 @@ public partial class AssetRepairTool : EditorPlugin List scenes = GetScenePaths("res://"); GD.Print($"Found {scenes.Count} scene(s) to process."); - var openedScenes = EditorInterface.Singleton.GetOpenScenes(); + string[] openedScenes = EditorInterface.Singleton.GetOpenScenes(); - foreach (var originalScenePath in scenes) + foreach (string originalScenePath in scenes) { // For some weird reason scenes from the GetScenePath are coming with 3 slashes instead of just two. - var scenePath = originalScenePath.Replace("res:///", "res://"); + string scenePath = originalScenePath.Replace("res:///", "res://"); GD.Print($"Processing scene: {scenePath}."); PackedScene? packedScene = ResourceLoader.Load(scenePath); @@ -40,7 +40,7 @@ public partial class AssetRepairTool : EditorPlugin } Node sceneInstance = packedScene.Instantiate(); - var modified = ProcessNode(sceneInstance, tagsManager); + bool modified = ProcessNode(sceneInstance, tagsManager); if (!modified) { @@ -92,13 +92,13 @@ public partial class AssetRepairTool : EditorPlugin dir.ListDirBegin(); while (true) { - var fileName = dir.GetNext(); + string fileName = dir.GetNext(); if (string.IsNullOrEmpty(fileName)) { break; } - var filePath = $"{basePath}/{fileName}"; + string filePath = $"{basePath}/{fileName}"; if (dir.CurrentIsDir()) { // Recursively scan subdirectories. @@ -123,7 +123,7 @@ public partial class AssetRepairTool : EditorPlugin /// if any ForgeEntity was modified. private static bool ProcessNode(Node node, TagsManager tagsManager) { - var modified = ValidateNode(node, tagsManager); + bool modified = ValidateNode(node, tagsManager); foreach (Node child in node.GetChildren()) { @@ -135,7 +135,7 @@ public partial class AssetRepairTool : EditorPlugin private static bool ValidateNode(Node node, TagsManager tagsManager) { - var modified = false; + bool modified = false; foreach (Dictionary propertyInfo in node.GetPropertyList()) { if (!propertyInfo.TryGetValue("class_name", out Variant className)) @@ -153,7 +153,7 @@ public partial class AssetRepairTool : EditorPlugin continue; } - var propertyName = nameObj.AsString(); + string propertyName = nameObj.AsString(); Variant value = node.Get(propertyName); if (value.VariantType != Variant.Type.Object) @@ -182,9 +182,9 @@ public partial class AssetRepairTool : EditorPlugin Array originalTags = container.ContainerTags; var newTags = new Array(); - var modified = false; + bool modified = false; - foreach (var tag in originalTags) + foreach (string tag in originalTags) { try { diff --git a/addons/forge/editor/attributes/AttributeEditorProperty.cs b/addons/forge/editor/attributes/AttributeEditorProperty.cs index 02998b54..e2b65504 100644 --- a/addons/forge/editor/attributes/AttributeEditorProperty.cs +++ b/addons/forge/editor/attributes/AttributeEditorProperty.cs @@ -11,7 +11,10 @@ public partial class AttributeEditorProperty : EditorProperty, ISerializationLis private const int ButtonSize = 26; private const int PopupSize = 300; - private Label _label = null!; + private Label? _label; + private Button? _button; + private Popup? _popup; + private Tree? _tree; public override void _Ready() { @@ -19,20 +22,20 @@ public partial class AttributeEditorProperty : EditorProperty, ISerializationLis var hBox = new HBoxContainer(); _label = new Label { Text = "None", SizeFlagsHorizontal = SizeFlags.ExpandFill }; - var button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) }; + _button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) }; hBox.AddChild(_label); - hBox.AddChild(button); + hBox.AddChild(_button); AddChild(hBox); - var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) }; - var tree = new Tree + _popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) }; + _tree = new Tree { HideRoot = true, AnchorRight = 1, AnchorBottom = 1, }; - popup.AddChild(tree); + _popup.AddChild(_tree); var bg = new StyleBoxFlat { @@ -40,74 +43,125 @@ public partial class AttributeEditorProperty : EditorProperty, ISerializationLis .GetEditorTheme() .GetColor("dark_color_2", "Editor"), }; - tree.AddThemeStyleboxOverride("panel", bg); + _tree.AddThemeStyleboxOverride("panel", bg); - AddChild(popup); + AddChild(_popup); - BuildAttributeTree(tree); + BuildAttributeTree(_tree); - button.Pressed += () => - { - Window win = GetWindow(); - popup.Position = (Vector2I)button.GlobalPosition - + win.Position - - new Vector2I(PopupSize - ButtonSize, -30); - popup.Popup(); - }; - - tree.ItemActivated += () => - { - TreeItem item = tree.GetSelected(); - if (item?.HasMeta("attribute_path") != true) - { - return; - } - - var fullPath = item.GetMeta("attribute_path").AsString(); - _label.Text = fullPath; - EmitChanged(GetEditedProperty(), fullPath); - popup.Hide(); - }; + _button.Pressed += OnButtonPressed; + _tree.ItemActivated += OnTreeItemActivated; } public override void _UpdateProperty() { - var value = GetEditedObject().Get(GetEditedProperty()).AsString(); + if (_label is null || !IsInstanceValid(_label)) + { + return; + } + + string value = GetEditedObject().Get(GetEditedProperty()).AsString(); _label.Text = string.IsNullOrEmpty(value) ? "None" : value; } + public override void _ExitTree() + { + ReleaseUiState(); + FreeAllChildren(); + base._ExitTree(); + } + public void OnBeforeSerialize() { - for (var i = GetChildCount() - 1; i >= 0; i--) - { - Node child = GetChild(i); - RemoveChild(child); - child.Free(); - } + ReleaseUiState(); + FreeAllChildren(); } public void OnAfterDeserialize() { + // This method was intentionally left blank. } private static void BuildAttributeTree(Tree tree) { TreeItem root = tree.CreateItem(); - foreach (var attributeSet in EditorUtils.GetAttributeSetOptions()) + foreach (string attributeSet in EditorUtils.GetAttributeSetOptions()) { TreeItem setItem = tree.CreateItem(root); setItem.SetText(0, attributeSet); setItem.Collapsed = true; - foreach (var attribute in EditorUtils.GetAttributeOptions(attributeSet)) + foreach (string attribute in EditorUtils.GetAttributeOptions(attributeSet)) { TreeItem attributeItem = tree.CreateItem(setItem); - var attributePath = $"{attributeSet}.{attribute}"; + string attributePath = $"{attributeSet}.{attribute}"; attributeItem.SetText(0, attribute); attributeItem.SetMeta("attribute_path", attributePath); } } } + + private void OnButtonPressed() + { + if (_button is null || _popup is null || !IsInstanceValid(_button) || !IsInstanceValid(_popup)) + { + return; + } + + Window win = GetWindow(); + _popup.Position = (Vector2I)_button.GlobalPosition + + win.Position + - new Vector2I(PopupSize - ButtonSize, -30); + _popup.Popup(); + } + + private void OnTreeItemActivated() + { + if (_tree is null || _popup is null || _label is null + || !IsInstanceValid(_tree) || !IsInstanceValid(_popup) || !IsInstanceValid(_label)) + { + return; + } + + TreeItem item = _tree.GetSelected(); + if (item?.HasMeta("attribute_path") != true) + { + return; + } + + string fullPath = item.GetMeta("attribute_path").AsString(); + _label.Text = fullPath; + EmitChanged(GetEditedProperty(), fullPath); + _popup.Hide(); + } + + private void ReleaseUiState() + { + if (_button is not null && IsInstanceValid(_button)) + { + _button.Pressed -= OnButtonPressed; + } + + if (_tree is not null && IsInstanceValid(_tree)) + { + _tree.ItemActivated -= OnTreeItemActivated; + } + + _label = null; + _button = null; + _popup = null; + _tree = null; + } + + private void FreeAllChildren() + { + for (int i = GetChildCount() - 1; i >= 0; i--) + { + Node child = GetChild(i); + RemoveChild(child); + child.Free(); + } + } } #endif diff --git a/addons/forge/editor/attributes/AttributeSetClassEditorProperty.cs b/addons/forge/editor/attributes/AttributeSetClassEditorProperty.cs index b08e9788..ea92b4ea 100644 --- a/addons/forge/editor/attributes/AttributeSetClassEditorProperty.cs +++ b/addons/forge/editor/attributes/AttributeSetClassEditorProperty.cs @@ -14,7 +14,7 @@ namespace Gamesmiths.Forge.Godot.Editor.Attributes; [Tool] public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializationListener { - private OptionButton _optionButton = null!; + private OptionButton? _optionButton; public override void _Ready() { @@ -22,60 +22,25 @@ public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializ AddChild(_optionButton); _optionButton.AddItem("Select AttributeSet Class"); - foreach (var option in EditorUtils.GetAttributeSetOptions()) + foreach (string option in EditorUtils.GetAttributeSetOptions()) { _optionButton.AddItem(option); } - _optionButton.ItemSelected += x => - { - var className = _optionButton.GetItemText((int)x); - EmitChanged(GetEditedProperty(), className); - - GodotObject @object = GetEditedObject(); - if (@object is not null) - { - var dictionary = new Dictionary(); - - var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet)); - Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className); - if (targetType is not null) - { - System.Collections.Generic.IEnumerable attributeProperties = targetType - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => x.PropertyType == typeof(EntityAttribute)); - - foreach (var propertyName in attributeProperties.Select(x => x.Name)) - { - if (@object is not ForgeAttributeSet forgeAttributeSet) - { - dictionary[propertyName] = new AttributeValues(0, 0, int.MaxValue); - continue; - } - - AttributeSet? attributeSet = forgeAttributeSet.GetAttributeSet(); - if (attributeSet is null) - { - dictionary[propertyName] = new AttributeValues(0, 0, int.MaxValue); - continue; - } - - EntityAttribute key = attributeSet.AttributesMap[className + "." + propertyName]; - dictionary[propertyName] = new AttributeValues(key.CurrentValue, key.Min, key.Max); - } - } - - EmitChanged("InitialAttributeValues", dictionary); - } - }; + _optionButton.ItemSelected += OnItemSelected; } public override void _UpdateProperty() { + if (_optionButton is null || !IsInstanceValid(_optionButton)) + { + return; + } + GodotObject obj = GetEditedObject(); StringName property = GetEditedProperty(); - var val = obj.Get(property).AsString(); - for (var i = 0; i < _optionButton.GetItemCount(); i++) + string val = obj.Get(property).AsString(); + for (int i = 0; i < _optionButton.GetItemCount(); i++) { if (_optionButton.GetItemText(i) == val) { @@ -85,18 +50,90 @@ public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializ } } + public override void _ExitTree() + { + ReleaseUiState(); + FreeAllChildren(); + base._ExitTree(); + } + public void OnBeforeSerialize() { - for (var i = GetChildCount() - 1; i >= 0; i--) + ReleaseUiState(); + FreeAllChildren(); + } + + public void OnAfterDeserialize() + { + } + + private void OnItemSelected(long index) + { + if (_optionButton is null || !IsInstanceValid(_optionButton)) + { + return; + } + + string className = _optionButton.GetItemText((int)index); + EmitChanged(GetEditedProperty(), className); + + GodotObject @object = GetEditedObject(); + if (@object is null) + { + return; + } + + var dictionary = new Dictionary(); + + var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet)); + Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className); + if (targetType is not null) + { + System.Collections.Generic.IEnumerable attributeProperties = targetType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.PropertyType == typeof(EntityAttribute)); + + foreach (string? propertyName in attributeProperties.Select(x => x.Name)) + { + if (@object is not ForgeAttributeSet forgeAttributeSet) + { + dictionary[propertyName] = new AttributeValues(0, 0, int.MaxValue); + continue; + } + + AttributeSet? attributeSet = forgeAttributeSet.GetAttributeSet(); + if (attributeSet is null) + { + dictionary[propertyName] = new AttributeValues(0, 0, int.MaxValue); + continue; + } + + EntityAttribute key = attributeSet.AttributesMap[className + "." + propertyName]; + dictionary[propertyName] = new AttributeValues(key.CurrentValue, key.Min, key.Max); + } + } + + EmitChanged("InitialAttributeValues", dictionary); + } + + private void ReleaseUiState() + { + if (_optionButton is not null && IsInstanceValid(_optionButton)) + { + _optionButton.ItemSelected -= OnItemSelected; + } + + _optionButton = null; + } + + private void FreeAllChildren() + { + for (int i = GetChildCount() - 1; i >= 0; i--) { Node child = GetChild(i); RemoveChild(child); child.Free(); } } - - public void OnAfterDeserialize() - { - } } #endif diff --git a/addons/forge/editor/attributes/AttributeSetValuesEditorProperty.cs b/addons/forge/editor/attributes/AttributeSetValuesEditorProperty.cs index b06b1b63..6f5bdc5b 100644 --- a/addons/forge/editor/attributes/AttributeSetValuesEditorProperty.cs +++ b/addons/forge/editor/attributes/AttributeSetValuesEditorProperty.cs @@ -40,7 +40,7 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty, ISeriali return; } - var className = obj.AttributeSetClass; + string className = obj.AttributeSetClass; var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet)); Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className); @@ -53,7 +53,7 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty, ISeriali .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(x => x.PropertyType == typeof(EntityAttribute)); - foreach (var attributeName in attributeProperties.Select(x => x.Name)) + foreach (string? attributeName in attributeProperties.Select(x => x.Name)) { var groupVBox = new VBoxContainer(); @@ -99,7 +99,7 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty, ISeriali VBoxContainer? attributesRoot = GetNodeOrNull("AttributesRoot"); if (attributesRoot is not null) { - for (var i = attributesRoot.GetChildCount() - 1; i >= 0; i--) + for (int i = attributesRoot.GetChildCount() - 1; i >= 0; i--) { Node child = attributesRoot.GetChild(i); attributesRoot.RemoveChild(child); @@ -169,7 +169,7 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty, ISeriali private static void FreeAllChildren(Node node) { - for (var i = node.GetChildCount() - 1; i >= 0; i--) + for (int i = node.GetChildCount() - 1; i >= 0; i--) { node.GetChild(i).QueueFree(); } diff --git a/addons/forge/editor/cues/CueHandlerInspectorPlugin.cs b/addons/forge/editor/cues/CueHandlerInspectorPlugin.cs index 87b973fb..246e2f65 100644 --- a/addons/forge/editor/cues/CueHandlerInspectorPlugin.cs +++ b/addons/forge/editor/cues/CueHandlerInspectorPlugin.cs @@ -13,28 +13,22 @@ public partial class CueHandlerInspectorPlugin : EditorInspectorPlugin public override bool _CanHandle(GodotObject @object) { // Find out if its an implementation of CueHandler without having to add [Tool] attribute to them. - try + if (@object?.GetScript().As() is CSharpScript script) { - if (@object?.GetScript().As() is null) - return false; - } - catch (Exception e) - { - return false; + StringName className = script.GetGlobalName(); + + Type baseType = typeof(ForgeCueHandler); + System.Reflection.Assembly assembly = baseType.Assembly; + + Type? implementationType = + Array.Find(assembly.GetTypes(), x => + x.Name == className && + baseType.IsAssignableFrom(x)); + + return implementationType is not null; } - var script = @object?.GetScript().As(); - StringName className = script.GetGlobalName(); - - Type baseType = typeof(ForgeCueHandler); - System.Reflection.Assembly assembly = baseType.Assembly; - - Type? implementationType = - Array.Find(assembly.GetTypes(), x => - x.Name == className && - baseType.IsAssignableFrom(x)); - - return implementationType is not null; + return false; } public override bool _ParseProperty( diff --git a/addons/forge/editor/cues/CueKeyEditorProperty.cs b/addons/forge/editor/cues/CueKeyEditorProperty.cs index a8c151e1..c9e13ceb 100644 --- a/addons/forge/editor/cues/CueKeyEditorProperty.cs +++ b/addons/forge/editor/cues/CueKeyEditorProperty.cs @@ -9,12 +9,15 @@ using Godot; namespace Gamesmiths.Forge.Godot.Editor.Cues; [Tool] -public partial class CueKeyEditorProperty : EditorProperty +public partial class CueKeyEditorProperty : EditorProperty, ISerializationListener { private const int ButtonSize = 26; private const int PopupSize = 300; - private Label _label = null!; + private Label? _label; + private Button? _button; + private Popup? _popup; + private Tree? _tree; public override void _Ready() { @@ -24,72 +27,63 @@ public partial class CueKeyEditorProperty : EditorProperty var hbox = new HBoxContainer(); _label = new Label { Text = "None", SizeFlagsHorizontal = SizeFlags.ExpandFill }; - var button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) }; + _button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) }; hbox.AddChild(_label); - hbox.AddChild(button); + hbox.AddChild(_button); AddChild(hbox); - var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) }; - var tree = new Tree + _popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) }; + _tree = new Tree { HideRoot = true, AnchorRight = 1, AnchorBottom = 1, }; - popup.AddChild(tree); + _popup.AddChild(_tree); var backgroundStyle = new StyleBoxFlat { BgColor = EditorInterface.Singleton.GetEditorTheme().GetColor("base_color", "Editor"), }; - tree.AddThemeStyleboxOverride("panel", backgroundStyle); + _tree.AddThemeStyleboxOverride("panel", backgroundStyle); - AddChild(popup); + AddChild(_popup); ForgeData pluginData = ResourceLoader.Load(ForgeData.ForgeDataResourcePath); var tagsManager = new TagsManager([.. pluginData.RegisteredTags]); - TreeItem root = tree.CreateItem(); - BuildTreeRecursively(tree, root, tagsManager.RootNode); + TreeItem root = _tree.CreateItem(); + BuildTreeRecursively(_tree, root, tagsManager.RootNode); - button.Pressed += () => - { - Window win = GetWindow(); - popup.Position = (Vector2I)button.GlobalPosition - + win.Position - - new Vector2I(PopupSize - ButtonSize, -30); - popup.Popup(); - }; - - tree.ItemActivated += () => - { - TreeItem item = tree.GetSelected(); - if (item is null) - { - return; - } - - // Build full path from root. - var segments = new List(); - TreeItem current = item; - while (current.GetParent() is not null) - { - segments.Insert(0, current.GetText(0)); - current = current.GetParent(); - } - - var fullPath = string.Join(".", segments); - - _label.Text = fullPath; - EmitChanged(GetEditedProperty(), fullPath); - popup.Hide(); - }; + _button.Pressed += OnButtonPressed; + _tree.ItemActivated += OnTreeItemActivated; } public override void _UpdateProperty() { - var property = GetEditedObject().Get(GetEditedProperty()).AsString(); - _label.Text = string.IsNullOrEmpty(property) ? "None" : property; + string property = GetEditedObject().Get(GetEditedProperty()).AsString(); + + if (_label is not null && IsInstanceValid(_label)) + { + _label.Text = string.IsNullOrEmpty(property) ? "None" : property; + } + } + + public override void _ExitTree() + { + ReleaseUiState(); + FreeAllChildren(); + base._ExitTree(); + } + + public void OnBeforeSerialize() + { + ReleaseUiState(); + FreeAllChildren(); + } + + public void OnAfterDeserialize() + { } private static void BuildTreeRecursively(Tree tree, TreeItem currentTreeItem, TagNode currentNode) @@ -102,5 +96,76 @@ public partial class CueKeyEditorProperty : EditorProperty BuildTreeRecursively(tree, childTreeNode, childTagNode); } } + + private void OnButtonPressed() + { + if (_button is null || _popup is null || !IsInstanceValid(_button) || !IsInstanceValid(_popup)) + { + return; + } + + Window win = GetWindow(); + _popup.Position = (Vector2I)_button.GlobalPosition + + win.Position + - new Vector2I(PopupSize - ButtonSize, -30); + _popup.Popup(); + } + + private void OnTreeItemActivated() + { + if (_tree is null || _popup is null || _label is null + || !IsInstanceValid(_tree) || !IsInstanceValid(_popup) || !IsInstanceValid(_label)) + { + return; + } + + TreeItem item = _tree.GetSelected(); + if (item is null) + { + return; + } + + var segments = new List(); + TreeItem current = item; + while (current.GetParent() is not null) + { + segments.Insert(0, current.GetText(0)); + current = current.GetParent(); + } + + string fullPath = string.Join(".", segments); + + _label.Text = fullPath; + EmitChanged(GetEditedProperty(), fullPath); + _popup.Hide(); + } + + private void ReleaseUiState() + { + if (_button is not null && IsInstanceValid(_button)) + { + _button.Pressed -= OnButtonPressed; + } + + if (_tree is not null && IsInstanceValid(_tree)) + { + _tree.ItemActivated -= OnTreeItemActivated; + } + + _label = null; + _button = null; + _popup = null; + _tree = null; + } + + private void FreeAllChildren() + { + for (int i = GetChildCount() - 1; i >= 0; i--) + { + Node child = GetChild(i); + RemoveChild(child); + child.Free(); + } + } } #endif diff --git a/addons/forge/editor/statescript/CustomNodeEditor.cs b/addons/forge/editor/statescript/CustomNodeEditor.cs index 794a4358..eea52052 100644 --- a/addons/forge/editor/statescript/CustomNodeEditor.cs +++ b/addons/forge/editor/statescript/CustomNodeEditor.cs @@ -172,6 +172,26 @@ internal abstract partial class CustomNodeEditor : RefCounted return _graphNode!.GetFoldStateInternal(key); } + /// + /// Gets the persisted fold state for a given key, with a custom default when unset. + /// + /// The key used to persist the fold state. + /// The default fold state when no persisted value exists. + protected bool GetFoldState(string key, bool defaultValue) + { + return _graphNode!.GetFoldStateInternal(key, defaultValue); + } + + /// + /// Persists a fold state change with undo support. + /// + /// The key used to persist the fold state. + /// The new folded state. + protected void SetFoldStateWithUndo(string key, bool folded) + { + _graphNode!.SetFoldStateWithUndoInternal(key, folded); + } + /// /// Finds an existing property binding by direction and index. /// @@ -245,6 +265,14 @@ internal abstract partial class CustomNodeEditor : RefCounted _graphNode!.ResetSize(); } + /// + /// Refreshes standard input-property foldable summaries. + /// + protected void RefreshInputPropertyFoldableTitles() + { + _graphNode!.UpdateInputPropertyFoldableTitlesInternal(); + } + /// /// Raises the event. /// diff --git a/addons/forge/editor/statescript/InlineConstantSummaryFormatter.cs b/addons/forge/editor/statescript/InlineConstantSummaryFormatter.cs new file mode 100644 index 00000000..4024fd04 --- /dev/null +++ b/addons/forge/editor/statescript/InlineConstantSummaryFormatter.cs @@ -0,0 +1,656 @@ +// Copyright © Gamesmiths Guild. + +#if TOOLS +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Gamesmiths.Forge.Godot.Resources.Statescript; +using Godot; + +namespace Gamesmiths.Forge.Godot.Editor.Statescript; + +internal static class InlineConstantSummaryFormatter +{ + private const string SummaryBadgeMetaKey = "forge_inline_summary_badge"; + private const string SummaryBadgeInstanceIdMetaKey = "forge_inline_summary_badge_instance_id"; + private const string SummaryBadgeResizeHookMetaKey = "forge_inline_summary_badge_resize_hook"; + private const string SummaryBadgeKindMetaKey = "forge_inline_summary_badge_kind"; + private const string SummaryBadgeTextMetaKey = "forge_inline_summary_badge_text"; + private const string SummaryBadgeSelectedVariableMetaKey = "forge_inline_summary_badge_selected_variable"; + + private const string SummaryBadgeSelectedSharedVariableSetPathMetaKey = + "forge_inline_summary_badge_selected_shared_set_path"; + + private const string SummaryBadgeSelectedSharedVariableMetaKey = + "forge_inline_summary_badge_selected_shared_variable"; + + private const float MinimumBadgeWidth = 76f; + private const float FoldableTitleChromeWidth = 30f; + private const float FoldableTitleBadgeGap = 6f; + private const float FoldableTitleRightPadding = 8f; + + private static readonly Color _numericIconColor = new(0x3dbcc9ff); + private static readonly Color _numericBackgroundColor = new(0x3dbcc918); + private static readonly Color _numericBorderColor = new(0x3dbcc9ff); + private static readonly Color _vectorIconColor = new(0xd48a3aff); + private static readonly Color _vectorBackgroundColor = new(0xd48a3a18); + private static readonly Color _vectorBorderColor = new(0xd48a3aff); + private static readonly Color _booleanIconColor = new(0xc2a24fff); + private static readonly Color _booleanBackgroundColor = new(0xc2a24f18); + private static readonly Color _booleanBorderColor = new(0xc2a24fff); + private static readonly Color _resolverIconColor = new(0xc06bcfff); + private static readonly Color _resolverBackgroundColor = new(0xc06bcf18); + private static readonly Color _resolverBorderColor = new(0xc06bcfff); + private static readonly Color _variableIconColor = new(0x5d7be0ff); + private static readonly Color _variableBackgroundColor = new(0x5d7be018); + private static readonly Color _variableBorderColor = new(0x5d7be0ff); + private static readonly Color _sharedVariableIconColor = new(0x46a86fff); + private static readonly Color _sharedVariableBackgroundColor = new(0x46a86f18); + private static readonly Color _sharedVariableBorderColor = new(0x46a86fff); + private static readonly Color _enumIconColor = new(0xc0c6d1ff); + private static readonly Color _enumBackgroundColor = new(0xc0c6d118); + private static readonly Color _enumBorderColor = new(0xc0c6d1ff); + + public static void ApplyFoldableTitle( + string baseTitle, + FoldableContainer foldable, + NodeEditorProperty? editor) + { + EnsureResizeSyncHook(foldable); + foldable.Title = baseTitle; + + SummaryBadgeData badgeData = GetBadgeData(foldable, editor); + PanelContainer badge = GetOrCreateSummaryBadge(foldable); + ConfigureSummaryBadge(badge, badgeData); + SynchronizeSiblingBadgeWidths(foldable); + } + + public static void ApplyFoldableTitle( + string baseTitle, + FoldableContainer foldable, + string? summary, + InlineSummaryBadgeKind badgeKind, + bool isConstant = false, + string? highlightedVariableName = null, + string? highlightedSharedVariableSetPath = null, + string? highlightedSharedVariableName = null) + { + EnsureResizeSyncHook(foldable); + foldable.Title = baseTitle; + + SummaryBadgeData badgeData = foldable.Folded && !string.IsNullOrWhiteSpace(summary) + ? CreateBadgeData( + summary, + badgeKind, + isConstant, + highlightedVariableName, + highlightedSharedVariableSetPath, + highlightedSharedVariableName) + : SummaryBadgeData.Hidden; + + PanelContainer badge = GetOrCreateSummaryBadge(foldable); + ConfigureSummaryBadge(badge, badgeData); + SynchronizeSiblingBadgeWidths(foldable); + } + + public static string GetFoldableTitle( + string baseTitle, + FoldableContainer foldable, + NodeEditorProperty? editor) + { + if (!foldable.Folded || editor is null) + { + return baseTitle; + } + + if (editor.TryGetInlineSummary(out string summary) && !string.IsNullOrWhiteSpace(summary)) + { + return $"{baseTitle} {summary}"; + } + + return string.IsNullOrWhiteSpace(editor.DisplayName) + ? baseTitle + : $"{baseTitle} {editor.DisplayName}"; + } + + public static string FormatVariant(Variant value, StatescriptVariableType valueType) + { + return valueType switch + { + StatescriptVariableType.Bool => value.AsBool() ? "True" : "False", + StatescriptVariableType.Byte => value.AsInt32().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.SByte => value.AsInt32().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.Char => ((char)value.AsInt32()).ToString(), + StatescriptVariableType.Decimal => value.AsDouble().ToString("G", CultureInfo.InvariantCulture), + StatescriptVariableType.Double => value.AsDouble().ToString("G", CultureInfo.InvariantCulture), + StatescriptVariableType.Float => value.AsSingle().ToString("G", CultureInfo.InvariantCulture), + StatescriptVariableType.Int => value.AsInt32().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.UInt => value.AsInt64().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.Long => value.AsInt64().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.ULong => value.AsInt64().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.Short => value.AsInt32().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.UShort => value.AsInt32().ToString(CultureInfo.InvariantCulture), + StatescriptVariableType.Vector2 => FormatVector2(value.AsVector2()), + StatescriptVariableType.Vector3 => FormatVector3(value.AsVector3()), + StatescriptVariableType.Vector4 => FormatVector4(value.AsVector4()), + StatescriptVariableType.Plane => FormatPlane(value.AsPlane()), + StatescriptVariableType.Quaternion => FormatQuaternion(value.AsQuaternion()), + _ => value.ToString(), + }; + } + + public static InlineSummaryBadgeKind GetBadgeKind(StatescriptVariableType valueType) + { + return valueType switch + { + StatescriptVariableType.Bool => InlineSummaryBadgeKind.Boolean, + StatescriptVariableType.Vector2 => InlineSummaryBadgeKind.Vector, + StatescriptVariableType.Vector3 => InlineSummaryBadgeKind.Vector, + StatescriptVariableType.Vector4 => InlineSummaryBadgeKind.Vector, + StatescriptVariableType.Plane => InlineSummaryBadgeKind.Vector, + StatescriptVariableType.Quaternion => InlineSummaryBadgeKind.Vector, + _ => InlineSummaryBadgeKind.Numeric, + }; + } + + internal static bool TryGetSummaryBadgeForHighlighting( + FoldableContainer foldable, + [NotNullWhen(true)] out PanelContainer? badge) + { + return TryGetSummaryBadge(foldable, out badge); + } + + private static SummaryBadgeData GetBadgeData(FoldableContainer foldable, NodeEditorProperty? editor) + { + if (!foldable.Folded || editor is null) + { + return SummaryBadgeData.Hidden; + } + + string? highlightedVariableName = null; + string? highlightedSharedVariableSetPath = null; + string? highlightedSharedVariableName = null; + + if (editor.TryGetHighlightedVariableName(out string propagatedVariableName) + && !string.IsNullOrWhiteSpace(propagatedVariableName)) + { + highlightedVariableName = propagatedVariableName; + } + + if (editor.TryGetHighlightedSharedVariable( + out string propagatedSharedVariableSetPath, + out string propagatedSharedVariableName) + && !string.IsNullOrWhiteSpace(propagatedSharedVariableSetPath) + && !string.IsNullOrWhiteSpace(propagatedSharedVariableName)) + { + highlightedSharedVariableSetPath = propagatedSharedVariableSetPath; + highlightedSharedVariableName = propagatedSharedVariableName; + } + + if (editor.TryGetInlineSummary(out string summary) && !string.IsNullOrWhiteSpace(summary)) + { + InlineSummaryBadgeKind badgeKind = editor.GetInlineSummaryBadgeKind(); + return CreateBadgeData( + summary, + badgeKind, + IsConstantBadgeKind(badgeKind), + highlightedVariableName, + highlightedSharedVariableSetPath, + highlightedSharedVariableName); + } + + return string.IsNullOrWhiteSpace(editor.DisplayName) + ? SummaryBadgeData.Hidden + : CreateBadgeData( + editor.DisplayName, + InlineSummaryBadgeKind.Resolver, + false, + highlightedVariableName, + highlightedSharedVariableSetPath, + highlightedSharedVariableName); + } + + private static SummaryBadgeData CreateBadgeData( + string text, + InlineSummaryBadgeKind badgeKind, + bool isConstant, + string? highlightedVariableName = null, + string? highlightedSharedVariableSetPath = null, + string? highlightedSharedVariableName = null) + { + BadgeVisualStyle style = GetBadgeStyle(badgeKind); + return new SummaryBadgeData( + GetBadgeIcon(badgeKind, isConstant), + text, + highlightedVariableName ?? string.Empty, + highlightedSharedVariableSetPath ?? string.Empty, + highlightedSharedVariableName ?? string.Empty, + badgeKind, + isConstant, + style.IconColor, + style.BackgroundColor, + style.BorderColor, + true); + } + + private static PanelContainer GetOrCreateSummaryBadge(FoldableContainer foldable) + { + if (TryGetSummaryBadge(foldable, out PanelContainer? existingBadge)) + { + return existingBadge; + } + + var badge = new PanelContainer + { + Name = "InlineSummaryBadge", + Visible = false, + MouseFilter = Control.MouseFilterEnum.Ignore, + SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter, + }; + + var row = new HBoxContainer + { + Name = "Row", + MouseFilter = Control.MouseFilterEnum.Ignore, + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + }; + row.AddThemeConstantOverride("separation", 4); + badge.AddChild(row); + + var iconLabel = new Label + { + Name = "Icon", + MouseFilter = Control.MouseFilterEnum.Ignore, + SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + }; + row.AddChild(iconLabel); + + var textLabel = new Label + { + Name = "Text", + MouseFilter = Control.MouseFilterEnum.Ignore, + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis, + }; + row.AddChild(textLabel); + + foldable.AddTitleBarControl(badge); + foldable.SetMeta(SummaryBadgeMetaKey, Variant.From(true)); + foldable.SetMeta(SummaryBadgeInstanceIdMetaKey, Variant.From((long)badge.GetInstanceId())); + return badge; + } + + private static void EnsureResizeSyncHook(FoldableContainer foldable) + { + if (foldable.HasMeta(SummaryBadgeResizeHookMetaKey) + && foldable.GetMeta(SummaryBadgeResizeHookMetaKey).AsBool()) + { + return; + } + + foldable.Resized += () => SynchronizeSiblingBadgeWidths(foldable); + foldable.SetMeta(SummaryBadgeResizeHookMetaKey, Variant.From(true)); + } + + private static void ConfigureSummaryBadge(PanelContainer badge, SummaryBadgeData badgeData) + { + badge.Visible = badgeData.Visible; + if (!badgeData.Visible) + { + badge.SetMeta(SummaryBadgeKindMetaKey, Variant.From((int)InlineSummaryBadgeKind.Resolver)); + badge.SetMeta(SummaryBadgeTextMetaKey, Variant.From(string.Empty)); + badge.CustomMinimumSize = Vector2.Zero; + return; + } + + badge.SetMeta(SummaryBadgeKindMetaKey, Variant.From((int)badgeData.BadgeKind)); + badge.SetMeta(SummaryBadgeTextMetaKey, Variant.From(badgeData.Text)); + badge.SetMeta( + "forge_inline_summary_badge_highlight_variable", + Variant.From(badgeData.HighlightVariableName)); + badge.SetMeta( + "forge_inline_summary_badge_highlight_shared_set_path", + Variant.From(badgeData.HighlightSharedVariableSetPath)); + badge.SetMeta( + "forge_inline_summary_badge_highlight_shared_variable", + Variant.From(badgeData.HighlightSharedVariableName)); + + StyleBoxFlat styleBox = new() + { + BgColor = badgeData.BackgroundColor, + BorderColor = badgeData.BorderColor, + CornerRadiusTopLeft = 8, + CornerRadiusTopRight = 8, + CornerRadiusBottomRight = 8, + CornerRadiusBottomLeft = 8, + BorderWidthLeft = 1, + BorderWidthTop = 1, + BorderWidthRight = 1, + BorderWidthBottom = 1, + ContentMarginLeft = 8, + ContentMarginTop = 3, + ContentMarginRight = 8, + ContentMarginBottom = 3, + }; + + badge.AddThemeStyleboxOverride("panel", styleBox); + + Label? iconLabel = badge.GetNodeOrNull