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