Compare commits

...

33 Commits

Author SHA1 Message Date
b2ab80c54c trying to remove rider plugin before CI build
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 29s
Create tag and build when new code gets to main / Export (push) Successful in 7m46s
2026-05-17 11:08:41 +02:00
06ef5d892b test
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 25s
Create tag and build when new code gets to main / Export (push) Successful in 7m18s
2026-05-17 02:32:06 +02:00
e09714cf83 update forge
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 27s
Create tag and build when new code gets to main / Export (push) Successful in 7m25s
2026-05-17 00:06:44 +02:00
8b54f00814 trying workaround
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 25s
Create tag and build when new code gets to main / Export (push) Successful in 7m0s
2026-05-16 23:52:05 +02:00
415897b7b0 debug
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 24s
Create tag and build when new code gets to main / Export (push) Successful in 8m17s
2026-05-16 21:57:04 +02:00
d302d75238 c for crouching
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 24s
Create tag and build when new code gets to main / Export (push) Successful in 5m33s
Create tag and build when new code gets to main / ReleaseName (push) Successful in 3s
Create tag and build when new code gets to main / Release (push) Successful in 14m28s
2026-05-16 20:58:11 +02:00
dc81796d52 fixed cue issue and setup proper waves
Some checks failed
Create tag and build when new code gets to main / BumpTag (push) Successful in 26s
Create tag and build when new code gets to main / Export (push) Has been cancelled
2026-05-16 20:56:20 +02:00
2103832e46 all spawners available can be used on first wave tick 2026-05-16 19:56:07 +02:00
1898d91a28 wave behavior and fixed explosion 2026-05-16 19:48:48 +02:00
b3ae3e37ea fixed weapon loss bug 2026-05-16 13:19:01 +02:00
4cd67023d9 token manager for projectile 2026-05-16 01:29:02 +02:00
afa335e7bf stunnable targets on hit 2026-05-15 15:29:24 +02:00
a0e99a959f parrying projectiles
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 28s
Create tag and build when new code gets to main / Export (push) Successful in 6m44s
2026-05-14 16:11:22 +02:00
0cd942d90e homing projectiles 2026-05-10 12:26:43 +02:00
150e007b22 basic projectiles
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 26s
Create tag and build when new code gets to main / Export (push) Successful in 5m9s
2026-05-07 14:53:30 +02:00
01a2e7582b enemy grant hit ability + prep projectiles 2026-05-06 19:55:05 +02:00
7ba4a3db3f fixed a few issues
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 30s
Create tag and build when new code gets to main / Export (push) Successful in 6m0s
2026-05-06 16:25:56 +02:00
bcc748ca6b More weapon events and abilities 2026-05-06 11:05:55 +02:00
1db30eafd9 removed obsolete references and maps, fixed menu hide pause issue 2026-05-05 17:04:09 +02:00
68e36742af removed obsolete interfaces for health and damage
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 27s
Create tag and build when new code gets to main / Export (push) Successful in 5m25s
2026-05-05 11:51:35 +02:00
33f55d04f3 made explosion forge compliant 2026-05-05 10:55:12 +02:00
a139990390 knockback forge implemented 2026-05-04 16:22:30 +02:00
b2b7baffe8 making dash through target a dedicated dash action 2026-05-04 13:22:25 +02:00
bed1384dc7 fixed inventory + using Sync bindings
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 57s
Create tag and build when new code gets to main / Export (push) Successful in 5m13s
2026-05-04 10:19:00 +02:00
99f383be00 Damage dealing through meta attribute and custom exec
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 30s
Create tag and build when new code gets to main / Export (push) Successful in 6m8s
2026-05-03 10:47:56 +02:00
631935fdc8 added meta attribute set 2026-05-02 16:45:27 +02:00
852f265b9f made weapon into a dependant and used new forgeneitty node as a node3d 2026-05-02 16:13:23 +02:00
0e6211943d new forge entity node to benefit from autoinject 2026-05-02 15:24:28 +02:00
fb30a08b89 hitting is now an ability 2026-05-02 11:19:56 +02:00
24f057c15f used DI for forge managers where possible 2026-04-28 16:34:10 +02:00
ec44306d48 forge friendlier health and damage management
Removed knockback though
2026-04-28 11:22:24 +02:00
dcfd937e53 moved forge resources around 2026-04-26 18:19:45 +02:00
cd7a230615 made the initial inventory loadout into a resource to initialize the injected dependency with 2026-04-26 17:38:25 +02:00
673 changed files with 18558 additions and 4998 deletions

View File

@@ -89,61 +89,61 @@ jobs:
# name: Test Report
# path: ${{ github.workspace }}/reports/test-result.html
#
OtherTest:
runs-on: godot
env:
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
steps:
- name: Checkout with LFS
uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
- uses: actions/setup-dotnet@v5
name: 💽 Setup .NET SDK
with:
dotnet-version: '9.0.x'
- name: 📦 Restore Dependencies
run: |
dotnet --version
dotnet restore
dotnet build
dotnet list package
- uses: chickensoft-games/setup-godot@v2
name: 🤖 Setup Godot
with:
# Version must include major, minor, and patch, and be >= 4.0.0
# Pre-release label is optional.
version: '4.6.2'
# Use .NET-enabled version of Godot (the default is also true).
use-dotnet: true
# Include the Godot Export Templates (the default is false).
include-templates: true
- name: 🔬 Verify Setup
run: |
dotnet --version
godot --version
- name: 🧑‍🔬 Generate .NET Bindings
run: godot --headless --build-solutions --import --quit || exit 0
- name: 🦺 Build Projects
run: dotnet build --configuration ExportRelease
- name: Run C# Tests
env:
GODOT_BIN: /root/bin/godot
shell: bash
run: |
dotnet test --no-build --settings .runsettings --results-directory ./reports --logger "console;verbosity=normal" --logger "trx;LogFileName=results.xml" -- GdUnit4.Parameters="--headless --import --quit"
- name: Upload test report
uses: actions/upload-artifact@v3-node20
if: always()
with:
name: Test Report
path: ${{ github.workspace }}/reports/test-result.html
# OtherTest:
# runs-on: godot
# env:
# RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
# steps:
# - name: Checkout with LFS
# uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
#
# - uses: actions/setup-dotnet@v5
# name: 💽 Setup .NET SDK
# with:
# dotnet-version: '9.0.x'
#
# - name: 📦 Restore Dependencies
# run: |
# dotnet --version
# dotnet restore
# dotnet build
# dotnet list package
#
# - uses: chickensoft-games/setup-godot@v2
# name: 🤖 Setup Godot
# with:
# # Version must include major, minor, and patch, and be >= 4.0.0
# # Pre-release label is optional.
# version: '4.6.2'
# # Use .NET-enabled version of Godot (the default is also true).
# use-dotnet: true
# # Include the Godot Export Templates (the default is false).
# include-templates: true
#
# - name: 🔬 Verify Setup
# run: |
# dotnet --version
# godot --version
#
# - name: 🧑‍🔬 Generate .NET Bindings
# run: godot --headless --build-solutions --import --quit || exit 0
#
# - name: 🦺 Build Projects
# run: dotnet build --configuration ExportRelease
#
# - name: Run C# Tests
# env:
# GODOT_BIN: /root/bin/godot
# shell: bash
# run: |
# dotnet test --no-build --settings .runsettings --results-directory ./reports --logger "console;verbosity=normal" --logger "trx;LogFileName=results.xml" -- GdUnit4.Parameters="--headless --import --quit"
#
# - name: Upload test report
# uses: actions/upload-artifact@v3-node20
# if: always()
# with:
# name: Test Report
# path: ${{ github.workspace }}/reports/test-result.html
Export:
runs-on: godot
@@ -156,6 +156,11 @@ jobs:
- name: Checkout with LFS
uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
- name: Remove problematic addons
run: |
rm -rf ${{ gitea.workspace }}/addons/gdUnit4
rm -rf ${{ gitea.workspace }}/addons/rider-plugin
- name: Setup Godot
id: setup-godot
uses: https://git.game-dev.space/minimata/setup-godot.git@main
@@ -163,16 +168,18 @@ jobs:
godot-version: ${GODOT_VERSION}
dotnet-version: ${DOTNET_VERSION}
- name: Remove GDUnit addon
run: |
rm -rf ${{ gitea.workspace }}/addons/gdUnit4
- name: Build Windows
run: |
mkdir -v -p build/windows
${{ steps.setup-godot.outputs.godot_bin }} --headless --verbose --export-release "Windows Desktop" build/windows/${{ env.GAME_NAME }}.exe
ls -la build/windows
zip -r Windows.zip build/windows
- name: Upload build as artifact
uses: actions/upload-artifact@v3-node20
with:
name: Windows build
path: ${{ github.workspace }}/build/windows
# - name: Setup Butler
# shell: bash
# env:

View File

@@ -128,9 +128,9 @@
<ItemGroup>
<Folder Include="addons\" />
<Folder Include="tests\" />
<Folder Include="tools\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.Sync" Version="2.3.0" />
<PackageReference Include="RustyOptions" Version="0.10.1" />
</ItemGroup>
<ItemGroup>

BIN
addons/forge-godot-main.zip Normal file

Binary file not shown.

View File

@@ -3,7 +3,6 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Gamesmiths.Forge" Version="0.3.0" />
<PackageReference Include="Gamesmiths.Forge" Version="0.3.3" />
</ItemGroup>
</Project>

View File

@@ -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);
}
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);
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<TPlugin>(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;

View File

@@ -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<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);
}
}
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();
}

View File

@@ -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());

View File

@@ -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;
/// <summary>
/// Action node that resolves an input value of any supported type and prints it through
/// <see cref="GD.Print(params Variant[])"/>.
/// Useful for validating resolver chains while testing Statescript graphs in the editor.
/// </summary>
public sealed class DebugNode : ActionNode
{
private readonly StatescriptVariableType _valueType;
/// <inheritdoc/>
public override string Description => "Prints the resolved input value to the Godot console for debugging.";
public DebugNode(StatescriptVariableType valueType = StatescriptVariableType.Int)
{
_valueType = valueType;
}
/// <inheritdoc/>
protected override void DefineParameters(
List<InputProperty> inputProperties,
List<OutputVariable> outputVariables)
{
inputProperties.Add(new InputProperty("Value", StatescriptVariableTypeConverter.ToSystemType(_valueType)));
}
/// <inheritdoc/>
protected override void Execute(GraphContext graphContext)
{
if (!graphContext.TryResolveVariant(InputProperties[0].BoundName, out Variant128 value))
{
GD.Print("[Statescript Debug] <unresolved>");
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()),
};
}
}

View File

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

View File

@@ -23,12 +23,12 @@ public partial class AssetRepairTool : EditorPlugin
List<string> 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<PackedScene>(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
/// <returns><see langword="true"/> if any ForgeEntity was modified.</returns>
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<string> originalTags = container.ContainerTags;
var newTags = new Array<string>();
var modified = false;
bool modified = false;
foreach (var tag in originalTags)
foreach (string tag in originalTags)
{
try
{

View File

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

View File

@@ -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,19 +22,67 @@ 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 =>
_optionButton.ItemSelected += OnItemSelected;
}
public override void _UpdateProperty()
{
var className = _optionButton.GetItemText((int)x);
if (_optionButton is null || !IsInstanceValid(_optionButton))
{
return;
}
GodotObject obj = GetEditedObject();
StringName property = GetEditedProperty();
string val = obj.Get(property).AsString();
for (int i = 0; i < _optionButton.GetItemCount(); i++)
{
if (_optionButton.GetItemText(i) == val)
{
_optionButton.Selected = i;
break;
}
}
}
public override void _ExitTree()
{
ReleaseUiState();
FreeAllChildren();
base._ExitTree();
}
public void OnBeforeSerialize()
{
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 not null)
if (@object is null)
{
return;
}
var dictionary = new Dictionary<string, AttributeValues>();
var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet));
@@ -45,7 +93,7 @@ public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializ
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(x => x.PropertyType == typeof(EntityAttribute));
foreach (var propertyName in attributeProperties.Select(x => x.Name))
foreach (string? propertyName in attributeProperties.Select(x => x.Name))
{
if (@object is not ForgeAttributeSet forgeAttributeSet)
{
@@ -67,36 +115,25 @@ public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializ
EmitChanged("InitialAttributeValues", dictionary);
}
};
private void ReleaseUiState()
{
if (_optionButton is not null && IsInstanceValid(_optionButton))
{
_optionButton.ItemSelected -= OnItemSelected;
}
public override void _UpdateProperty()
{
GodotObject obj = GetEditedObject();
StringName property = GetEditedProperty();
var val = obj.Get(property).AsString();
for (var i = 0; i < _optionButton.GetItemCount(); i++)
{
if (_optionButton.GetItemText(i) == val)
{
_optionButton.Selected = i;
break;
}
}
_optionButton = null;
}
public void OnBeforeSerialize()
private void FreeAllChildren()
{
for (var i = GetChildCount() - 1; i >= 0; i--)
for (int i = GetChildCount() - 1; i >= 0; i--)
{
Node child = GetChild(i);
RemoveChild(child);
child.Free();
}
}
public void OnAfterDeserialize()
{
}
}
#endif

View File

@@ -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<VBoxContainer>("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();
}

View File

@@ -13,17 +13,8 @@ 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<CSharpScript>() is CSharpScript script)
{
if (@object?.GetScript().As<CSharpScript>() is null)
return false;
}
catch (Exception e)
{
return false;
}
var script = @object?.GetScript().As<CSharpScript>();
StringName className = script.GetGlobalName();
Type baseType = typeof(ForgeCueHandler);
@@ -37,6 +28,9 @@ public partial class CueHandlerInspectorPlugin : EditorInspectorPlugin
return implementationType is not null;
}
return false;
}
public override bool _ParseProperty(
GodotObject @object,
Variant.Type type,

View File

@@ -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,73 +27,64 @@ 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>(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<string>();
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();
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<string>();
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

View File

@@ -172,6 +172,26 @@ internal abstract partial class CustomNodeEditor : RefCounted
return _graphNode!.GetFoldStateInternal(key);
}
/// <summary>
/// Gets the persisted fold state for a given key, with a custom default when unset.
/// </summary>
/// <param name="key">The key used to persist the fold state.</param>
/// <param name="defaultValue">The default fold state when no persisted value exists.</param>
protected bool GetFoldState(string key, bool defaultValue)
{
return _graphNode!.GetFoldStateInternal(key, defaultValue);
}
/// <summary>
/// Persists a fold state change with undo support.
/// </summary>
/// <param name="key">The key used to persist the fold state.</param>
/// <param name="folded">The new folded state.</param>
protected void SetFoldStateWithUndo(string key, bool folded)
{
_graphNode!.SetFoldStateWithUndoInternal(key, folded);
}
/// <summary>
/// Finds an existing property binding by direction and index.
/// </summary>
@@ -245,6 +265,14 @@ internal abstract partial class CustomNodeEditor : RefCounted
_graphNode!.ResetSize();
}
/// <summary>
/// Refreshes standard input-property foldable summaries.
/// </summary>
protected void RefreshInputPropertyFoldableTitles()
{
_graphNode!.UpdateInputPropertyFoldableTitlesInternal();
}
/// <summary>
/// Raises the <see cref="StatescriptGraphNode.PropertyBindingChanged"/> event.
/// </summary>

View File

@@ -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<Label>("Row/Icon");
Label? textLabel = badge.GetNodeOrNull<Label>("Row/Text");
if (iconLabel is null || textLabel is null)
{
return;
}
iconLabel.SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin;
textLabel.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
iconLabel.Text = badgeData.IconText;
textLabel.Text = badgeData.Text;
iconLabel.CustomMinimumSize = new Vector2(MeasureWidestBadgeIconWidth(iconLabel), 0);
iconLabel.AddThemeColorOverride("font_color", badgeData.IconColor);
textLabel.AddThemeColorOverride("font_color", textLabel.GetThemeColor("font_color", "Label"));
textLabel.CustomMinimumSize = Vector2.Zero;
if (badge.HasMeta(SummaryBadgeSelectedVariableMetaKey))
{
string selectedVariableName = badge.GetMeta(SummaryBadgeSelectedVariableMetaKey).AsString();
if (!string.IsNullOrEmpty(selectedVariableName)
&& selectedVariableName == badgeData.HighlightVariableName)
{
styleBox.BorderWidthLeft = Math.Max(styleBox.BorderWidthLeft, 2);
styleBox.BorderWidthTop = Math.Max(styleBox.BorderWidthTop, 2);
styleBox.BorderWidthRight = Math.Max(styleBox.BorderWidthRight, 2);
styleBox.BorderWidthBottom = Math.Max(styleBox.BorderWidthBottom, 2);
styleBox.BorderColor = new Color(0x56b6c2ff);
styleBox.BgColor = new Color(0x56b6c2ff);
iconLabel.AddThemeColorOverride("font_color", Colors.Black);
textLabel.AddThemeColorOverride("font_color", Colors.Black);
}
}
if (badge.HasMeta(SummaryBadgeSelectedSharedVariableSetPathMetaKey)
&& badge.HasMeta(SummaryBadgeSelectedSharedVariableMetaKey))
{
string selectedSharedVariableSetPath = badge.GetMeta(SummaryBadgeSelectedSharedVariableSetPathMetaKey)
.AsString();
string selectedSharedVariableName = badge.GetMeta(SummaryBadgeSelectedSharedVariableMetaKey)
.AsString();
if (!string.IsNullOrEmpty(selectedSharedVariableSetPath)
&& !string.IsNullOrEmpty(selectedSharedVariableName)
&& selectedSharedVariableSetPath == badgeData.HighlightSharedVariableSetPath
&& selectedSharedVariableName == badgeData.HighlightSharedVariableName)
{
styleBox.BorderWidthLeft = Math.Max(styleBox.BorderWidthLeft, 2);
styleBox.BorderWidthTop = Math.Max(styleBox.BorderWidthTop, 2);
styleBox.BorderWidthRight = Math.Max(styleBox.BorderWidthRight, 2);
styleBox.BorderWidthBottom = Math.Max(styleBox.BorderWidthBottom, 2);
styleBox.BorderColor = new Color(0x56b6c2ff);
styleBox.BgColor = new Color(0x56b6c2ff);
iconLabel.AddThemeColorOverride("font_color", Colors.Black);
textLabel.AddThemeColorOverride("font_color", Colors.Black);
}
}
}
private static float MeasureWidestBadgeIconWidth(Label label)
{
float maxWidth = 0f;
foreach (InlineSummaryBadgeKind badgeKind in Enum.GetValues<InlineSummaryBadgeKind>())
{
maxWidth = Math.Max(maxWidth, MeasureLabelTextWidth(label, GetBadgeIcon(badgeKind, false)));
}
return Math.Max(maxWidth, MeasureLabelTextWidth(label, GetBadgeIcon(InlineSummaryBadgeKind.Resolver, true)));
}
private static bool TryGetSummaryBadge(FoldableContainer foldable, [NotNullWhen(true)] out PanelContainer? badge)
{
badge = null;
if (!foldable.HasMeta(SummaryBadgeMetaKey) || !foldable.HasMeta(SummaryBadgeInstanceIdMetaKey))
{
return false;
}
ulong badgeInstanceId = (ulong)foldable.GetMeta(SummaryBadgeInstanceIdMetaKey).AsInt64();
if (badgeInstanceId == 0)
{
return false;
}
if (GodotObject.InstanceFromId(badgeInstanceId) is PanelContainer existingBadge
&& GodotObject.IsInstanceValid(existingBadge))
{
badge = existingBadge;
return true;
}
return false;
}
private static void SynchronizeSiblingBadgeWidths(FoldableContainer foldable)
{
if (foldable.GetParent() is not Node parent)
{
return;
}
var siblingFoldables = new List<FoldableContainer>();
var siblingBadges = new List<PanelContainer>();
foreach (Node child in parent.GetChildren())
{
if (child is FoldableContainer siblingFoldable
&& TryGetSummaryBadge(siblingFoldable, out PanelContainer? badge))
{
siblingFoldables.Add(siblingFoldable);
siblingBadges.Add(badge);
}
}
if (siblingBadges.Count == 0)
{
return;
}
foreach (PanelContainer badge in siblingBadges)
{
badge.CustomMinimumSize = Vector2.Zero;
}
float widestTitleWidth = 0;
foreach (FoldableContainer siblingFoldable in siblingFoldables)
{
widestTitleWidth = Math.Max(widestTitleWidth, MeasureFoldableTitleWidth(siblingFoldable));
}
float availableWidth = parent is Control parentControl
? parentControl.Size.X
- widestTitleWidth
- FoldableTitleChromeWidth
- FoldableTitleBadgeGap
- FoldableTitleRightPadding
: float.MaxValue;
float maxWidth = MinimumBadgeWidth;
if (!float.IsPositiveInfinity(availableWidth) && !float.IsNaN(availableWidth))
{
maxWidth = Math.Max(0f, availableWidth);
}
for (int i = 0; i < siblingBadges.Count; i++)
{
PanelContainer badge = siblingBadges[i];
badge.CustomMinimumSize = badge.Visible
? new Vector2(maxWidth, 0)
: Vector2.Zero;
}
}
private static BadgeVisualStyle GetBadgeStyle(InlineSummaryBadgeKind badgeKind)
{
return badgeKind switch
{
InlineSummaryBadgeKind.Numeric => new BadgeVisualStyle(
_numericIconColor,
_numericBackgroundColor,
_numericBorderColor),
InlineSummaryBadgeKind.Vector => new BadgeVisualStyle(
_vectorIconColor,
_vectorBackgroundColor,
_vectorBorderColor),
InlineSummaryBadgeKind.Boolean => new BadgeVisualStyle(
_booleanIconColor,
_booleanBackgroundColor,
_booleanBorderColor),
InlineSummaryBadgeKind.Variable => new BadgeVisualStyle(
_variableIconColor,
_variableBackgroundColor,
_variableBorderColor),
InlineSummaryBadgeKind.SharedVariable => new BadgeVisualStyle(
_sharedVariableIconColor,
_sharedVariableBackgroundColor,
_sharedVariableBorderColor),
InlineSummaryBadgeKind.Enum => new BadgeVisualStyle(
_enumIconColor,
_enumBackgroundColor,
_enumBorderColor),
_ => new BadgeVisualStyle(
_resolverIconColor,
_resolverBackgroundColor,
_resolverBorderColor),
};
}
private static bool IsConstantBadgeKind(InlineSummaryBadgeKind badgeKind)
{
return badgeKind is InlineSummaryBadgeKind.Numeric
or InlineSummaryBadgeKind.Vector
or InlineSummaryBadgeKind.Boolean;
}
private static float MeasureFoldableTitleWidth(FoldableContainer foldable)
{
Font? font = foldable.GetThemeDefaultFont();
if (font is null)
{
return 0;
}
int fontSize = foldable.GetThemeDefaultFontSize();
return font.GetStringSize(
foldable.Title,
HorizontalAlignment.Left,
-1,
fontSize,
TextServer.JustificationFlag.None,
TextServer.Direction.Auto,
TextServer.Orientation.Horizontal).X;
}
private static string GetBadgeIcon(InlineSummaryBadgeKind badgeKind, bool isConstant)
{
return badgeKind switch
{
InlineSummaryBadgeKind.Numeric => "#",
InlineSummaryBadgeKind.Vector => "▦",
InlineSummaryBadgeKind.Boolean => "●",
InlineSummaryBadgeKind.Variable => "𝑥",
InlineSummaryBadgeKind.SharedVariable => "𝑦",
InlineSummaryBadgeKind.Enum => "≡",
_ => isConstant ? "#" : "ƒ",
};
}
private static float MeasureLabelTextWidth(Label label, string text)
{
Font? font = label.GetThemeFont("font", "Label") ?? label.GetThemeDefaultFont();
if (font is null)
{
return 0;
}
int fontSize = label.GetThemeFontSize("font_size", "Label");
if (fontSize <= 0)
{
fontSize = label.GetThemeDefaultFontSize();
}
return font.GetStringSize(
text,
HorizontalAlignment.Left,
-1,
fontSize,
TextServer.JustificationFlag.None,
TextServer.Direction.Auto,
TextServer.Orientation.Horizontal).X;
}
private static string FormatVector2(Vector2 value)
{
return $"({FormatNumber(value.X)}, {FormatNumber(value.Y)})";
}
private static string FormatVector3(Vector3 value)
{
return $"({FormatNumber(value.X)}, {FormatNumber(value.Y)}, {FormatNumber(value.Z)})";
}
private static string FormatVector4(Vector4 value)
{
return $"({FormatNumber(value.X)}, {FormatNumber(value.Y)}, {FormatNumber(value.Z)}, {FormatNumber(value.W)})";
}
private static string FormatPlane(Plane value)
{
return $"({FormatNumber(value.Normal.X)}, {FormatNumber(value.Normal.Y)}, {FormatNumber(value.Normal.Z)}, " +
$"{FormatNumber(value.D)})";
}
private static string FormatQuaternion(Quaternion value)
{
return $"({FormatNumber(value.X)}, {FormatNumber(value.Y)}, {FormatNumber(value.Z)}, {FormatNumber(value.W)})";
}
private static string FormatNumber(float value)
{
return value.ToString("G", CultureInfo.InvariantCulture);
}
private readonly record struct BadgeVisualStyle(
Color IconColor,
Color BackgroundColor,
Color BorderColor);
private readonly record struct SummaryBadgeData(
string IconText,
string Text,
string HighlightVariableName,
string HighlightSharedVariableSetPath,
string HighlightSharedVariableName,
InlineSummaryBadgeKind BadgeKind,
bool IsConstant,
Color IconColor,
Color BackgroundColor,
Color BorderColor,
bool Visible)
{
public static SummaryBadgeData Hidden => new(
string.Empty,
string.Empty,
string.Empty,
string.Empty,
string.Empty,
InlineSummaryBadgeKind.Resolver,
false,
Colors.Transparent,
Colors.Transparent,
Colors.Transparent,
false);
}
}
#endif

View File

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

View File

@@ -0,0 +1,16 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
internal enum InlineSummaryBadgeKind
{
Resolver = 0,
Numeric = 1,
Vector = 2,
Boolean = 3,
Variable = 4,
SharedVariable = 5,
Enum = 6,
}
#endif

View File

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

View File

@@ -0,0 +1,9 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
internal sealed record InputPropertyFoldableContext(FoldableContainer Foldable, string BaseTitle);
#endif

View File

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

View File

@@ -14,6 +14,8 @@ namespace Gamesmiths.Forge.Godot.Editor.Statescript;
[Tool]
internal abstract partial class NodeEditorProperty : PanelContainer
{
private Type[] _allowedExpectedTypes = [];
/// <summary>
/// Gets the display name shown in the resolver type dropdown (e.g., "Variable", "Constant", "Attribute").
/// </summary>
@@ -58,6 +60,58 @@ internal abstract partial class NodeEditorProperty : PanelContainer
/// </summary>
public event Action? LayoutSizeChanged;
/// <summary>
/// Configures the concrete input types allowed for this editor when the surrounding context accepts more than one.
/// </summary>
/// <param name="allowedExpectedTypes">The allowed expected types.</param>
public void ConfigureAllowedExpectedTypes(params Type[] allowedExpectedTypes)
{
_allowedExpectedTypes = allowedExpectedTypes;
}
/// <summary>
/// Tries to provide a short inline summary for the current editor state when embedded in a collapsed foldout.
/// </summary>
/// <param name="summary">The inline summary, when available.</param>
/// <returns><see langword="true"/> when an inline summary is available.</returns>
public virtual bool TryGetInlineSummary(out string summary)
{
summary = string.Empty;
return false;
}
/// <summary>
/// Gets the preferred badge style for inline foldout summaries.
/// </summary>
public virtual InlineSummaryBadgeKind GetInlineSummaryBadgeKind()
{
return InlineSummaryBadgeKind.Resolver;
}
/// <summary>
/// Tries to provide the graph-variable name represented by this editor or one of its nested editors for highlight
/// propagation in folded summaries.
/// </summary>
/// <param name="variableName">The variable name, when available.</param>
public virtual bool TryGetHighlightedVariableName(out string variableName)
{
variableName = string.Empty;
return false;
}
/// <summary>
/// Tries to provide the shared-variable identity represented by this editor or one of its nested editors for
/// highlight propagation in folded summaries.
/// </summary>
/// <param name="sharedVariableSetPath">The shared-variable set path, when available.</param>
/// <param name="variableName">The shared variable name, when available.</param>
public virtual bool TryGetHighlightedSharedVariable(out string sharedVariableSetPath, out string variableName)
{
sharedVariableSetPath = string.Empty;
variableName = string.Empty;
return false;
}
/// <summary>
/// Clears all delegate fields to prevent serialization issues during hot-reload. Called before the editor is
/// serialized or freed.
@@ -74,5 +128,10 @@ internal abstract partial class NodeEditorProperty : PanelContainer
{
LayoutSizeChanged?.Invoke();
}
protected Type[] GetAllowedExpectedTypes(Type fallbackExpectedType)
{
return _allowedExpectedTypes.Length > 0 ? _allowedExpectedTypes : [fallbackExpectedType];
}
}
#endif

View File

@@ -0,0 +1,95 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
internal static class SharedVariableHighlightState
{
private static string? _selectedSetPath;
private static string? _selectedVariableName;
private static string? _activeInspectorSetPath;
public static event Action? Changed;
public static void SetInspectorContext(string? setPath)
{
string normalized = string.IsNullOrWhiteSpace(setPath) ? string.Empty : setPath;
if (string.Equals(_activeInspectorSetPath, normalized, StringComparison.Ordinal))
{
return;
}
_activeInspectorSetPath = normalized;
Changed?.Invoke();
}
public static void ClearInspectorContext(string? setPath = null)
{
if (setPath is not null
&& !string.Equals(_activeInspectorSetPath, setPath, StringComparison.Ordinal))
{
return;
}
if (string.IsNullOrEmpty(_activeInspectorSetPath))
{
return;
}
_activeInspectorSetPath = string.Empty;
Changed?.Invoke();
}
public static void SetSelection(string? setPath, string? variableName)
{
string normalizedSetPath = string.IsNullOrWhiteSpace(setPath) ? string.Empty : setPath;
string normalizedVariableName = string.IsNullOrWhiteSpace(variableName) ? string.Empty : variableName;
if (string.IsNullOrEmpty(normalizedSetPath) || string.IsNullOrEmpty(normalizedVariableName))
{
normalizedSetPath = string.Empty;
normalizedVariableName = string.Empty;
}
if (string.Equals(_selectedSetPath, normalizedSetPath, StringComparison.Ordinal)
&& string.Equals(_selectedVariableName, normalizedVariableName, StringComparison.Ordinal))
{
return;
}
_selectedSetPath = normalizedSetPath;
_selectedVariableName = normalizedVariableName;
Changed?.Invoke();
}
public static bool TryGetActiveSelection(out string setPath, out string variableName)
{
if (string.IsNullOrEmpty(_selectedSetPath)
|| string.IsNullOrEmpty(_selectedVariableName)
|| !string.Equals(_activeInspectorSetPath, _selectedSetPath, StringComparison.Ordinal))
{
setPath = string.Empty;
variableName = string.Empty;
return false;
}
setPath = _selectedSetPath;
variableName = _selectedVariableName;
return true;
}
public static bool HasAnySelection()
{
return !string.IsNullOrEmpty(_selectedSetPath) && !string.IsNullOrEmpty(_selectedVariableName);
}
public static bool IsSelected(string? setPath, string? variableName)
{
return TryGetActiveSelection(out string activeSetPath, out string activeVariableName)
&& string.Equals(activeSetPath, setPath, StringComparison.Ordinal)
&& string.Equals(activeVariableName, variableName, StringComparison.Ordinal);
}
}
#endif

View File

@@ -0,0 +1 @@
uid://2gcypje3sxpa

View File

@@ -16,7 +16,15 @@ namespace Gamesmiths.Forge.Godot.Editor.Statescript;
[Tool]
internal sealed partial class SharedVariableSetEditorProperty : EditorProperty, ISerializationListener
{
private const string BackgroundPanelNodeName = "BackgroundPanel";
private const string RootNodeName = "Root";
private const string HeaderRowNodeName = "HeaderRow";
private const string AddButtonNodeName = "AddButton";
private const string VariableListNodeName = "VariableList";
private const string VariableNameButtonMetaKey = "_shared_variable_name_button";
private static readonly Color _variableColor = new(0xe5c07bff);
private static readonly Color _highlightColor = new(0x56b6c2ff);
private readonly HashSet<string> _expandedArrays = [];
@@ -33,6 +41,7 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
private Texture2D? _addIcon;
private Texture2D? _removeIcon;
private string? _selectedVariableName;
/// <summary>
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
@@ -45,11 +54,21 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
public override void _Ready()
{
base._Ready();
SharedVariableHighlightState.Changed += OnSharedVariableHighlightChanged;
_addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
_removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
if (GetEditedObject() is ForgeSharedVariableSet sharedVariableSet)
{
SharedVariableHighlightState.SetInspectorContext(sharedVariableSet.ResourcePath);
SharedVariableHighlightState.SetSelection(sharedVariableSet.ResourcePath, _selectedVariableName);
}
var backgroundPanel = new PanelContainer
{
Name = BackgroundPanelNodeName,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
@@ -70,14 +89,19 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
AddChild(backgroundPanel);
SetBottomEditor(backgroundPanel);
_root = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_root = new VBoxContainer
{
Name = RootNodeName,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
backgroundPanel.AddChild(_root);
var headerHBox = new HBoxContainer();
var headerHBox = new HBoxContainer { Name = HeaderRowNodeName };
_root.AddChild(headerHBox);
_addButton = new Button
{
Name = AddButtonNodeName,
Text = "Add Variable",
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
@@ -87,34 +111,55 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
_root.AddChild(new HSeparator());
_variableList = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_variableList = new VBoxContainer
{
Name = VariableListNodeName,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
_root.AddChild(_variableList);
}
public override void _UpdateProperty()
{
EnsureControlsCached();
if (GetEditedObject() is ForgeSharedVariableSet sharedVariableSet)
{
SharedVariableHighlightState.SetInspectorContext(sharedVariableSet.ResourcePath);
}
SyncSelectedVariableFromHighlightState();
RebuildList();
}
public override void _ExitTree()
{
SharedVariableHighlightState.Changed -= OnSharedVariableHighlightChanged;
if (GetEditedObject() is ForgeSharedVariableSet sharedVariableSet)
{
SharedVariableHighlightState.ClearInspectorContext(sharedVariableSet.ResourcePath);
}
ReleaseUiState();
FreeAllChildren();
base._ExitTree();
}
public void OnBeforeSerialize()
{
if (_addButton is not null)
{
_addButton.Pressed -= OnAddPressed;
}
ClearVariableList();
_creationDialog?.Free();
_creationDialog = null;
_newNameEdit = null;
_newTypeDropdown = null;
_newArrayToggle = null;
SharedVariableHighlightState.Changed -= OnSharedVariableHighlightChanged;
ReleaseUiState();
}
public void OnAfterDeserialize()
{
if (_addButton is not null)
EnsureControlsCached();
SharedVariableHighlightState.Changed += OnSharedVariableHighlightChanged;
SyncSelectedVariableFromHighlightState();
if (_addButton is not null && IsInstanceValid(_addButton))
{
_addButton.Pressed += OnAddPressed;
}
@@ -122,6 +167,28 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
RebuildList();
}
private static int FindTypeDropdownIndex(OptionButton dropdown, StatescriptVariableType variableType)
{
for (int i = 0; i < dropdown.ItemCount; i++)
{
if (dropdown.GetItemId(i) == (int)variableType)
{
return i;
}
}
return 0;
}
private static void UpdateVariableNameButtonAppearance(Button button, bool isSelected)
{
Color buttonColor = isSelected ? _highlightColor : _variableColor;
button.AddThemeColorOverride("font_color", buttonColor);
button.AddThemeColorOverride("font_pressed_color", buttonColor);
button.AddThemeColorOverride("font_hover_color", buttonColor.Lightened(0.2f));
button.AddThemeColorOverride("font_hover_pressed_color", buttonColor.Lightened(0.2f));
}
private Array<ForgeSharedVariableDefinition> GetDefinitions()
{
GodotObject obj = GetEditedObject();
@@ -133,7 +200,18 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
private void NotifyChanged()
{
if (GetEditedObject() is Resource resource)
GodotObject obj = GetEditedObject();
string propertyName = GetEditedProperty();
if (obj is not ForgeSharedVariableSet sharedVariableSet)
{
return;
}
obj.Set(propertyName, sharedVariableSet.Variables);
EmitChanged(propertyName, sharedVariableSet.Variables);
if (obj is Resource resource)
{
resource.EmitChanged();
}
@@ -141,35 +219,31 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
private void RebuildList()
{
EnsureControlsCached();
if (_variableList is null)
{
return;
}
// Defer the actual rebuild so that any in-progress signal emission (e.g. a button Pressed handler that
// triggered an add/remove) finishes before we free the emitting nodes.
CallDeferred(MethodName.RebuildListDeferred);
}
private void RebuildListDeferred()
{
if (_variableList is null)
{
return;
}
SyncSelectedVariableFromHighlightState();
ClearVariableList();
Array<ForgeSharedVariableDefinition> definitions = GetDefinitions();
for (var i = 0; i < definitions.Count; i++)
for (int i = 0; i < definitions.Count; i++)
{
AddVariableRow(definitions, i);
}
RefreshVariableSelectionVisuals();
}
private void ClearVariableList()
{
EnsureControlsCached();
if (_variableList is null)
{
return;
@@ -182,6 +256,75 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
}
}
private void EnsureControlsCached()
{
_root ??= GetNodeOrNull<VBoxContainer>(
$"{BackgroundPanelNodeName}/{RootNodeName}");
_addButton ??= GetNodeOrNull<Button>(
$"{BackgroundPanelNodeName}/{RootNodeName}/{HeaderRowNodeName}/{AddButtonNodeName}");
_variableList ??= GetNodeOrNull<VBoxContainer>(
$"{BackgroundPanelNodeName}/{RootNodeName}/{VariableListNodeName}");
}
private void ReleaseUiState()
{
if (_addButton is not null && IsInstanceValid(_addButton))
{
_addButton.Pressed -= OnAddPressed;
}
ClearVariableList();
if (_creationDialog is not null && IsInstanceValid(_creationDialog))
{
_creationDialog.Confirmed -= OnCreationConfirmed;
_creationDialog.Canceled -= OnCreationCanceled;
_creationDialog.Free();
}
_creationDialog = null;
_newNameEdit = null;
_newTypeDropdown = null;
_newArrayToggle = null;
_root = null;
_variableList = null;
_addButton = null;
}
private void FreeAllChildren()
{
for (int i = GetChildCount() - 1; i >= 0; i--)
{
Node child = GetChild(i);
RemoveChild(child);
child.Free();
}
}
private void OnSharedVariableHighlightChanged()
{
SyncSelectedVariableFromHighlightState();
RefreshVariableSelectionVisuals();
}
private void SyncSelectedVariableFromHighlightState()
{
if (GetEditedObject() is not ForgeSharedVariableSet sharedVariableSet)
{
_selectedVariableName = null;
return;
}
if (SharedVariableHighlightState.TryGetActiveSelection(out string selectedSetPath, out string variableName)
&& string.Equals(selectedSetPath, sharedVariableSet.ResourcePath, System.StringComparison.Ordinal))
{
_selectedVariableName = variableName;
return;
}
_selectedVariableName = null;
}
private void AddVariableRow(Array<ForgeSharedVariableDefinition> definitions, int index)
{
if (_variableList is null || index < 0 || index >= definitions.Count)
@@ -197,17 +340,25 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
rowContainer.AddChild(headerRow);
var nameLabel = new Label
bool isSelected = _selectedVariableName == def.VariableName;
var nameButton = new Button
{
Text = def.VariableName,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
Flat = true,
ToggleMode = true,
ButtonPressed = isSelected,
Alignment = HorizontalAlignment.Left,
};
nameLabel.AddThemeColorOverride("font_color", _variableColor);
nameLabel.AddThemeFontOverride(
nameButton.SetMeta(VariableNameButtonMetaKey, def.VariableName);
UpdateVariableNameButtonAppearance(nameButton, isSelected);
nameButton.AddThemeFontOverride(
"font",
EditorInterface.Singleton.GetEditorTheme().GetFont("bold", "EditorFonts"));
headerRow.AddChild(nameLabel);
nameButton.Toggled += pressed => SetSelectedVariable(def.VariableName, pressed);
headerRow.AddChild(nameButton);
var typeLabel = new Label
{
@@ -218,7 +369,7 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
typeLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.6f));
headerRow.AddChild(typeLabel);
var capturedIndex = index;
int capturedIndex = index;
var deleteButton = new Button
{
@@ -245,6 +396,56 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
rowContainer.AddChild(new HSeparator());
}
private void SetSelectedVariable(string variableName, bool selected)
{
if (selected)
{
_selectedVariableName = variableName;
}
else if (_selectedVariableName == variableName)
{
_selectedVariableName = null;
}
if (GetEditedObject() is ForgeSharedVariableSet sharedVariableSet)
{
SharedVariableHighlightState.SetInspectorContext(sharedVariableSet.ResourcePath);
SharedVariableHighlightState.SetSelection(sharedVariableSet.ResourcePath, _selectedVariableName);
}
else
{
SharedVariableHighlightState.SetSelection(null, null);
}
RefreshVariableSelectionVisuals();
}
private void RefreshVariableSelectionVisuals()
{
if (_variableList is null)
{
return;
}
RefreshVariableSelectionVisualsRecursive(_variableList);
}
private void RefreshVariableSelectionVisualsRecursive(Node parent)
{
foreach (Node child in parent.GetChildren())
{
if (child is Button button && button.HasMeta(VariableNameButtonMetaKey))
{
string variableName = button.GetMeta(VariableNameButtonMetaKey).AsString();
bool isSelected = _selectedVariableName == variableName;
button.SetPressedNoSignal(isSelected);
UpdateVariableNameButtonAppearance(button, isSelected);
}
RefreshVariableSelectionVisualsRecursive(child);
}
}
private Control CreateValueEditor(ForgeSharedVariableDefinition def)
{
if (def.VariableType == StatescriptVariableType.Bool)
@@ -310,7 +511,7 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
var headerRow = new HBoxContainer();
vBox.AddChild(headerRow);
var isExpanded = _expandedArrays.Contains(def.VariableName);
bool isExpanded = _expandedArrays.Contains(def.VariableName);
var elementsContainer = new VBoxContainer
{
@@ -330,7 +531,7 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
{
elementsContainer.Visible = x;
var wasExpanded = !x;
bool wasExpanded = !x;
if (x)
{
@@ -379,9 +580,9 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
vBox.AddChild(elementsContainer);
for (var i = 0; i < def.InitialArrayValues.Count; i++)
for (int i = 0; i < def.InitialArrayValues.Count; i++)
{
var capturedIndex = i;
int capturedIndex = i;
if (def.VariableType == StatescriptVariableType.Bool)
{
@@ -502,7 +703,7 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
private void AddArrayElement(ForgeSharedVariableDefinition def, Variant value)
{
var wasExpanded = _expandedArrays.Contains(def.VariableName);
bool wasExpanded = _expandedArrays.Contains(def.VariableName);
if (_undoRedo is not null)
{
@@ -583,6 +784,8 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
(int)variableType);
}
_newTypeDropdown.Selected = FindTypeDropdownIndex(_newTypeDropdown, StatescriptVariableType.Int);
typeRow.AddChild(_newTypeDropdown);
var arrayRow = new HBoxContainer();
@@ -605,7 +808,7 @@ internal sealed partial class SharedVariableSetEditorProperty : EditorProperty,
return;
}
var name = _newNameEdit.Text.Trim();
string name = _newNameEdit.Text.Trim();
if (string.IsNullOrEmpty(name))
{

View File

@@ -206,7 +206,7 @@ internal sealed partial class StatescriptAddNodeDialog : ConfirmationDialog, ISe
IReadOnlyList<StatescriptNodeDiscovery.NodeTypeInfo> discoveredTypes =
StatescriptNodeDiscovery.GetDiscoveredNodeTypes();
var filterLower = filter.ToLowerInvariant();
string filterLower = filter.ToLowerInvariant();
TreeItem? actionCategory = null;
TreeItem? conditionCategory = null;
@@ -304,7 +304,7 @@ internal sealed partial class StatescriptAddNodeDialog : ConfirmationDialog, ISe
GetOkButton().Disabled = false;
var metadata = selected.GetMetadata(0).AsString();
string metadata = selected.GetMetadata(0).AsString();
UpdateDescription(metadata);
}
@@ -330,7 +330,7 @@ internal sealed partial class StatescriptAddNodeDialog : ConfirmationDialog, ISe
return;
}
var metadata = selected.GetMetadata(0).AsString();
string metadata = selected.GetMetadata(0).AsString();
if (metadata == "__exit__")
{

View File

@@ -139,8 +139,8 @@ internal static partial class StatescriptEditorControls
Func<int, double> getComponent,
Action<double[]>? onChanged)
{
var componentCount = GetVectorComponentCount(type);
var labels = GetVectorComponentLabels(type);
int componentCount = GetVectorComponentCount(type);
string[] labels = GetVectorComponentLabels(type);
var vBox = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
var row = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
@@ -153,11 +153,11 @@ internal static partial class StatescriptEditorControls
vBox.AddChild(panelContainer);
panelContainer.AddChild(row);
var values = new double[componentCount];
double[] values = new double[componentCount];
var handler = new VectorComponentHandler(values) { OnChanged = onChanged };
vBox.AddChild(handler);
for (var i = 0; i < componentCount; i++)
for (int i = 0; i < componentCount; i++)
{
values[i] = getComponent(i);

View File

@@ -73,7 +73,7 @@ public partial class StatescriptGraphEditorDock
return;
}
var path = _newStatescriptPathEdit.Text.Trim();
string path = _newStatescriptPathEdit.Text.Trim();
if (string.IsNullOrEmpty(path))
{
return;
@@ -270,7 +270,7 @@ public partial class StatescriptGraphEditorDock
{
if (_pendingConnectionIsOutput)
{
var inputPort = FindFirstEnabledInputPort(newNodeId);
int inputPort = FindFirstEnabledInputPort(newNodeId);
if (inputPort >= 0)
{
OnConnectionRequest(
@@ -282,7 +282,7 @@ public partial class StatescriptGraphEditorDock
}
else
{
var outputPort = FindFirstEnabledOutputPort(newNodeId);
int outputPort = FindFirstEnabledOutputPort(newNodeId);
if (outputPort >= 0)
{
OnConnectionRequest(

View File

@@ -238,6 +238,7 @@ public partial class StatescriptGraphEditorDock
if (CurrentGraph == graph)
{
InvalidateCachedGraphVisuals(graph);
LoadGraphIntoEditor(graph);
}
}
@@ -339,7 +340,7 @@ public partial class StatescriptGraphEditorDock
return string.Empty;
}
var nodeId = $"node_{_nextNodeId++}";
string nodeId = $"node_{_nextNodeId++}";
var nodeResource = new StatescriptNode
{
@@ -371,10 +372,9 @@ public partial class StatescriptGraphEditorDock
if (CurrentGraph == graph && _graphEdit is not null)
{
var graphNode = new StatescriptGraphNode();
_graphEdit.AddChild(graphNode);
graphNode.Initialize(nodeResource, graph);
graphNode.SetUndoRedo(_undoRedo);
GraphTab? tab = FindTab(graph);
StatescriptGraphNode graphNode = AddGraphNodeVisual(nodeResource, graph);
tab?.CachedGraphNodes.Add(graphNode);
}
}
@@ -384,6 +384,7 @@ public partial class StatescriptGraphEditorDock
if (CurrentGraph == graph)
{
InvalidateCachedGraphVisuals(graph);
LoadGraphIntoEditor(graph);
}
}
@@ -424,7 +425,7 @@ public partial class StatescriptGraphEditorDock
foreach (StatescriptGraphNode sgn in selectedNodes)
{
StatescriptNode original = sgn.NodeResource!;
var newNodeId = $"node_{_nextNodeId++}";
string newNodeId = $"node_{_nextNodeId++}";
duplicatedIds[original.NodeId] = newNodeId;
var duplicated = new StatescriptNode
@@ -457,16 +458,16 @@ public partial class StatescriptGraphEditorDock
graph.Nodes.Add(duplicated);
var graphNode = new StatescriptGraphNode();
_graphEdit.AddChild(graphNode);
graphNode.Initialize(duplicated, graph);
GraphTab? tab = FindTab(graph);
StatescriptGraphNode graphNode = AddGraphNodeVisual(duplicated, graph);
tab?.CachedGraphNodes.Add(graphNode);
graphNode.Selected = true;
}
foreach (StatescriptConnection connection in graph.Connections)
{
if (duplicatedIds.TryGetValue(connection.FromNode, out var newFrom)
&& duplicatedIds.TryGetValue(connection.ToNode, out var newTo))
if (duplicatedIds.TryGetValue(connection.FromNode, out string? newFrom)
&& duplicatedIds.TryGetValue(connection.ToNode, out string? newTo))
{
_graphEdit.ConnectNode(newFrom, connection.OutputPort, newTo, connection.InputPort);
}

View File

@@ -54,8 +54,11 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
private string[]? _serializedTabPaths;
private int _serializedActiveTab = -1;
private bool[]? _serializedVariablesStates;
private string?[]? _serializedSelectedVariables;
private string[]? _serializedConnections;
private int[]? _serializedConnectionCounts;
private bool _persistedVariablesPanelVisible = true;
private bool _sharedVariableHighlightSubscribed;
/// <summary>
/// Gets the currently active graph resource, if any.
@@ -92,13 +95,16 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
_filesystemChangedCallable = new Callable(this, nameof(OnFilesystemChanged));
_fileSystem.Connect(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable);
SubscribeSharedVariableHighlightState();
}
public override void _ExitTree()
{
base._ExitTree();
UnsubscribeSharedVariableHighlightState();
ClearGraphEditor();
DisposeCachedGraphVisuals();
_openTabs.Clear();
if (_fileSystem?.IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable)
@@ -108,10 +114,14 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
}
DisconnectUISignals();
_fileSystem = null;
_filesystemChangedCallable = default;
}
public void OnBeforeSerialize()
{
UnsubscribeSharedVariableHighlightState();
if (_fileSystem?.IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable)
== true)
{
@@ -121,6 +131,8 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
_serializedTabPaths = GetOpenResourcePaths();
_serializedActiveTab = GetActiveTabIndex();
_serializedVariablesStates = GetVariablesPanelStates();
_serializedSelectedVariables = GetSelectedVariableStates();
_persistedVariablesPanelVisible = _variablePanel?.Visible ?? _persistedVariablesPanelVisible;
SyncVisualNodePositionsToGraph();
SyncConnectionsToCurrentGraph();
@@ -133,10 +145,10 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
var allConnections = new List<string>();
_serializedConnectionCounts = new int[_openTabs.Count];
for (var i = 0; i < _openTabs.Count; i++)
for (int i = 0; i < _openTabs.Count; i++)
{
StatescriptGraph graph = _openTabs[i].GraphResource;
var count = 0;
int count = 0;
foreach (StatescriptConnection c in graph.Connections)
{
allConnections.Add($"{c.FromNode},{c.OutputPort},{c.ToNode},{c.InputPort}");
@@ -159,6 +171,8 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
}
}
DisposeCachedGraphVisuals();
_openTabs.Clear();
}
@@ -173,6 +187,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
}
ConnectUISignals();
SubscribeSharedVariableHighlightState();
if (_serializedTabPaths?.Length > 0)
{
@@ -210,23 +225,30 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
for (var i = 0; i < _openTabs.Count; i++)
PersistCurrentVariablePanelState();
for (int i = 0; i < _openTabs.Count; i++)
{
if (_openTabs[i].GraphResource == graph || (!string.IsNullOrEmpty(graph.ResourcePath)
&& _openTabs[i].ResourcePath == graph.ResourcePath))
{
_tabBar.CurrentTab = i;
SetCurrentTabWithoutLoading(i);
ApplyVariablesPanelState(i);
return;
}
}
graph.EnsureEntryNode();
var tab = new GraphTab(graph);
var tab = new GraphTab(graph)
{
VariablesPanelOpen = _variablePanel?.Visible ?? false,
};
_openTabs.Add(tab);
_tabBar.AddTab(graph.StatescriptName);
_tabBar.CurrentTab = _openTabs.Count - 1;
SetCurrentTabWithoutLoading(_openTabs.Count - 1);
LoadGraphIntoEditor(graph);
UpdateVisibility();
@@ -242,7 +264,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
var currentTab = _tabBar.CurrentTab;
int currentTab = _tabBar.CurrentTab;
if (currentTab < 0 || currentTab >= _openTabs.Count)
{
return;
@@ -257,7 +279,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
/// <returns>An array of resource paths.</returns>
public string[] GetOpenResourcePaths()
{
return [.. _openTabs.Select(x => x.ResourcePath)];
return [.. GetPersistedTabs().Select(x => x.ResourcePath)];
}
/// <summary>
@@ -266,7 +288,35 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
/// <returns>The active tab index, or -1 if no tabs are open.</returns>
public int GetActiveTabIndex()
{
return _tabBar?.CurrentTab ?? -1;
PersistCurrentVariablePanelState();
if (_tabBar is null)
{
return -1;
}
int currentTab = _tabBar.CurrentTab;
if (currentTab < 0 || currentTab >= _openTabs.Count)
{
return -1;
}
GraphTab current = _openTabs[currentTab];
if (string.IsNullOrEmpty(current.ResourcePath))
{
return -1;
}
GraphTab[] persistedTabs = GetPersistedTabs();
for (int i = 0; i < persistedTabs.Length; i++)
{
if (persistedTabs[i].ResourcePath == current.ResourcePath)
{
return i;
}
}
return -1;
}
/// <summary>
@@ -276,7 +326,14 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
/// </returns>
public bool[] GetVariablesPanelStates()
{
return [.. _openTabs.Select(x => x.VariablesPanelOpen)];
PersistCurrentVariablePanelState();
return [.. GetPersistedTabs().Select(x => x.VariablesPanelOpen)];
}
public string?[] GetSelectedVariableStates()
{
PersistCurrentVariablePanelState();
return [.. GetPersistedTabs().Select(x => x.SelectedVariableName)];
}
/// <summary>
@@ -331,10 +388,10 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
_tabBar.RemoveTab(0);
}
var skippedTabs = 0;
for (var i = 0; i < paths.Length; i++)
int skippedTabs = 0;
for (int i = 0; i < paths.Length; i++)
{
var path = paths[i];
string path = paths[i];
StatescriptGraph? graph = LoadGraphFromPath(path);
if (graph is null)
@@ -346,7 +403,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
graph.EnsureEntryNode();
var tab = new GraphTab(graph);
var currentTab = i - skippedTabs;
int currentTab = i - skippedTabs;
if (variablesStates is not null && currentTab < variablesStates.Length)
{
tab.VariablesPanelOpen = variablesStates[currentTab];
@@ -360,10 +417,17 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
if (activeIndex >= 0 && activeIndex < _openTabs.Count)
{
_tabBar.CurrentTab = activeIndex;
_openTabs[activeIndex].VariablesPanelOpen = _persistedVariablesPanelVisible;
SetCurrentTabWithoutLoading(activeIndex);
LoadGraphIntoEditor(_openTabs[activeIndex].GraphResource);
ApplyVariablesPanelState(activeIndex);
}
else if (_openTabs.Count > 0)
{
SetCurrentTabWithoutLoading(0);
LoadGraphIntoEditor(_openTabs[0].GraphResource);
ApplyVariablesPanelState(0);
}
UpdateVisibility();
}
@@ -388,7 +452,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
private static string GetBaseFilePath(string resourcePath)
{
var separatorIndex = resourcePath.IndexOf("::", StringComparison.Ordinal);
int separatorIndex = resourcePath.IndexOf("::", StringComparison.Ordinal);
return separatorIndex >= 0 ? resourcePath[..separatorIndex] : resourcePath;
}
@@ -401,7 +465,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
{
if (IsSubResourcePath(path))
{
var basePath = GetBaseFilePath(path);
string basePath = GetBaseFilePath(path);
if (!ResourceLoader.Exists(basePath))
{
return null;
@@ -426,7 +490,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
private static StatescriptGraph? FindSubResourceGraph(Resource parentResource, string subResourcePath)
{
foreach (var propertyName in parentResource.GetPropertyList()
foreach (string? propertyName in parentResource.GetPropertyList()
.Select(p => p["name"].AsString()))
{
Variant value = parentResource.Get(propertyName);
@@ -455,7 +519,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return graph;
}
foreach (var propertyName in resource.GetPropertyList()
foreach (string? propertyName in resource.GetPropertyList()
.Select(p => p["name"].AsString()))
{
Variant value = resource.Get(propertyName);
@@ -479,11 +543,11 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
private static void SaveGraphResource(StatescriptGraph graph)
{
var path = graph.ResourcePath;
string path = graph.ResourcePath;
if (IsSubResourcePath(path))
{
var basePath = GetBaseFilePath(path);
string basePath = GetBaseFilePath(path);
Resource? parentResource = ResourceLoader.Load(basePath);
if (parentResource is not null)
{
@@ -505,15 +569,17 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
var paths = _serializedTabPaths;
var activeTab = _serializedActiveTab;
var varStates = _serializedVariablesStates;
var savedConnections = _serializedConnections;
var connectionCounts = _serializedConnectionCounts;
string[] paths = _serializedTabPaths;
int activeTab = _serializedActiveTab;
bool[]? varStates = _serializedVariablesStates;
string?[]? selectedVariables = _serializedSelectedVariables;
string[]? savedConnections = _serializedConnections;
int[]? connectionCounts = _serializedConnectionCounts;
_serializedTabPaths = null;
_serializedActiveTab = -1;
_serializedVariablesStates = null;
_serializedSelectedVariables = null;
_serializedConnections = null;
_serializedConnectionCounts = null;
@@ -530,8 +596,8 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
_tabBar.RemoveTab(0);
}
var skippedTabs = 0;
for (var i = 0; i < paths.Length; i++)
int skippedTabs = 0;
for (int i = 0; i < paths.Length; i++)
{
if (!ResourceLoader.Exists(paths[i]))
{
@@ -549,12 +615,17 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
graph.EnsureEntryNode();
var tab = new GraphTab(graph);
var currentTab = i - skippedTabs;
int currentTab = i - skippedTabs;
if (varStates is not null && currentTab < varStates.Length)
{
tab.VariablesPanelOpen = varStates[currentTab];
}
if (selectedVariables is not null && currentTab < selectedVariables.Length)
{
tab.SelectedVariableName = selectedVariables[currentTab];
}
_openTabs.Add(tab);
_tabBar.AddTab(graph.StatescriptName);
}
@@ -563,18 +634,18 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
if (savedConnections is not null && connectionCounts is not null)
{
var offset = 0;
for (var i = 0; i < _openTabs.Count && i < connectionCounts.Length; i++)
int offset = 0;
for (int i = 0; i < _openTabs.Count && i < connectionCounts.Length; i++)
{
StatescriptGraph graph = _openTabs[i].GraphResource;
graph.Connections.Clear();
for (var j = 0; j < connectionCounts[i] && offset < savedConnections.Length; j++, offset++)
for (int j = 0; j < connectionCounts[i] && offset < savedConnections.Length; j++, offset++)
{
var parts = savedConnections[offset].Split(',');
string[] parts = savedConnections[offset].Split(',');
if (parts.Length != 4
|| !int.TryParse(parts[1], out var outPort)
|| !int.TryParse(parts[3], out var inPort))
|| !int.TryParse(parts[1], out int outPort)
|| !int.TryParse(parts[3], out int inPort))
{
continue;
}
@@ -592,10 +663,17 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
if (activeTab >= 0 && activeTab < _openTabs.Count)
{
_tabBar.CurrentTab = activeTab;
_openTabs[activeTab].VariablesPanelOpen = _persistedVariablesPanelVisible;
SetCurrentTabWithoutLoading(activeTab);
LoadGraphIntoEditor(_openTabs[activeTab].GraphResource);
ApplyVariablesPanelState(activeTab);
}
else if (_openTabs.Count > 0)
{
SetCurrentTabWithoutLoading(0);
LoadGraphIntoEditor(_openTabs[0].GraphResource);
ApplyVariablesPanelState(0);
}
UpdateVisibility();
}
@@ -607,13 +685,15 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
DisposeCachedGraphVisuals(_openTabs[tabIndex]);
_openTabs.RemoveAt(tabIndex);
_tabBar.RemoveTab(tabIndex);
if (_openTabs.Count > 0)
{
var newTab = Mathf.Min(tabIndex, _openTabs.Count - 1);
_tabBar.CurrentTab = newTab;
int newTab = Mathf.Min(tabIndex, _openTabs.Count - 1);
SetCurrentTabWithoutLoading(newTab);
LoadGraphIntoEditor(_openTabs[newTab].GraphResource);
ApplyVariablesPanelState(newTab);
}
@@ -820,7 +900,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
private void UpdateVisibility()
{
var hasOpenGraph = _openTabs.Count > 0;
bool hasOpenGraph = _openTabs.Count > 0;
if (_splitContainer is not null)
{
@@ -855,22 +935,37 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
var wasLoading = _isLoadingGraph;
GraphTab? tab = FindTab(graph);
if (tab is null)
{
return;
}
bool wasLoading = _isLoadingGraph;
_isLoadingGraph = true;
ClearGraphEditor();
DetachVisibleGraphNodes();
_graphEdit.Zoom = graph.Zoom;
UpdateNextNodeId(graph);
tab.CachedGraphNodes.RemoveAll(x => !IsInstanceValid(x));
if (tab.CachedGraphNodes.Count == 0)
{
foreach (StatescriptNode nodeResource in graph.Nodes)
{
var graphNode = new StatescriptGraphNode();
_graphEdit.AddChild(graphNode);
graphNode.Initialize(nodeResource, graph);
graphNode.SetUndoRedo(_undoRedo);
StatescriptGraphNode graphNode = AddGraphNodeVisual(nodeResource, graph);
tab.CachedGraphNodes.Add(graphNode);
}
}
else
{
AttachCachedGraphNodes(tab);
}
_graphEdit.ClearConnections();
foreach (StatescriptConnection connection in graph.Connections)
{
@@ -881,6 +976,8 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
connection.InputPort);
}
ReapplyCurrentNodeHighlights();
_isLoadingGraph = wasLoading;
_ = ApplyScrollNextFrame(graph.ScrollOffset);
@@ -912,21 +1009,137 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
{
if (node is StatescriptGraphNode graphNode)
{
graphNode.OnBeforeSerialize();
RemoveGraphNodeVisual(graphNode);
}
}
}
_graphEdit.RemoveChild(node);
node.Free();
private void DetachVisibleGraphNodes()
{
if (_graphEdit is null)
{
return;
}
_graphEdit.ClearConnections();
var toDetach = new List<Node>();
toDetach.AddRange(_graphEdit.GetChildren().Where(x => x is GraphNode));
foreach (Node node in toDetach)
{
_graphEdit.RemoveChild(node);
}
}
private StatescriptGraphNode AddGraphNodeVisual(StatescriptNode nodeResource, StatescriptGraph graph)
{
var graphNode = new StatescriptGraphNode();
_graphEdit!.AddChild(graphNode);
graphNode.Initialize(nodeResource, graph);
graphNode.SetUndoRedo(_undoRedo);
graphNode.PropertyBindingChanged += OnGraphNodePropertyBindingChanged;
return graphNode;
}
private void AttachCachedGraphNodes(GraphTab tab)
{
if (_graphEdit is null)
{
return;
}
foreach (StatescriptGraphNode graphNode in tab.CachedGraphNodes)
{
if (!IsInstanceValid(graphNode))
{
continue;
}
if (graphNode.GetParent() is Node parent)
{
parent.RemoveChild(graphNode);
}
_graphEdit.AddChild(graphNode);
}
}
private void RemoveGraphNodeVisual(StatescriptGraphNode graphNode)
{
graphNode.PropertyBindingChanged -= OnGraphNodePropertyBindingChanged;
graphNode.OnBeforeSerialize();
if (graphNode.GetParent() is Node parent)
{
parent.RemoveChild(graphNode);
}
for (int i = 0; i < _openTabs.Count; i++)
{
_openTabs[i].CachedGraphNodes.Remove(graphNode);
}
graphNode.Free();
}
private void DisposeCachedGraphVisuals()
{
for (int i = 0; i < _openTabs.Count; i++)
{
DisposeCachedGraphVisuals(_openTabs[i]);
}
}
private void DisposeCachedGraphVisuals(GraphTab tab)
{
for (int i = tab.CachedGraphNodes.Count - 1; i >= 0; i--)
{
StatescriptGraphNode graphNode = tab.CachedGraphNodes[i];
if (!IsInstanceValid(graphNode))
{
tab.CachedGraphNodes.RemoveAt(i);
continue;
}
RemoveGraphNodeVisual(graphNode);
}
}
private GraphTab? FindTab(StatescriptGraph graph)
{
for (int i = 0; i < _openTabs.Count; i++)
{
if (_openTabs[i].GraphResource == graph || (!string.IsNullOrEmpty(graph.ResourcePath)
&& _openTabs[i].ResourcePath == graph.ResourcePath))
{
return _openTabs[i];
}
}
return null;
}
private void SetCurrentTabWithoutLoading(int tabIndex)
{
if (_tabBar is null)
{
return;
}
bool wasLoading = _isLoadingGraph;
_isLoadingGraph = true;
_tabBar.CurrentTab = tabIndex;
_isLoadingGraph = wasLoading;
}
private void UpdateNextNodeId(StatescriptGraph graph)
{
var maxId = 0;
foreach (var nodeId in graph.Nodes.Select(x => x.NodeId))
int maxId = 0;
foreach (string? nodeId in graph.Nodes.Select(x => x.NodeId))
{
if (nodeId.StartsWith("node_", StringComparison.InvariantCultureIgnoreCase)
&& int.TryParse(nodeId["node_".Length..], out var id)
&& int.TryParse(nodeId["node_".Length..], out int id)
&& id >= maxId)
{
maxId = id + 1;
@@ -946,7 +1159,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
for (var i = 0; i < _openTabs.Count; i++)
for (int i = 0; i < _openTabs.Count; i++)
{
_tabBar.SetTabTitle(i, _openTabs[i].GraphResource.StatescriptName);
}
@@ -990,7 +1203,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
for (var i = 0; i < _openTabs.Count; i++)
for (int i = 0; i < _openTabs.Count; i++)
{
if (i == newTabIndex)
{
@@ -1007,6 +1220,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
if (_variablePanel is not null)
{
_openTabs[i].VariablesPanelOpen = _variablePanel.Visible;
_openTabs[i].SelectedVariableName = _variablePanel.GetSelectedVariableName();
}
return;
@@ -1098,10 +1312,16 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
_variablePanel.Visible = pressed;
var current = _tabBar.CurrentTab;
int current = _tabBar.CurrentTab;
if (current >= 0 && current < _openTabs.Count)
{
_openTabs[current].VariablesPanelOpen = pressed;
_persistedVariablesPanelVisible = pressed;
if (!pressed)
{
_openTabs[current].SelectedVariableName = null;
}
}
if (pressed)
@@ -1110,9 +1330,17 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
if (graph is not null)
{
_variablePanel.SetGraph(graph);
if (current >= 0 && current < _openTabs.Count)
{
_variablePanel.RestoreSelectedVariable(_openTabs[current].SelectedVariableName);
}
}
}
else
{
_variablePanel.ClearSelectedVariable();
}
}
private void OnGraphVariablesChanged()
{
@@ -1122,6 +1350,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
InvalidateCachedGraphVisuals(graph);
LoadGraphIntoEditor(graph);
}
@@ -1131,29 +1360,125 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
}
private void OnVariableHighlightChanged(string? variableName)
{
int current = _tabBar?.CurrentTab ?? -1;
if (current >= 0 && current < _openTabs.Count)
{
_openTabs[current].SelectedVariableName = variableName;
}
if (!string.IsNullOrEmpty(variableName))
{
SharedVariableHighlightState.SetSelection(null, null);
}
ReapplyCurrentNodeHighlights();
}
private void OnGraphNodePropertyBindingChanged()
{
ReapplyCurrentNodeHighlights();
}
private void SubscribeSharedVariableHighlightState()
{
if (_sharedVariableHighlightSubscribed)
{
return;
}
SharedVariableHighlightState.Changed += OnSharedVariableHighlightChanged;
_sharedVariableHighlightSubscribed = true;
}
private void UnsubscribeSharedVariableHighlightState()
{
if (!_sharedVariableHighlightSubscribed)
{
return;
}
SharedVariableHighlightState.Changed -= OnSharedVariableHighlightChanged;
_sharedVariableHighlightSubscribed = false;
}
private void OnSharedVariableHighlightChanged()
{
if (SharedVariableHighlightState.HasAnySelection())
{
ClearGraphVariableSelections();
return;
}
ApplySharedVariableHighlightToNodes();
}
private void ApplySharedVariableHighlightToNodes()
{
if (_graphEdit is null)
{
return;
}
bool hasSelection = SharedVariableHighlightState.TryGetActiveSelection(
out string sharedVariableSetPath,
out string sharedVariableName);
foreach (Node child in _graphEdit.GetChildren())
{
if (child is StatescriptGraphNode graphNode)
{
graphNode.SetHighlightedSharedVariable(
hasSelection ? sharedVariableSetPath : null,
hasSelection ? sharedVariableName : null);
}
}
}
private void ReapplyCurrentNodeHighlights()
{
if (_graphEdit is null)
{
return;
}
string? selectedVariableName = null;
int current = _tabBar?.CurrentTab ?? -1;
if (current >= 0 && current < _openTabs.Count)
{
selectedVariableName = _openTabs[current].SelectedVariableName;
}
foreach (Node child in _graphEdit.GetChildren())
{
if (child is StatescriptGraphNode graphNode)
{
graphNode.SetHighlightedVariable(variableName);
graphNode.SetHighlightedVariable(selectedVariableName);
}
}
ApplySharedVariableHighlightToNodes();
}
private void ClearGraphVariableSelections()
{
for (int i = 0; i < _openTabs.Count; i++)
{
_openTabs[i].SelectedVariableName = null;
}
if (_variablePanel is not null)
{
_variablePanel.ClearSelectedVariable();
return;
}
ReapplyCurrentNodeHighlights();
}
private void EnsureVariablesPanelVisible()
{
if (_variablePanel is null || _variablesToggleButton is null || _openTabs.Count == 0)
{
return;
}
if (_variablePanel.Visible)
if (_variablePanel is null || _variablesToggleButton is null)
{
return;
}
@@ -1161,7 +1486,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
_variablePanel.Visible = true;
_variablesToggleButton.SetPressedNoSignal(true);
var current = _tabBar?.CurrentTab ?? -1;
int current = _tabBar?.CurrentTab ?? -1;
if (current >= 0 && current < _openTabs.Count)
{
_openTabs[current].VariablesPanelOpen = true;
@@ -1174,23 +1499,44 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
}
}
private void InvalidateCachedGraphVisuals(StatescriptGraph graph)
{
GraphTab? tab = FindTab(graph);
if (tab is null)
{
return;
}
for (int i = tab.CachedGraphNodes.Count - 1; i >= 0; i--)
{
StatescriptGraphNode graphNode = tab.CachedGraphNodes[i];
if (!IsInstanceValid(graphNode))
{
tab.CachedGraphNodes.RemoveAt(i);
continue;
}
RemoveGraphNodeVisual(graphNode);
}
}
private void OnFilesystemChanged()
{
for (var i = 0; i < _openTabs.Count; i++)
for (int i = 0; i < _openTabs.Count; i++)
{
_openTabs[i].UpdateCachedPathIfMissing();
}
for (var i = _openTabs.Count - 1; i >= 0; i--)
for (int i = _openTabs.Count - 1; i >= 0; i--)
{
var path = _openTabs[i].ResourcePath;
string path = _openTabs[i].ResourcePath;
if (string.IsNullOrEmpty(path))
{
continue;
}
var filePath = GetBaseFilePath(path);
string filePath = GetBaseFilePath(path);
if (!FileAccess.FileExists(filePath))
{
CloseTabByIndex(i);
@@ -1208,14 +1554,50 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return;
}
var shouldShow = _openTabs[tabIndex].VariablesPanelOpen;
bool shouldShow = _openTabs[tabIndex].VariablesPanelOpen;
if ((_tabBar?.CurrentTab ?? -1) == tabIndex)
{
shouldShow = _persistedVariablesPanelVisible;
_openTabs[tabIndex].VariablesPanelOpen = shouldShow;
}
_variablePanel.Visible = shouldShow;
_variablesToggleButton.SetPressedNoSignal(shouldShow);
if (shouldShow)
{
_variablePanel.SetGraph(_openTabs[tabIndex].GraphResource);
_variablePanel.RestoreSelectedVariable(_openTabs[tabIndex].SelectedVariableName);
}
else
{
_variablePanel.ClearSelectedVariable();
}
}
private void PersistCurrentVariablePanelState()
{
if (_variablePanel is null || _tabBar is null)
{
return;
}
int current = _tabBar.CurrentTab;
if (current < 0 || current >= _openTabs.Count)
{
return;
}
_openTabs[current].VariablesPanelOpen = _variablePanel.Visible;
_openTabs[current].SelectedVariableName = _variablePanel.GetSelectedVariableName();
_persistedVariablesPanelVisible = _variablePanel.Visible;
}
private GraphTab[] GetPersistedTabs()
{
PersistCurrentVariablePanelState();
return [.. _openTabs.Where(x => !string.IsNullOrEmpty(x.ResourcePath))];
}
private void OnGraphEditGuiInput(InputEvent @event)
@@ -1350,7 +1732,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return -1;
}
for (var i = 0; i < graphNode.GetChildCount(); i++)
for (int i = 0; i < graphNode.GetChildCount(); i++)
{
if (graphNode.IsSlotEnabledLeft(i))
{
@@ -1374,7 +1756,7 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
return -1;
}
for (var i = 0; i < graphNode.GetChildCount(); i++)
for (int i = 0; i < graphNode.GetChildCount(); i++)
{
if (graphNode.IsSlotEnabledRight(i))
{
@@ -1391,12 +1773,16 @@ public partial class StatescriptGraphEditorDock : EditorDock, ISerializationList
public StatescriptGraph GraphResource { get; }
public List<StatescriptGraphNode> CachedGraphNodes { get; } = [];
public string ResourcePath => !string.IsNullOrEmpty(GraphResource?.ResourcePath)
? GraphResource.ResourcePath
: _cachedPath;
public bool VariablesPanelOpen { get; set; }
public string? SelectedVariableName { get; set; }
public GraphTab(StatescriptGraph graphResource)
{
GraphResource = graphResource;

View File

@@ -1,6 +1,10 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
@@ -9,17 +13,48 @@ namespace Gamesmiths.Forge.Godot.Editor.Statescript;
public partial class StatescriptGraphNode
{
private bool ReferencesVariable(string variableName)
private readonly Dictionary<OptionButton, StyleBoxFlat> _baseDropdownStyles = [];
private StyleBoxFlat? _basePanelStyle;
private StyleBoxFlat? _baseSelectedPanelStyle;
private static bool ResolverReferencesVariable(StatescriptResolverResource? resolver, string variableName)
{
if (NodeResource is null)
if (resolver is null || string.IsNullOrEmpty(variableName))
{
return false;
}
foreach (StatescriptNodeProperty binding in NodeResource.PropertyBindings)
var visited = new HashSet<nint>();
return ResolverReferencesVariableRecursive(resolver, variableName, visited);
}
private static bool ResolverReferencesVariableRecursive(
StatescriptResolverResource resolver,
string variableName,
HashSet<nint> visited)
{
if (binding.Resolver is VariableResolverResource varRes
&& varRes.VariableName == variableName)
nint instanceId = (nint)resolver.GetInstanceId();
if (!visited.Add(instanceId))
{
return false;
}
if (resolver is VariableResolverResource variableResolver
&& string.Equals(variableResolver.VariableName, variableName, StringComparison.Ordinal))
{
return true;
}
foreach (PropertyInfo property in resolver.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!property.CanRead || !typeof(StatescriptResolverResource).IsAssignableFrom(property.PropertyType))
{
continue;
}
if (property.GetValue(resolver) is StatescriptResolverResource nestedResolver
&& ResolverReferencesVariableRecursive(nestedResolver, variableName, visited))
{
return true;
}
@@ -28,23 +63,103 @@ public partial class StatescriptGraphNode
return false;
}
private static bool ResolverReferencesSharedVariable(
StatescriptResolverResource? resolver,
string sharedVariableSetPath,
string variableName)
{
if (resolver is null
|| string.IsNullOrEmpty(sharedVariableSetPath)
|| string.IsNullOrEmpty(variableName))
{
return false;
}
var visited = new HashSet<nint>();
return ResolverReferencesSharedVariableRecursive(resolver, sharedVariableSetPath, variableName, visited);
}
private static bool ResolverReferencesSharedVariableRecursive(
StatescriptResolverResource resolver,
string sharedVariableSetPath,
string variableName,
HashSet<nint> visited)
{
nint instanceId = (nint)resolver.GetInstanceId();
if (!visited.Add(instanceId))
{
return false;
}
if (resolver is SharedVariableResolverResource sharedVariableResolver
&& string.Equals(sharedVariableResolver.SharedVariableSetPath, sharedVariableSetPath, StringComparison.Ordinal)
&& string.Equals(sharedVariableResolver.VariableName, variableName, StringComparison.Ordinal))
{
return true;
}
foreach (PropertyInfo property in resolver.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!property.CanRead || !typeof(StatescriptResolverResource).IsAssignableFrom(property.PropertyType))
{
continue;
}
if (property.GetValue(resolver) is StatescriptResolverResource nestedResolver
&& ResolverReferencesSharedVariableRecursive(
nestedResolver,
sharedVariableSetPath,
variableName,
visited))
{
return true;
}
}
return false;
}
private bool ReferencesVariable(string variableName)
{
if (NodeResource is null)
{
return false;
}
return NodeResource.PropertyBindings.Any(binding => ResolverReferencesVariable(binding.Resolver, variableName));
}
private bool ReferencesSharedVariable(string? sharedVariableSetPath, string? variableName)
{
if (NodeResource is null
|| string.IsNullOrEmpty(sharedVariableSetPath)
|| string.IsNullOrEmpty(variableName))
{
return false;
}
return NodeResource.PropertyBindings.Any(binding =>
ResolverReferencesSharedVariable(binding.Resolver, sharedVariableSetPath, variableName));
}
private void ApplyHighlightBorder()
{
if (_isHighlighted)
{
if (GetThemeStylebox("panel") is not StyleBoxFlat baseStyle)
EnsureBasePanelStylesStored();
if (_basePanelStyle is null || _baseSelectedPanelStyle is null)
{
return;
}
if (_isHighlighted)
{
StyleBoxFlat baseStyle = _basePanelStyle;
var highlightStyle = (StyleBoxFlat)baseStyle.Duplicate();
highlightStyle.BorderColor = _highlightColor;
highlightStyle.BorderWidthTop = 2;
highlightStyle.BorderWidthBottom = 2;
highlightStyle.BorderWidthLeft = 2;
highlightStyle.BorderWidthRight = 2;
highlightStyle.BgColor = baseStyle.BgColor.Lerp(_highlightColor, 0.15f);
AddThemeStyleboxOverride("panel", highlightStyle);
@@ -52,10 +167,8 @@ public partial class StatescriptGraphNode
}
else
{
RemoveThemeStyleboxOverride("panel");
RemoveThemeStyleboxOverride("panel_selected");
ApplyBottomPadding();
AddThemeStyleboxOverride("panel", (StyleBoxFlat)_basePanelStyle.Duplicate());
AddThemeStyleboxOverride("panel_selected", (StyleBoxFlat)_baseSelectedPanelStyle.Duplicate());
}
}
@@ -76,48 +189,107 @@ public partial class StatescriptGraphNode
{
HighlightLabelIfMatches(label);
}
else if (child is FoldableContainer foldable)
{
HighlightFoldableSummaryBadgeIfPresent(foldable);
}
else if (child is PanelContainer badge && badge.HasMeta("forge_inline_summary_badge_kind"))
{
HighlightSummaryBadgeIfMatches(badge);
}
UpdateHighlightsRecursive(child);
}
}
private void HighlightFoldableSummaryBadgeIfPresent(FoldableContainer foldable)
{
if (!InlineConstantSummaryFormatter.TryGetSummaryBadgeForHighlighting(foldable, out PanelContainer? badge))
{
return;
}
HighlightSummaryBadgeIfMatches(badge);
}
private void HighlightOptionButtonIfMatches(OptionButton dropdown)
{
if (!dropdown.HasMeta("is_variable_dropdown"))
EnsureBaseDropdownStyleStored(dropdown);
if (!dropdown.HasMeta("is_variable_dropdown") && !dropdown.HasMeta("is_shared_variable_dropdown"))
{
return;
}
if (string.IsNullOrEmpty(_highlightedVariableName))
if (!_baseDropdownStyles.TryGetValue(dropdown, out StyleBoxFlat? baseStyle))
{
dropdown.RemoveThemeStyleboxOverride("normal");
return;
}
var selectedIdx = dropdown.Selected;
if (string.IsNullOrEmpty(_highlightedVariableName)
&& (string.IsNullOrEmpty(_highlightedSharedVariableSetPath)
|| string.IsNullOrEmpty(_highlightedSharedVariableName)))
{
dropdown.AddThemeStyleboxOverride("normal", (StyleBoxFlat)baseStyle.Duplicate());
return;
}
int selectedIdx = dropdown.Selected;
if (selectedIdx < 0)
{
dropdown.RemoveThemeStyleboxOverride("normal");
dropdown.AddThemeStyleboxOverride("normal", (StyleBoxFlat)baseStyle.Duplicate());
return;
}
var selectedText = dropdown.GetItemText(selectedIdx);
if (selectedText == _highlightedVariableName)
{
if (dropdown.GetThemeStylebox("normal") is not StyleBoxFlat baseStyle)
{
return;
}
string selectedText = dropdown.GetItemText(selectedIdx);
bool isSharedVariableDropdown = dropdown.HasMeta("is_shared_variable_dropdown");
string dropdownSetPath = dropdown.HasMeta("shared_variable_set_path")
? dropdown.GetMeta("shared_variable_set_path").AsString()
: string.Empty;
bool isMatch = isSharedVariableDropdown
? !string.IsNullOrEmpty(_highlightedSharedVariableSetPath)
&& !string.IsNullOrEmpty(_highlightedSharedVariableName)
&& selectedText == _highlightedSharedVariableName
&& dropdownSetPath == _highlightedSharedVariableSetPath
: selectedText == _highlightedVariableName;
if (isMatch)
{
var highlightStyle = (StyleBoxFlat)baseStyle.Duplicate();
highlightStyle.BgColor = baseStyle.BgColor.Lerp(_highlightColor, 0.25f);
dropdown.AddThemeStyleboxOverride("normal", highlightStyle);
}
else
{
dropdown.RemoveThemeStyleboxOverride("normal");
dropdown.AddThemeStyleboxOverride("normal", (StyleBoxFlat)baseStyle.Duplicate());
}
}
private void EnsureBasePanelStylesStored()
{
if (_basePanelStyle is null && GetThemeStylebox("panel") is StyleBoxFlat panelStyle)
{
_basePanelStyle = (StyleBoxFlat)panelStyle.Duplicate();
}
if (_baseSelectedPanelStyle is null
&& GetThemeStylebox("panel_selected") is StyleBoxFlat selectedPanelStyle)
{
_baseSelectedPanelStyle = (StyleBoxFlat)selectedPanelStyle.Duplicate();
}
}
private void EnsureBaseDropdownStyleStored(OptionButton dropdown)
{
if (_baseDropdownStyles.ContainsKey(dropdown))
{
return;
}
if (dropdown.GetThemeStylebox("normal") is StyleBoxFlat baseStyle)
{
_baseDropdownStyles[dropdown] = (StyleBoxFlat)baseStyle.Duplicate();
}
}
@@ -145,5 +317,75 @@ public partial class StatescriptGraphNode
label.RemoveMeta("is_highlight_colored");
}
}
private void HighlightSummaryBadgeIfMatches(PanelContainer badge)
{
if (badge.GetNodeOrNull<Label>("Row/Text") is not Label textLabel
|| badge.GetNodeOrNull<Label>("Row/Icon") is not Label iconLabel)
{
return;
}
if (!badge.HasMeta("forge_inline_summary_badge_base_stylebox")
&& badge.GetThemeStylebox("panel") is StyleBoxFlat baseStyle)
{
badge.SetMeta("forge_inline_summary_badge_base_stylebox", Variant.From(baseStyle.Duplicate()));
}
if (!badge.HasMeta("forge_inline_summary_badge_base_stylebox")
|| badge.GetMeta("forge_inline_summary_badge_base_stylebox").Obj is not StyleBoxFlat storedBase)
{
return;
}
string propagatedVariableName = badge.HasMeta("forge_inline_summary_badge_highlight_variable")
? badge.GetMeta("forge_inline_summary_badge_highlight_variable").AsString()
: string.Empty;
string propagatedSharedVariableName = badge.HasMeta("forge_inline_summary_badge_highlight_shared_variable")
? badge.GetMeta("forge_inline_summary_badge_highlight_shared_variable").AsString()
: string.Empty;
string propagatedSharedVariableSetPath = badge.HasMeta("forge_inline_summary_badge_highlight_shared_set_path")
? badge.GetMeta("forge_inline_summary_badge_highlight_shared_set_path").AsString()
: string.Empty;
bool isMatch = !string.IsNullOrEmpty(_highlightedVariableName)
&& (textLabel.Text == _highlightedVariableName
|| propagatedVariableName == _highlightedVariableName);
isMatch = isMatch
|| (!string.IsNullOrEmpty(_highlightedSharedVariableSetPath)
&& !string.IsNullOrEmpty(_highlightedSharedVariableName)
&& (textLabel.Text == _highlightedSharedVariableName
|| propagatedSharedVariableName == _highlightedSharedVariableName)
&& propagatedSharedVariableSetPath == _highlightedSharedVariableSetPath);
badge.SetMeta("forge_inline_summary_badge_selected_variable", Variant.From(_highlightedVariableName ?? string.Empty));
badge.SetMeta(
"forge_inline_summary_badge_selected_shared_set_path",
Variant.From(_highlightedSharedVariableSetPath ?? string.Empty));
badge.SetMeta(
"forge_inline_summary_badge_selected_shared_variable",
Variant.From(_highlightedSharedVariableName ?? string.Empty));
var style = (StyleBoxFlat)storedBase.Duplicate();
if (isMatch)
{
style.BorderWidthLeft = Math.Max(style.BorderWidthLeft, 2);
style.BorderWidthTop = Math.Max(style.BorderWidthTop, 2);
style.BorderWidthRight = Math.Max(style.BorderWidthRight, 2);
style.BorderWidthBottom = Math.Max(style.BorderWidthBottom, 2);
style.BorderColor = _highlightColor;
style.BgColor = _highlightColor;
iconLabel.AddThemeColorOverride("font_color", Colors.Black);
textLabel.AddThemeColorOverride("font_color", Colors.Black);
}
else
{
iconLabel.RemoveThemeColorOverride("font_color");
textLabel.RemoveThemeColorOverride("font_color");
}
badge.AddThemeStyleboxOverride("panel", style);
}
}
#endif

View File

@@ -24,23 +24,43 @@ public partial class StatescriptGraphNode
}
var container = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
sectionContainer.AddChild(container);
var key = new PropertySlotKey(StatescriptPropertyDirection.Input, index);
string baseTitle = $"{propInfo.Label}:";
var propertyFoldable = new FoldableContainer
{
Title = baseTitle,
Folded = GetFoldState(GetInputPropertyFoldKey(index), true),
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
_foldableKeys[propertyFoldable] = GetInputPropertyFoldKey(index);
_inputPropertyFoldables[key] = new InputPropertyFoldableContext(propertyFoldable, baseTitle);
propertyFoldable.FoldingChanged += OnSectionFoldingChanged;
sectionContainer.AddChild(propertyFoldable);
propertyFoldable.AddChild(container);
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
container.AddChild(headerRow);
var nameLabel = new Label
{
Text = propInfo.Label,
CustomMinimumSize = new Vector2(60, 0),
};
nameLabel.AddThemeColorOverride("font_color", _inputPropertyColor);
headerRow.AddChild(nameLabel);
List<Func<NodeEditorProperty>> resolverFactories =
StatescriptResolverRegistry.GetCompatibleFactories(propInfo.ExpectedType);
if (propInfo.IsArray)
{
resolverFactories.RemoveAll(factory =>
{
return StatescriptResolverRegistry.GetResolverTypeId(factory) != "ArrayVariable";
});
}
else
{
resolverFactories.RemoveAll(factory =>
{
return StatescriptResolverRegistry.GetResolverTypeId(factory) == "ArrayVariable";
});
}
if (resolverFactories.Count == 0)
{
var errorLabel = new Label
@@ -49,7 +69,8 @@ public partial class StatescriptGraphNode
};
errorLabel.AddThemeColorOverride("font_color", Colors.Red);
headerRow.AddChild(errorLabel);
container.AddChild(errorLabel);
UpdateInputPropertyFoldableTitle(key);
return;
}
@@ -61,20 +82,18 @@ public partial class StatescriptGraphNode
foreach (Func<NodeEditorProperty> factory in resolverFactories)
{
using NodeEditorProperty temp = factory();
resolverDropdown.AddItem(temp.DisplayName);
resolverDropdown.AddItem(StatescriptResolverRegistry.GetDisplayName(factory));
}
StatescriptNodeProperty? binding = FindBinding(StatescriptPropertyDirection.Input, index);
var selectedIndex = 0;
int selectedIndex = 0;
if (binding?.Resolver is not null)
{
for (var i = 0; i < resolverFactories.Count; i++)
for (int i = 0; i < resolverFactories.Count; i++)
{
using NodeEditorProperty temp = resolverFactories[i]();
if (temp.ResolverTypeId == GetResolverTypeId(binding.Resolver))
if (StatescriptResolverRegistry.GetResolverTypeId(resolverFactories[i])
== GetResolverTypeId(binding.Resolver))
{
selectedIndex = i;
break;
@@ -83,16 +102,7 @@ public partial class StatescriptGraphNode
}
else
{
for (var i = 0; i < resolverFactories.Count; i++)
{
using NodeEditorProperty temp = resolverFactories[i]();
if (temp.ResolverTypeId == "Variant")
{
selectedIndex = i;
break;
}
}
selectedIndex = StatescriptResolverRegistry.GetDefaultFactoryIndex(resolverFactories, propInfo.IsArray);
}
resolverDropdown.Selected = selectedIndex;
@@ -101,7 +111,6 @@ public partial class StatescriptGraphNode
var editorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
container.AddChild(editorContainer);
var key = new PropertySlotKey(StatescriptPropertyDirection.Input, index);
_inputPropertyContexts[key] = new InputPropertyContext(resolverFactories, propInfo, editorContainer);
ShowResolverEditorUI(
@@ -113,8 +122,11 @@ public partial class StatescriptGraphNode
index,
propInfo.IsArray);
var capturedIndex = index;
resolverDropdown.ItemSelected += selectedItem => OnInputResolverDropdownItemSelected(selectedItem, capturedIndex);
UpdateInputPropertyFoldableTitle(key);
int capturedIndex = index;
resolverDropdown.ItemSelected +=
selectedItem => OnInputResolverDropdownItemSelected(selectedItem, capturedIndex);
}
private void OnInputResolverDropdownItemSelected(long x, int index)
@@ -155,24 +167,29 @@ public partial class StatescriptGraphNode
SaveResolverEditor(editor, StatescriptPropertyDirection.Input, index);
}
UpdateInputPropertyFoldableTitle(key);
StatescriptNodeProperty? updated = FindBinding(StatescriptPropertyDirection.Input, index);
var newResolver = updated?.Resolver?.Duplicate() as StatescriptResolverResource;
if (_undoRedo is not null)
{
_undoRedo.CreateAction("Change Resolver Type", customContext: _graph);
_undoRedo.AddDoMethod(
this,
MethodName.ApplyResolverBinding,
(int)StatescriptPropertyDirection.Input,
index,
newResolver ?? new StatescriptResolverResource());
Variant.From(newResolver));
_undoRedo.AddUndoMethod(
this,
MethodName.ApplyResolverBinding,
(int)StatescriptPropertyDirection.Input,
index,
oldResolver ?? new StatescriptResolverResource());
Variant.From(oldResolver));
_undoRedo.CommitAction(false);
}
@@ -212,12 +229,12 @@ public partial class StatescriptGraphNode
}
StatescriptNodeProperty? binding = FindBinding(StatescriptPropertyDirection.Output, index);
var selectedIndex = 0;
int selectedIndex = 0;
if (binding?.Resolver is VariableResolverResource varRes
&& !string.IsNullOrEmpty(varRes.VariableName))
{
for (var i = 0; i < _graph.Variables.Count; i++)
for (int i = 0; i < _graph.Variables.Count; i++)
{
if (_graph.Variables[i].VariableName == varRes.VariableName)
{
@@ -233,14 +250,15 @@ public partial class StatescriptGraphNode
if (binding is null)
{
var variableName = _graph.Variables[selectedIndex].VariableName;
string variableName = _graph.Variables[selectedIndex].VariableName;
EnsureBinding(StatescriptPropertyDirection.Output, index).Resolver =
new VariableResolverResource { VariableName = variableName };
}
}
var capturedIndex = index;
variableDropdown.ItemSelected += selectedItem => OnOutputVariableDropdownItemSelected(selectedItem, capturedIndex);
int capturedIndex = index;
variableDropdown.ItemSelected +=
selectedItem => OnOutputVariableDropdownItemSelected(selectedItem, capturedIndex);
hBox.AddChild(variableDropdown);
}
@@ -255,25 +273,28 @@ public partial class StatescriptGraphNode
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
as StatescriptResolverResource;
var variableName = _graph.Variables[(int)x].VariableName;
string variableName = _graph.Variables[(int)x].VariableName;
var newResolver = new VariableResolverResource { VariableName = variableName };
EnsureBinding(StatescriptPropertyDirection.Output, index).Resolver = newResolver;
if (_undoRedo is not null)
{
_undoRedo.CreateAction("Change Output Variable", customContext: _graph);
_undoRedo.AddDoMethod(
this,
MethodName.ApplyResolverBinding,
(int)StatescriptPropertyDirection.Output,
index,
(StatescriptResolverResource)newResolver.Duplicate());
_undoRedo.AddUndoMethod(
this,
MethodName.ApplyResolverBinding,
(int)StatescriptPropertyDirection.Output,
index,
oldResolver ?? new StatescriptResolverResource());
Variant.From(oldResolver));
_undoRedo.CommitAction(false);
}
@@ -295,6 +316,7 @@ public partial class StatescriptGraphNode
}
NodeEditorProperty resolverEditor = factory();
resolverEditor.ConfigureAllowedExpectedTypes(expectedType);
var key = new PropertySlotKey(direction, propertyIndex);
@@ -333,18 +355,21 @@ public partial class StatescriptGraphNode
if (_undoRedo is not null)
{
_undoRedo.CreateAction("Change Node Property", customContext: _graph);
_undoRedo.AddDoMethod(
this,
MethodName.ApplyResolverBinding,
(int)direction,
propertyIndex,
newResolver ?? new StatescriptResolverResource());
Variant.From(newResolver));
_undoRedo.AddUndoMethod(
this,
MethodName.ApplyResolverBinding,
(int)direction,
propertyIndex,
oldResolver ?? new StatescriptResolverResource());
Variant.From(oldResolver));
_undoRedo.CommitAction(false);
}

View File

@@ -19,6 +19,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
{
private const string FoldInputKey = "_fold_input";
private const string FoldOutputKey = "_fold_output";
private const string FoldInputPropertyKeyPrefix = "_fold_input_property_";
private const string CustomWidthKey = "_custom_width";
private static readonly Color _entryColor = new(0x2a4a8dff);
@@ -34,6 +35,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
private readonly Dictionary<PropertySlotKey, NodeEditorProperty> _activeResolverEditors = [];
private readonly Dictionary<FoldableContainer, string> _foldableKeys = [];
private readonly Dictionary<PropertySlotKey, InputPropertyFoldableContext> _inputPropertyFoldables = [];
private StatescriptNodeDiscovery.NodeTypeInfo? _typeInfo;
private StatescriptGraph? _graph;
@@ -42,6 +44,8 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
private bool _resizeConnected;
private float _widthBeforeResize;
private string? _highlightedVariableName;
private string? _highlightedSharedVariableSetPath;
private string? _highlightedSharedVariableName;
private bool _isHighlighted;
/// <summary>
@@ -79,9 +83,14 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
public void SetHighlightedVariable(string? variableName)
{
_highlightedVariableName = variableName;
_isHighlighted = !string.IsNullOrEmpty(variableName) && ReferencesVariable(variableName!);
ApplyHighlightBorder();
UpdateChildHighlights();
RefreshHighlightState();
}
public void SetHighlightedSharedVariable(string? sharedVariableSetPath, string? variableName)
{
_highlightedSharedVariableSetPath = sharedVariableSetPath;
_highlightedSharedVariableName = variableName;
RefreshHighlightState();
}
/// <summary>
@@ -95,6 +104,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
_graph = graph;
_activeResolverEditors.Clear();
_foldableKeys.Clear();
_inputPropertyFoldables.Clear();
Name = resource.NodeId;
Title = resource.Title;
@@ -119,6 +129,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
{
SetupNodeByType(resource.NodeType);
ApplyBottomPadding();
RefreshHighlightState();
return;
}
@@ -133,12 +144,14 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
}
ApplyBottomPadding();
RefreshHighlightState();
}
public void OnBeforeSerialize()
{
_inputPropertyContexts.Clear();
_foldableKeys.Clear();
_inputPropertyFoldables.Clear();
_activeCustomEditor?.Unbind();
_activeCustomEditor = null;
@@ -187,6 +200,16 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
return GetFoldState(key);
}
internal bool GetFoldStateInternal(string key, bool defaultValue)
{
return GetFoldState(key, defaultValue);
}
internal void SetFoldStateWithUndoInternal(string key, bool folded)
{
SetFoldStateWithUndo(key, folded);
}
internal StatescriptNodeProperty? FindBindingInternal(
StatescriptPropertyDirection direction,
int propertyIndex)
@@ -226,13 +249,15 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
MethodName.ApplyResolverBinding,
(int)direction,
propertyIndex,
newResolver ?? new StatescriptResolverResource());
Variant.From(newResolver));
_undoRedo.AddUndoMethod(
this,
MethodName.ApplyResolverBinding,
(int)direction,
propertyIndex,
oldResolver ?? new StatescriptResolverResource());
Variant.From(oldResolver));
_undoRedo.CommitAction(false);
}
@@ -253,6 +278,11 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
PropertyBindingChanged?.Invoke();
}
internal void UpdateInputPropertyFoldableTitlesInternal()
{
UpdateInputPropertyFoldableTitles();
}
private static string GetResolverTypeId(StatescriptResolverResource resolver)
{
return resolver.ResolverTypeId;
@@ -267,11 +297,16 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
}
}
private static string GetInputPropertyFoldKey(int propertyIndex)
{
return $"{FoldInputPropertyKeyPrefix}{propertyIndex}";
}
private void SetupFromTypeInfo(StatescriptNodeDiscovery.NodeTypeInfo typeInfo)
{
var maxSlots = Math.Max(typeInfo.InputPortLabels.Length, typeInfo.OutputPortLabels.Length);
int maxSlots = Math.Max(typeInfo.InputPortLabels.Length, typeInfo.OutputPortLabels.Length);
for (var slot = 0; slot < maxSlots; slot++)
for (int slot = 0; slot < maxSlots; slot++)
{
var hBox = new HBoxContainer();
hBox.AddThemeConstantOverride("separation", 16);
@@ -342,14 +377,14 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
{
if (typeInfo.InputPropertiesInfo.Length > 0)
{
var folded = GetFoldState(FoldInputKey);
bool folded = GetFoldState(FoldInputKey);
FoldableContainer inputContainer = AddPropertySectionDivider(
"Input Properties",
_inputPropertyColor,
FoldInputKey,
folded);
for (var i = 0; i < typeInfo.InputPropertiesInfo.Length; i++)
for (int i = 0; i < typeInfo.InputPropertiesInfo.Length; i++)
{
AddInputPropertyRow(typeInfo.InputPropertiesInfo[i], i, inputContainer);
}
@@ -357,14 +392,14 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
if (typeInfo.OutputVariablesInfo.Length > 0)
{
var folded = GetFoldState(FoldOutputKey);
bool folded = GetFoldState(FoldOutputKey);
FoldableContainer outputContainer = AddPropertySectionDivider(
"Output Variables",
_outputVariableColor,
FoldOutputKey,
folded);
for (var i = 0; i < typeInfo.OutputVariablesInfo.Length; i++)
for (int i = 0; i < typeInfo.OutputVariablesInfo.Length; i++)
{
AddOutputVariableRow(typeInfo.OutputVariablesInfo[i], i, outputContainer);
}
@@ -384,6 +419,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
{
Title = sectionTitle,
Folded = folded,
CustomMinimumSize = new Vector2(192, 0),
};
sectionContainer.AddThemeColorOverride("font_color", color);
@@ -400,24 +436,52 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
{
foreach (KeyValuePair<FoldableContainer, string> kvp in _foldableKeys.Where(kvp => IsInstanceValid(kvp.Key)))
{
var stored = GetFoldState(kvp.Value);
bool stored = GetFoldState(kvp.Value);
if (kvp.Key.Folded != stored)
{
SetFoldStateWithUndo(kvp.Value, kvp.Key.Folded);
}
}
UpdateInputPropertyFoldableTitles();
RefreshHighlightState();
ResetSize();
}
private void UpdateInputPropertyFoldableTitle(PropertySlotKey key)
{
if (!_inputPropertyFoldables.TryGetValue(key, out InputPropertyFoldableContext? context)
|| !IsInstanceValid(context.Foldable))
{
return;
}
_activeResolverEditors.TryGetValue(key, out NodeEditorProperty? editor);
InlineConstantSummaryFormatter.ApplyFoldableTitle(context.BaseTitle, context.Foldable, editor);
}
private void UpdateInputPropertyFoldableTitles()
{
foreach (PropertySlotKey key in _inputPropertyFoldables.Keys.ToArray())
{
UpdateInputPropertyFoldableTitle(key);
}
}
private bool GetFoldState(string key)
{
return GetFoldState(key, false);
}
private bool GetFoldState(string key, bool defaultValue)
{
if (NodeResource is not null && NodeResource.CustomData.TryGetValue(key, out Variant value))
{
return value.AsBool();
}
return false;
return defaultValue;
}
private void SetFoldState(string key, bool folded)
@@ -437,7 +501,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
return;
}
var oldFolded = GetFoldState(key);
bool oldFolded = GetFoldState(key);
if (oldFolded == folded)
{
@@ -478,12 +542,12 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
private void OnResizeEnd(Vector2 newSize)
{
var newWidth = CustomMinimumSize.X;
float newWidth = CustomMinimumSize.X;
if (_undoRedo is not null && NodeResource is not null
&& !Mathf.IsEqualApprox(_widthBeforeResize, newWidth))
{
var oldWidth = _widthBeforeResize;
float oldWidth = _widthBeforeResize;
_undoRedo.CreateAction("Resize Node", customContext: _graph);
_undoRedo.AddDoMethod(
@@ -512,7 +576,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
if (NodeResource is not null
&& NodeResource.CustomData.TryGetValue(CustomWidthKey, out Variant value))
{
var width = (float)value.AsDouble();
float width = (float)value.AsDouble();
if (width > 0)
{
@@ -534,7 +598,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
private void ApplyResolverBinding(
int directionInt,
int propertyIndex,
StatescriptResolverResource resolver)
Variant resolverVariant)
{
if (NodeResource is null)
{
@@ -543,7 +607,9 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
var direction = (StatescriptPropertyDirection)directionInt;
StatescriptNodeProperty binding = EnsureBinding(direction, propertyIndex);
binding.Resolver = resolver;
binding.Resolver = resolverVariant.VariantType == Variant.Type.Nil
? null
: resolverVariant.AsGodotObject() as StatescriptResolverResource;
RebuildNode();
}
@@ -560,6 +626,14 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
Size = new Vector2(Size.X, 0);
}
private void RefreshHighlightState()
{
_isHighlighted = (!string.IsNullOrEmpty(_highlightedVariableName) && ReferencesVariable(_highlightedVariableName))
|| ReferencesSharedVariable(_highlightedSharedVariableSetPath, _highlightedSharedVariableName);
ApplyHighlightBorder();
UpdateChildHighlights();
}
private StatescriptNodeProperty? FindBinding(
StatescriptPropertyDirection direction,
int propertyIndex)
@@ -607,7 +681,7 @@ public partial class StatescriptGraphNode : GraphNode, ISerializationListener
return;
}
for (var i = NodeResource.PropertyBindings.Count - 1; i >= 0; i--)
for (int i = NodeResource.PropertyBindings.Count - 1; i >= 0; i--)
{
StatescriptNodeProperty binding = NodeResource.PropertyBindings[i];

View File

@@ -49,7 +49,7 @@ internal static class StatescriptNodeDiscovery
{
IReadOnlyList<NodeTypeInfo> types = GetDiscoveredNodeTypes();
for (var i = 0; i < types.Count; i++)
for (int i = 0; i < types.Count; i++)
{
if (types[i].RuntimeTypeName == runtimeTypeName)
{
@@ -131,11 +131,11 @@ internal static class StatescriptNodeDiscovery
private static NodeTypeInfo BuildNodeTypeInfo(Type type, StatescriptNodeType nodeType)
{
var displayName = FormatDisplayName(type.Name);
var runtimeTypeName = type.FullName!;
string displayName = FormatDisplayName(type.Name);
string runtimeTypeName = type.FullName!;
// Get constructor parameter names.
var constructorParamNames = GetConstructorParameterNames(type);
string[] constructorParamNames = GetConstructorParameterNames(type);
// Determine ports and description by instantiating a temporary node.
string[] inputLabels;
@@ -194,8 +194,8 @@ internal static class StatescriptNodeDiscovery
ConstructorInfo constructor = constructors.OrderBy(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++)
{
Type paramType = parameters[i].ParameterType;
@@ -236,8 +236,8 @@ internal static class StatescriptNodeDiscovery
private static string[] GetInputPortLabels(Node node, StatescriptNodeType nodeType)
{
var count = node.InputPorts.Length;
var labels = new string[count];
int count = node.InputPorts.Length;
string[] labels = new string[count];
switch (nodeType)
{
@@ -268,7 +268,7 @@ internal static class StatescriptNodeDiscovery
labels[1] = "Abort";
}
for (var i = 2; i < count; i++)
for (int i = 2; i < count; i++)
{
labels[i] = $"Input {i}";
}
@@ -276,7 +276,7 @@ internal static class StatescriptNodeDiscovery
break;
default:
for (var i = 0; i < count; i++)
for (int i = 0; i < count; i++)
{
labels[i] = $"Input {i}";
}
@@ -289,8 +289,8 @@ internal static class StatescriptNodeDiscovery
private static string[] GetOutputPortLabels(Node node, StatescriptNodeType nodeType)
{
var count = node.OutputPorts.Length;
var labels = new string[count];
int count = node.OutputPorts.Length;
string[] labels = new string[count];
switch (nodeType)
{
@@ -336,7 +336,7 @@ internal static class StatescriptNodeDiscovery
labels[3] = "Subgraph";
}
for (var i = 4; i < count; i++)
for (int i = 4; i < count; i++)
{
labels[i] = $"Event {i}";
}
@@ -344,7 +344,7 @@ internal static class StatescriptNodeDiscovery
break;
default:
for (var i = 0; i < count; i++)
for (int i = 0; i < count; i++)
{
labels[i] = $"Output {i}";
}
@@ -357,10 +357,10 @@ internal static class StatescriptNodeDiscovery
private static bool[] GetSubgraphFlags(Node node)
{
var count = node.OutputPorts.Length;
var flags = new bool[count];
int count = node.OutputPorts.Length;
bool[] flags = new bool[count];
for (var i = 0; i < count; i++)
for (int i = 0; i < count; i++)
{
flags[i] = node.OutputPorts[i] is SubgraphPort;
}
@@ -371,7 +371,7 @@ internal static class StatescriptNodeDiscovery
private static InputPropertyInfo[] GetInputPropertiesInfo(Node node)
{
var propertiesInfo = new InputPropertyInfo[node.InputProperties.Length];
for (var i = 0; i < node.InputProperties.Length; i++)
for (int i = 0; i < node.InputProperties.Length; i++)
{
propertiesInfo[i] = new InputPropertyInfo(
node.InputProperties[i].Label,
@@ -384,7 +384,7 @@ internal static class StatescriptNodeDiscovery
private static OutputVariableInfo[] GetOutputVariablesInfo(Node node)
{
var variablesInfo = new OutputVariableInfo[node.OutputVariables.Length];
for (var i = 0; i < node.OutputVariables.Length; i++)
for (int i = 0; i < node.OutputVariables.Length; i++)
{
variablesInfo[i] = new OutputVariableInfo(
node.OutputVariables[i].Label,
@@ -425,7 +425,7 @@ internal static class StatescriptNodeDiscovery
// Insert spaces before capital letters for camelCase names.
var result = new System.Text.StringBuilder();
for (var i = 0; i < typeName.Length; i++)
for (int i = 0; i < typeName.Length; i++)
{
if (i > 0 && char.IsUpper(typeName[i]) && !char.IsUpper(typeName[i - 1]))
{

View File

@@ -36,19 +36,61 @@ internal static class StatescriptResolverRegistry
/// <returns>A list of compatible resolver editor factories.</returns>
public static List<Func<NodeEditorProperty>> GetCompatibleFactories(Type expectedType)
{
var result = new List<Func<NodeEditorProperty>>();
return [.. _factories.Where(factory => IsCompatibleFactory(factory, expectedType))];
}
foreach (Func<NodeEditorProperty> factory in _factories)
public static int GetDefaultFactoryIndex(List<Func<NodeEditorProperty>> factories, bool isArray)
{
using NodeEditorProperty temp = factory();
if (temp.IsCompatibleWith(expectedType))
for (int i = 0; i < factories.Count; i++)
{
result.Add(factory);
if (isArray)
{
if (GetResolverTypeId(factories[i]) == "ArrayVariable")
{
return i;
}
}
else if (GetResolverTypeId(factories[i]) == "Variant")
{
return i;
}
}
return result;
return 0;
}
public static string GetDisplayName(Func<NodeEditorProperty> factory)
{
return UseTemporaryEditor(factory, static editor => editor.DisplayName);
}
public static string GetResolverTypeId(Func<NodeEditorProperty> factory)
{
return UseTemporaryEditor(factory, static editor => editor.ResolverTypeId);
}
public static bool IsCompatibleFactory(Func<NodeEditorProperty> factory, Type expectedType)
{
return UseTemporaryEditor(factory, editor => editor.IsCompatibleWith(expectedType));
}
private static TResult UseTemporaryEditor<TResult>(
Func<NodeEditorProperty> factory,
Func<NodeEditorProperty, TResult> selector)
{
NodeEditorProperty editor = factory();
try
{
return selector(editor);
}
finally
{
if (global::Godot.GodotObject.IsInstanceValid(editor))
{
editor.Free();
}
}
}
}
#endif

View File

@@ -68,6 +68,12 @@ internal sealed partial class StatescriptVariablePanel
private void DoRemoveVariable(StatescriptGraph graph, StatescriptGraphVariable variable, int index)
{
if (_selectedVariableName == variable.VariableName)
{
_selectedVariableName = null;
VariableHighlightChanged?.Invoke(null);
}
graph.Variables.RemoveAt(index);
ClearReferencesToVariable(variable.VariableName);
RebuildList();

View File

@@ -73,7 +73,7 @@ internal sealed partial class StatescriptVariablePanel
var headerRow = new HBoxContainer();
vBox.AddChild(headerRow);
var isExpanded = _expandedArrays.Contains(variable.VariableName);
bool isExpanded = _expandedArrays.Contains(variable.VariableName);
var elementsContainer = new VBoxContainer
{
@@ -93,7 +93,7 @@ internal sealed partial class StatescriptVariablePanel
{
elementsContainer.Visible = x;
var wasExpanded = !x;
bool wasExpanded = !x;
if (x)
{
@@ -162,9 +162,9 @@ internal sealed partial class StatescriptVariablePanel
vBox.AddChild(elementsContainer);
for (var i = 0; i < variable.InitialArrayValues.Count; i++)
for (int i = 0; i < variable.InitialArrayValues.Count; i++)
{
var capturedIndex = i;
int capturedIndex = i;
if (variable.VariableType == StatescriptVariableType.Bool)
{

View File

@@ -17,6 +17,11 @@ namespace Gamesmiths.Forge.Godot.Editor.Statescript;
internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISerializationListener
{
private const string ExpandedArraysMetaKey = "_expanded_arrays";
private const string HeaderRowNodeName = "HeaderRow";
private const string AddButtonNodeName = "AddButton";
private const string VariablesScrollNodeName = "VariablesScroll";
private const string VariableListNodeName = "VariableList";
private const string VariableNameButtonMetaKey = "_variable_name_button";
private static readonly Color _variableColor = new(0xe5c07bff);
private static readonly Color _highlightColor = new(0x56b6c2ff);
@@ -64,7 +69,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
SizeFlagsVertical = SizeFlags.ExpandFill;
CustomMinimumSize = new Vector2(360, 0);
var headerHBox = new HBoxContainer();
var headerHBox = new HBoxContainer { Name = HeaderRowNodeName };
AddChild(headerHBox);
var titleLabel = new Label
@@ -77,6 +82,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
_addButton = new Button
{
Name = AddButtonNodeName,
Icon = _addIcon,
Flat = true,
TooltipText = "Add Variable",
@@ -91,6 +97,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
var scrollContainer = new ScrollContainer
{
Name = VariablesScrollNodeName,
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
@@ -99,6 +106,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
_variableList = new VBoxContainer
{
Name = VariableListNodeName,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
@@ -108,45 +116,19 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
public override void _ExitTree()
{
base._ExitTree();
if (_addButton is not null)
{
_addButton.Pressed -= OnAddPressed;
}
_creationDialog?.QueueFree();
_creationDialog = null;
_newNameEdit = null;
_newTypeDropdown = null;
_newArrayToggle = null;
ReleaseUiState();
}
public void OnBeforeSerialize()
{
if (_addButton is not null)
{
_addButton.Pressed -= OnAddPressed;
}
if (_variableList is not null)
{
foreach (Node child in _variableList.GetChildren())
{
_variableList.RemoveChild(child);
child.Free();
}
}
_creationDialog?.Free();
_creationDialog = null;
_newNameEdit = null;
_newTypeDropdown = null;
_newArrayToggle = null;
ReleaseUiState();
}
public void OnAfterDeserialize()
{
if (_addButton is not null)
EnsureControlsCached();
if (_addButton is not null && IsInstanceValid(_addButton))
{
_addButton.Pressed += OnAddPressed;
}
@@ -165,6 +147,31 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
RebuildList();
}
public string? GetSelectedVariableName()
{
return _selectedVariableName;
}
public void RestoreSelectedVariable(string? variableName)
{
if (string.IsNullOrEmpty(variableName) || !HasVariableNamed(variableName))
{
_selectedVariableName = null;
}
else
{
_selectedVariableName = variableName;
}
RefreshVariableSelectionVisuals();
VariableHighlightChanged?.Invoke(_selectedVariableName);
}
public void ClearSelectedVariable()
{
RestoreSelectedVariable(null);
}
/// <summary>
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
/// </summary>
@@ -179,26 +186,95 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
/// </summary>
public void RebuildList()
{
EnsureControlsCached();
if (_variableList is null)
{
return;
}
ClearVariableList();
if (_graph is null)
{
return;
}
for (int i = 0; i < _graph.Variables.Count; i++)
{
AddVariableRow(_graph.Variables[i], i);
}
}
private static int FindTypeDropdownIndex(OptionButton dropdown, StatescriptVariableType variableType)
{
for (int i = 0; i < dropdown.ItemCount; i++)
{
if (dropdown.GetItemId(i) == (int)variableType)
{
return i;
}
}
return 0;
}
private static void UpdateVariableNameButtonAppearance(Button button, bool isSelected)
{
Color buttonColor = isSelected ? _highlightColor : _variableColor;
button.AddThemeColorOverride("font_color", buttonColor);
button.AddThemeColorOverride("font_pressed_color", _highlightColor);
button.AddThemeColorOverride("font_hover_color", buttonColor.Lightened(0.2f));
button.AddThemeColorOverride("font_hover_pressed_color", _highlightColor.Lightened(0.2f));
}
private void EnsureControlsCached()
{
_addButton ??= GetNodeOrNull<Button>($"{HeaderRowNodeName}/{AddButtonNodeName}");
_variableList ??= GetNodeOrNull<VBoxContainer>($"{VariablesScrollNodeName}/{VariableListNodeName}");
}
private void ClearVariableList()
{
EnsureControlsCached();
if (_variableList is null || !IsInstanceValid(_variableList))
{
return;
}
foreach (Node child in _variableList.GetChildren())
{
_variableList.RemoveChild(child);
child.Free();
}
if (_graph is null)
{
return;
}
for (var i = 0; i < _graph.Variables.Count; i++)
private void ReleaseUiState()
{
AddVariableRow(_graph.Variables[i], i);
if (_addButton is not null && IsInstanceValid(_addButton))
{
_addButton.Pressed -= OnAddPressed;
}
ClearVariableList();
if (_creationDialog is not null && IsInstanceValid(_creationDialog))
{
if (_creationDialog is AcceptDialog acceptDialog)
{
acceptDialog.Confirmed -= OnCreationConfirmed;
}
_creationDialog.Free();
}
_creationDialog = null;
_newNameEdit = null;
_newTypeDropdown = null;
_newArrayToggle = null;
_variableList = null;
_addButton = null;
}
private void SaveExpandedArrayState()
@@ -208,7 +284,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
return;
}
var packed = new string[_expandedArrays.Count];
string[] packed = new string[_expandedArrays.Count];
_expandedArrays.CopyTo(packed);
_graph.SetMeta(ExpandedArraysMetaKey, Variant.From(packed));
}
@@ -226,7 +302,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
if (meta.VariantType == Variant.Type.PackedStringArray)
{
foreach (var name in meta.AsStringArray())
foreach (string name in meta.AsStringArray())
{
_expandedArrays.Add(name);
}
@@ -254,7 +330,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
rowContainer.AddChild(headerRow);
var isSelected = _selectedVariableName == variable.VariableName;
bool isSelected = _selectedVariableName == variable.VariableName;
var nameButton = new Button
{
@@ -266,28 +342,15 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
Alignment = HorizontalAlignment.Left,
};
Color buttonColor = isSelected ? _highlightColor : _variableColor;
nameButton.AddThemeColorOverride("font_color", buttonColor);
nameButton.AddThemeColorOverride("font_pressed_color", _highlightColor);
nameButton.AddThemeColorOverride("font_hover_color", buttonColor.Lightened(0.2f));
nameButton.AddThemeColorOverride("font_hover_pressed_color", _highlightColor.Lightened(0.2f));
nameButton.SetMeta(VariableNameButtonMetaKey, variable.VariableName);
UpdateVariableNameButtonAppearance(nameButton, isSelected);
nameButton.AddThemeFontOverride(
"font",
EditorInterface.Singleton.GetEditorTheme().GetFont("bold", "EditorFonts"));
nameButton.Toggled += pressed =>
{
if (pressed)
{
_selectedVariableName = variable.VariableName;
}
else if (_selectedVariableName == variable.VariableName)
{
_selectedVariableName = null;
}
RebuildList();
VariableHighlightChanged?.Invoke(_selectedVariableName);
SetSelectedVariable(variable.VariableName, pressed);
};
headerRow.AddChild(nameButton);
@@ -301,7 +364,7 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
typeLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.6f));
headerRow.AddChild(typeLabel);
var capturedIndex = index;
int capturedIndex = index;
var deleteButton = new Button
{
@@ -328,6 +391,47 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
rowContainer.AddChild(new HSeparator());
}
private void SetSelectedVariable(string variableName, bool selected)
{
if (selected)
{
_selectedVariableName = variableName;
}
else if (_selectedVariableName == variableName)
{
_selectedVariableName = null;
}
RefreshVariableSelectionVisuals();
VariableHighlightChanged?.Invoke(_selectedVariableName);
}
private void RefreshVariableSelectionVisuals()
{
if (_variableList is null)
{
return;
}
RefreshVariableSelectionVisualsRecursive(_variableList);
}
private void RefreshVariableSelectionVisualsRecursive(Node parent)
{
foreach (Node child in parent.GetChildren())
{
if (child is Button button && button.HasMeta(VariableNameButtonMetaKey))
{
string variableName = button.GetMeta(VariableNameButtonMetaKey).AsString();
bool isSelected = _selectedVariableName == variableName;
button.SetPressedNoSignal(isSelected);
UpdateVariableNameButtonAppearance(button, isSelected);
}
RefreshVariableSelectionVisualsRecursive(child);
}
}
private void OnAddPressed()
{
if (_graph is null)
@@ -373,12 +477,14 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
StatescriptVariableType[] allTypes = StatescriptVariableTypeConverter.GetAllTypes();
for (var t = 0; t < allTypes.Length; t++)
for (int t = 0; t < allTypes.Length; t++)
{
_newTypeDropdown.AddItem(StatescriptVariableTypeConverter.GetDisplayName(allTypes[t]), t);
_newTypeDropdown.AddItem(
StatescriptVariableTypeConverter.GetDisplayName(allTypes[t]),
(int)allTypes[t]);
}
_newTypeDropdown.Selected = (int)StatescriptVariableType.Int;
_newTypeDropdown.Selected = FindTypeDropdownIndex(_newTypeDropdown, StatescriptVariableType.Int);
typeRow.AddChild(_newTypeDropdown);
var arrayRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
@@ -404,20 +510,20 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
return;
}
var name = _newNameEdit.Text.Trim();
string name = _newNameEdit.Text.Trim();
if (string.IsNullOrEmpty(name) || HasVariableNamed(name))
{
return;
}
var selectedIndex = _newTypeDropdown.Selected;
int selectedIndex = _newTypeDropdown.Selected;
if (selectedIndex < 0)
{
return;
}
var selectedId = _newTypeDropdown.GetItemId(selectedIndex);
int selectedId = _newTypeDropdown.GetItemId(selectedIndex);
var varType = (StatescriptVariableType)selectedId;
var newVariable = new StatescriptGraphVariable
@@ -431,8 +537,8 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
if (_undoRedo is not null)
{
_undoRedo.CreateAction("Add Graph Variable", customContext: _graph);
_undoRedo.AddDoMethod(this, MethodName.DoAddVariable, _graph!, newVariable);
_undoRedo.AddUndoMethod(this, MethodName.UndoAddVariable, _graph!, newVariable);
_undoRedo.AddDoMethod(this, MethodName.DoAddVariable, _graph, newVariable);
_undoRedo.AddUndoMethod(this, MethodName.UndoAddVariable, _graph, newVariable);
_undoRedo.CommitAction();
}
else
@@ -459,8 +565,8 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
if (_undoRedo is not null)
{
_undoRedo.CreateAction("Remove Graph Variable", customContext: _graph);
_undoRedo.AddDoMethod(this, MethodName.DoRemoveVariable, _graph!, variable, index);
_undoRedo.AddUndoMethod(this, MethodName.UndoRemoveVariable, _graph!, variable, index);
_undoRedo.AddDoMethod(this, MethodName.DoRemoveVariable, _graph, variable, index);
_undoRedo.AddUndoMethod(this, MethodName.UndoRemoveVariable, _graph, variable, index);
_undoRedo.CommitAction();
}
else
@@ -497,8 +603,8 @@ internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISeriali
}
const string baseName = "variable";
var counter = 1;
var name = baseName;
int counter = 1;
string name = baseName;
while (HasVariableNamed(name))
{

View File

@@ -0,0 +1,196 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.NodeEditors;
/// <summary>
/// Custom editor for <c>DebugNode</c>. Lets the user choose the concrete value type to debug, then constrains the
/// source resolver picker to that type so debug output can be interpreted correctly.
/// </summary>
[Tool]
internal sealed partial class DebugNodeEditor : CustomNodeEditor
{
private const string ValueTypeKey = "valueType";
private const string TypeFoldKey = "_debug_type_fold";
private StatescriptVariableType _selectedType = StatescriptVariableType.Int;
private VBoxContainer? _inputRootContainer;
private VBoxContainer? _inputEditorContainer;
private StatescriptNodeDiscovery.NodeTypeInfo? _cachedTypeInfo;
private OptionButton? _typeDropdown;
private FoldableContainer? _typeFoldable;
/// <inheritdoc/>
public override string HandledRuntimeTypeName => "Gamesmiths.Forge.Statescript.Nodes.Action.DebugNode";
/// <inheritdoc/>
public override void BuildPropertySections(StatescriptNodeDiscovery.NodeTypeInfo typeInfo)
{
_cachedTypeInfo = typeInfo;
if (NodeResource.CustomData.TryGetValue(ValueTypeKey, out Variant storedType))
{
_selectedType = (StatescriptVariableType)storedType.AsInt32();
}
bool inputFolded = GetFoldState("_fold_input");
FoldableContainer inputContainer = AddPropertySectionDivider(
"Input Properties",
InputPropertyColor,
"_fold_input",
inputFolded);
_inputRootContainer = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
inputContainer.AddChild(_inputRootContainer);
BuildTypeRow(_inputRootContainer);
if (typeInfo.InputPropertiesInfo.Length > 0)
{
RebuildInputEditor(typeInfo.InputPropertiesInfo[0]);
}
}
private static void AddTypeOption(OptionButton dropdown, StatescriptVariableType type, string label)
{
dropdown.AddItem(label, (int)type);
}
private void BuildTypeRow(VBoxContainer root)
{
_typeFoldable = new FoldableContainer
{
Title = "Type:",
Folded = GetFoldState(TypeFoldKey, true),
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
_typeFoldable.FoldingChanged += OnTypeFoldableFoldingChanged;
root.AddChild(_typeFoldable);
var container = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
_typeFoldable.AddChild(container);
var headerRow = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
container.AddChild(headerRow);
_typeDropdown = new OptionButton
{
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
CustomMinimumSize = new Vector2(80, 0),
};
foreach (StatescriptVariableType variableType in StatescriptVariableTypeConverter.GetAllTypes())
{
AddTypeOption(
_typeDropdown,
variableType,
StatescriptVariableTypeConverter.GetDisplayName(variableType));
}
_typeDropdown.Selected = FindSelectedTypeIndex(_typeDropdown);
_typeDropdown.ItemSelected += OnTypeDropdownItemSelected;
headerRow.AddChild(_typeDropdown);
UpdateTypeFoldableTitle();
}
private int FindSelectedTypeIndex(OptionButton dropdown)
{
for (int i = 0; i < dropdown.ItemCount; i++)
{
if (dropdown.GetItemId(i) == (int)_selectedType)
{
return i;
}
}
return 0;
}
private void OnTypeDropdownItemSelected(long index)
{
if (_typeDropdown is null)
{
return;
}
var selectedValue = (StatescriptVariableType)_typeDropdown.GetItemId((int)index);
NodeResource.CustomData[ValueTypeKey] = Variant.From((int)selectedValue);
_selectedType = selectedValue;
UpdateTypeFoldableTitle();
RemoveBinding(StatescriptPropertyDirection.Input, 0);
ActiveResolverEditors.Remove(new PropertySlotKey(StatescriptPropertyDirection.Input, 0));
if (_cachedTypeInfo is not null
&& _inputEditorContainer is not null
&& _cachedTypeInfo.InputPropertiesInfo.Length > 0)
{
RebuildInputEditor(_cachedTypeInfo.InputPropertiesInfo[0]);
}
RaisePropertyBindingChanged();
ResetSize();
}
private void OnTypeFoldableFoldingChanged(bool folded)
{
SetFoldStateWithUndo(TypeFoldKey, folded);
UpdateTypeFoldableTitle();
RefreshInputPropertyFoldableTitles();
ResetSize();
}
private void UpdateTypeFoldableTitle()
{
if (_typeFoldable is null)
{
return;
}
InlineConstantSummaryFormatter.ApplyFoldableTitle(
"Type:",
_typeFoldable,
StatescriptVariableTypeConverter.GetDisplayName(_selectedType),
InlineSummaryBadgeKind.Enum);
}
private void RebuildInputEditor(StatescriptNodeDiscovery.InputPropertyInfo originalInfo)
{
if (_inputRootContainer is null)
{
return;
}
ClearValueRows();
Type clrType = StatescriptVariableTypeConverter.ToSystemType(_selectedType);
_inputEditorContainer = _inputRootContainer;
AddInputPropertyRow(
new StatescriptNodeDiscovery.InputPropertyInfo(originalInfo.Label, clrType),
0,
_inputEditorContainer);
}
private void ClearValueRows()
{
if (_inputRootContainer is null)
{
return;
}
foreach (Node child in _inputRootContainer.GetChildren())
{
if (child == _typeFoldable)
{
continue;
}
_inputRootContainer.RemoveChild(child);
child.Free();
}
}
}
#endif

View File

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

View File

@@ -21,6 +21,8 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
{
private const string FoldInputKey = "_fold_input";
private const string FoldOutputKey = "_fold_output";
private const string ScopeFoldKey = "_fold_output_scope";
private const string TargetFoldKey = "_fold_output_target";
private const string ScopeKey = "_output_scope";
private readonly List<string> _setPaths = [];
@@ -33,6 +35,8 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
private VBoxContainer? _cachedInputEditorContainer;
private VBoxContainer? _cachedTargetContainer;
private int _cachedOutputIndex;
private FoldableContainer? _scopeFoldable;
private FoldableContainer? _targetFoldable;
private bool _isSharedScope;
@@ -50,7 +54,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
{
_cachedTypeInfo = typeInfo;
var inputFolded = GetFoldState(FoldInputKey);
bool inputFolded = GetFoldState(FoldInputKey);
FoldableContainer inputContainer = AddPropertySectionDivider(
"Input Properties",
InputPropertyColor,
@@ -66,7 +70,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
inputContainer.AddChild(inputEditorContainer);
var outputFolded = GetFoldState(FoldOutputKey);
bool outputFolded = GetFoldState(FoldOutputKey);
FoldableContainer outputContainer = AddPropertySectionDivider(
"Output Variables",
OutputVariableColor,
@@ -122,9 +126,9 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
private static void ScanFilesystemDirectory(EditorFileSystemDirectory dir, List<string> results)
{
for (var i = 0; i < dir.GetFileCount(); i++)
for (int i = 0; i < dir.GetFileCount(); i++)
{
var path = dir.GetFilePath(i);
string path = dir.GetFilePath(i);
if (!path.EndsWith(".tres", StringComparison.InvariantCultureIgnoreCase)
&& !path.EndsWith(".res", StringComparison.InvariantCultureIgnoreCase))
@@ -140,7 +144,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
}
}
for (var i = 0; i < dir.GetSubdirCount(); i++)
for (int i = 0; i < dir.GetSubdirCount(); i++)
{
ScanFilesystemDirectory(dir.GetSubdir(i), results);
}
@@ -190,22 +194,22 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
sectionContainer.AddChild(outerVBox);
_scopeFoldable = new FoldableContainer
{
Title = "Scope:",
Folded = GetFoldState(ScopeFoldKey, true),
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
_scopeFoldable.FoldingChanged += OnScopeFoldableFoldingChanged;
outerVBox.AddChild(_scopeFoldable);
// Scope toggle row.
var scopeRow = new HBoxContainer
{
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
outerVBox.AddChild(scopeRow);
var scopeLabel = new Label
{
Text = "Scope",
CustomMinimumSize = new Vector2(60, 0),
};
scopeLabel.AddThemeColorOverride("font_color", OutputVariableColor);
scopeRow.AddChild(scopeLabel);
_scopeFoldable.AddChild(scopeRow);
var graphButton = new CheckBox
{
@@ -228,6 +232,15 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
scopeRow.AddChild(graphButton);
scopeRow.AddChild(sharedButton);
_targetFoldable = new FoldableContainer
{
Title = $"{varInfo.Label}:",
Folded = GetFoldState(TargetFoldKey, true),
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
_targetFoldable.FoldingChanged += OnTargetFoldableFoldingChanged;
outerVBox.AddChild(_targetFoldable);
// Target variable container (rebuilt when scope changes).
var targetContainer = new VBoxContainer
{
@@ -235,9 +248,11 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
};
_cachedTargetContainer = targetContainer;
outerVBox.AddChild(targetContainer);
_targetFoldable.AddChild(targetContainer);
RebuildTargetUI(varInfo, index, targetContainer);
UpdateScopeFoldableTitle();
UpdateTargetFoldableTitle(varInfo.Label);
graphButton.Pressed += () => OnScopeChanged(false, varInfo, index);
sharedButton.Pressed += () => OnScopeChanged(true, varInfo, index);
@@ -259,7 +274,9 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
as StatescriptResolverResource;
_isSharedScope = isShared;
NodeResource.CustomData[ScopeKey] = Variant.From(isShared ? (int)VariableScope.Shared : (int)VariableScope.Graph);
NodeResource.CustomData[ScopeKey] = Variant.From(isShared
? (int)VariableScope.Shared
: (int)VariableScope.Graph);
// Clear the output binding since scope changed.
RemoveBinding(StatescriptPropertyDirection.Output, index);
@@ -310,6 +327,8 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
newInputResolver,
"Change Variable Scope Input");
UpdateScopeFoldableTitle();
UpdateTargetFoldableTitle(varInfo.Label);
RaisePropertyBindingChanged();
ResetSize();
}
@@ -365,12 +384,12 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
}
StatescriptNodeProperty? binding = FindBinding(StatescriptPropertyDirection.Output, index);
var selectedIndex = 0;
int selectedIndex = 0;
if (binding?.Resolver is VariableResolverResource varRes
&& !string.IsNullOrEmpty(varRes.VariableName))
{
for (var i = 0; i < Graph.Variables.Count; i++)
for (int i = 0; i < Graph.Variables.Count; i++)
{
if (Graph.Variables[i].VariableName == varRes.VariableName)
{
@@ -432,6 +451,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
});
_sharedVarDropdown = new OptionButton { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
_sharedVarDropdown.SetMeta("is_shared_variable_dropdown", true);
PopulateSharedVariableDropdown();
varRow.AddChild(_sharedVarDropdown);
@@ -452,9 +472,9 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
_setDropdown.AddItem("(None)");
_setPaths.Add(string.Empty);
foreach (var path in FindAllSharedVariableSetPaths())
foreach (string path in FindAllSharedVariableSetPaths())
{
var displayName = path[(path.LastIndexOf('/') + 1)..];
string displayName = path[(path.LastIndexOf('/') + 1)..];
if (displayName.EndsWith(".tres", StringComparison.OrdinalIgnoreCase))
{
@@ -466,7 +486,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
}
// Restore selection.
for (var i = 0; i < _setPaths.Count; i++)
for (int i = 0; i < _setPaths.Count; i++)
{
if (_setPaths[i] == _selectedSetPath)
{
@@ -486,6 +506,8 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
return;
}
_sharedVarDropdown.SetMeta("shared_variable_set_path", _selectedSetPath);
_sharedVarDropdown.Clear();
_variableNames.Clear();
@@ -498,7 +520,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
if (set is not null)
{
foreach (var variableName in set.Variables.Select(x => x.VariableName))
foreach (string? variableName in set.Variables.Select(x => x.VariableName))
{
if (string.IsNullOrEmpty(variableName))
{
@@ -512,7 +534,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
}
// Restore selection.
for (var i = 0; i < _variableNames.Count; i++)
for (int i = 0; i < _variableNames.Count; i++)
{
if (_variableNames[i] == _selectedSharedVarName)
{
@@ -532,7 +554,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
return;
}
var idx = _setDropdown.Selected;
int idx = _setDropdown.Selected;
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver?.Duplicate()
as StatescriptResolverResource;
@@ -597,7 +619,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
return;
}
var idx = _sharedVarDropdown.Selected;
int idx = _sharedVarDropdown.Selected;
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver?.Duplicate()
as StatescriptResolverResource;
@@ -605,7 +627,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
as StatescriptResolverResource;
StatescriptVariableType? previousType = _resolvedType;
var previousIsArray = _resolvedIsArray;
bool previousIsArray = _resolvedIsArray;
if (idx >= 0 && idx < _variableNames.Count)
{
@@ -666,6 +688,11 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
"Change Shared Target Variable Input");
}
if (_cachedTypeInfo?.OutputVariablesInfo.Length > 0)
{
UpdateTargetFoldableTitle(_cachedTypeInfo.OutputVariablesInfo[0].Label);
}
RaisePropertyBindingChanged();
ResetSize();
}
@@ -724,11 +751,11 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
return;
}
var index = _cachedOutputIndex;
var variableIndex = (int)x - 1;
int index = _cachedOutputIndex;
int variableIndex = (int)x - 1;
StatescriptVariableType? previousType = _resolvedType;
var previousIsArray = _resolvedIsArray;
bool previousIsArray = _resolvedIsArray;
var oldOutputResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
as StatescriptResolverResource;
@@ -743,7 +770,7 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
}
else
{
var variableName = Graph.Variables[variableIndex].VariableName;
string variableName = Graph.Variables[variableIndex].VariableName;
EnsureBinding(StatescriptPropertyDirection.Output, index).Resolver =
new VariableResolverResource { VariableName = variableName };
@@ -787,10 +814,97 @@ internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
"Change Target Variable Input");
}
if (_cachedTypeInfo?.OutputVariablesInfo.Length > 0)
{
UpdateTargetFoldableTitle(_cachedTypeInfo.OutputVariablesInfo[0].Label);
}
RaisePropertyBindingChanged();
ResetSize();
}
private void OnScopeFoldableFoldingChanged(bool folded)
{
SetFoldStateWithUndo(ScopeFoldKey, folded);
UpdateScopeFoldableTitle();
RaisePropertyBindingChanged();
ResetSize();
}
private void OnTargetFoldableFoldingChanged(bool folded)
{
SetFoldStateWithUndo(TargetFoldKey, folded);
if (_cachedTypeInfo?.OutputVariablesInfo.Length > 0)
{
UpdateTargetFoldableTitle(_cachedTypeInfo.OutputVariablesInfo[0].Label);
}
RaisePropertyBindingChanged();
ResetSize();
}
private void UpdateScopeFoldableTitle()
{
if (_scopeFoldable is null)
{
return;
}
InlineConstantSummaryFormatter.ApplyFoldableTitle(
"Scope:",
_scopeFoldable,
_isSharedScope ? "Shared" : "Graph",
InlineSummaryBadgeKind.Enum);
}
private void UpdateTargetFoldableTitle(string label)
{
if (_targetFoldable is null)
{
return;
}
string summary = _isSharedScope
? _selectedSharedVarName
: GetSelectedGraphVariableName();
InlineSummaryBadgeKind badgeKind = _isSharedScope
? InlineSummaryBadgeKind.SharedVariable
: InlineSummaryBadgeKind.Variable;
string highlightedVariableName = !_isSharedScope
? summary
: string.Empty;
string highlightedSharedVariableSetPath = _isSharedScope ? _selectedSetPath : string.Empty;
string highlightedSharedVariableName = _isSharedScope ? summary : string.Empty;
InlineConstantSummaryFormatter.ApplyFoldableTitle(
$"{label}:",
_targetFoldable,
string.IsNullOrWhiteSpace(summary) ? "(None)" : summary,
badgeKind,
highlightedVariableName: highlightedVariableName,
highlightedSharedVariableSetPath: highlightedSharedVariableSetPath,
highlightedSharedVariableName: highlightedSharedVariableName);
}
private string GetSelectedGraphVariableName()
{
if (_cachedOutputIndex < 0)
{
return string.Empty;
}
if (FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver
is VariableResolverResource varRes)
{
return varRes.VariableName;
}
return string.Empty;
}
private void RebuildInputUI(
StatescriptNodeDiscovery.InputPropertyInfo propInfo,
VBoxContainer container)

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ACosHResolverEditor : ScalarUnaryResolverEditorBase<ACosHResolverResource>
{
public override string DisplayName => "ACosH";
public override string ResolverTypeId => "ACosH";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ACosResolverEditor : ScalarUnaryResolverEditorBase<ACosResolverResource>
{
public override string DisplayName => "ACos";
public override string ResolverTypeId => "ACos";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ASinHResolverEditor : ScalarUnaryResolverEditorBase<ASinHResolverResource>
{
public override string DisplayName => "ASinH";
public override string ResolverTypeId => "ASinH";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ASinResolverEditor : ScalarUnaryResolverEditorBase<ASinResolverResource>
{
public override string DisplayName => "ASin";
public override string ResolverTypeId => "ASin";
}
#endif

View File

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

View File

@@ -0,0 +1,21 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ATan2ResolverEditor : ScalarBinaryResolverEditorBase<ATan2ResolverResource>
{
public override string DisplayName => "ATan2";
public override string ResolverTypeId => "ATan2";
protected override string LeftTitle => "Y:";
protected override string RightTitle => "X:";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ATanHResolverEditor : ScalarUnaryResolverEditorBase<ATanHResolverResource>
{
public override string DisplayName => "ATanH";
public override string ResolverTypeId => "ATanH";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ATanResolverEditor : ScalarUnaryResolverEditorBase<ATanResolverResource>
{
public override string DisplayName => "ATan";
public override string ResolverTypeId => "ATan";
}
#endif

View File

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

View File

@@ -0,0 +1,34 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class AbsResolverEditor : NumericOrVectorUnaryResolverEditorBase<AbsResolverResource>
{
public override string DisplayName => "Abs";
public override string ResolverTypeId => "Abs";
protected override Type[] GetFactoryExpectedTypes(Type expectedType)
{
return expectedType == typeof(ForgeVariant128)
? [typeof(int), typeof(float), typeof(double), typeof(SysVector2), typeof(SysVector3), typeof(SysVector4)]
: [expectedType];
}
protected override Type GetNestedExpectedType(Type expectedType)
{
return expectedType;
}
}
#endif

View File

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

View File

@@ -129,6 +129,11 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
_onChanged = null;
}
private static bool IsCompatibleType(Type expectedType, StatescriptVariableType fieldType)
{
return StatescriptVariableTypeConverter.IsCompatible(expectedType, fieldType);
}
private static string FindExistingProvider(StatescriptGraph graph, StatescriptNodeProperty? currentProperty)
{
foreach (StatescriptNode node in graph.Nodes)
@@ -158,13 +163,7 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
return null;
}
Type? type = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.FirstOrDefault(
x => typeof(IActivationDataProvider).IsAssignableFrom(x)
&& !x.IsAbstract
&& !x.IsInterface
&& x.Name == className);
Type? type = ResolveProviderType(className);
if (type is null)
{
@@ -174,6 +173,43 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
return Activator.CreateInstance(type) as IActivationDataProvider;
}
private static IEnumerable<Type> GetProviderTypes()
{
return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(x => typeof(IActivationDataProvider).IsAssignableFrom(x)
&& !x.IsAbstract
&& !x.IsInterface);
}
private static string GetProviderIdentifier(Type type)
{
return type.FullName ?? type.Name;
}
private static bool ProviderMatches(Type type, string providerIdentifier)
{
return type.AssemblyQualifiedName == providerIdentifier
|| type.FullName == providerIdentifier
|| type.Name == providerIdentifier;
}
private static Type? ResolveProviderType(string providerIdentifier)
{
return GetProviderTypes().FirstOrDefault(x => ProviderMatches(x, providerIdentifier));
}
private static bool ProviderIdentifiersMatch(string left, string right)
{
if (left == right)
{
return true;
}
Type? leftType = ResolveProviderType(left);
return leftType is not null && ProviderMatches(leftType, right);
}
private void OnProviderDropdownItemSelected(long index)
{
if (_providerDropdown is null)
@@ -181,7 +217,7 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
return;
}
var idx = _providerDropdown.Selected;
int idx = _providerDropdown.Selected;
_selectedProviderClassName = idx >= 0 && idx < _providerClassNames.Count
? _providerClassNames[idx]
: string.Empty;
@@ -200,7 +236,7 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
return;
}
var dropdownIndex = _fieldDropdown.Selected;
int dropdownIndex = _fieldDropdown.Selected;
if (dropdownIndex >= 0 && dropdownIndex < _fieldNames.Count)
{
@@ -239,37 +275,39 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
_providerClassNames.Add(string.Empty);
// Re-scan the graph each time to pick up changes from other editors.
var graphLockedProvider = _graph is not null
string graphLockedProvider = _graph is not null
? FindExistingProvider(_graph, _currentProperty)
: string.Empty;
if (!string.IsNullOrEmpty(graphLockedProvider))
{
// Another node already uses a provider: only show that one (plus None).
_providerDropdown.AddItem(graphLockedProvider);
_providerClassNames.Add(graphLockedProvider);
Type? lockedProviderType = ResolveProviderType(graphLockedProvider);
string lockedProviderIdentifier = lockedProviderType is null
? graphLockedProvider
: GetProviderIdentifier(lockedProviderType);
string lockedProviderDisplayName = lockedProviderType?.Name ?? graphLockedProvider;
_providerDropdown.AddItem(lockedProviderDisplayName);
_providerClassNames.Add(lockedProviderIdentifier);
}
else
{
foreach (var name in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(x => typeof(IActivationDataProvider).IsAssignableFrom(x)
&& !x.IsAbstract
&& !x.IsInterface)
.Select(x => x.Name))
foreach (Type providerType in GetProviderTypes())
{
_providerDropdown.AddItem(name);
_providerClassNames.Add(name);
_providerDropdown.AddItem(providerType.Name);
_providerClassNames.Add(GetProviderIdentifier(providerType));
}
}
// Restore selection.
if (!string.IsNullOrEmpty(_selectedProviderClassName))
{
for (var i = 0; i < _providerClassNames.Count; i++)
for (int i = 0; i < _providerClassNames.Count; i++)
{
if (_providerClassNames[i] == _selectedProviderClassName)
if (ProviderIdentifiersMatch(_providerClassNames[i], _selectedProviderClassName))
{
_selectedProviderClassName = _providerClassNames[i];
_providerDropdown.Selected = i;
return;
}
@@ -306,8 +344,7 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
continue;
}
if (_expectedType != typeof(Variant128)
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, field.FieldType))
if (!IsCompatibleType(_expectedType, field.FieldType))
{
continue;
}
@@ -320,7 +357,7 @@ internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
// Restore selection.
if (!string.IsNullOrEmpty(_selectedFieldName))
{
for (var i = 0; i < _fieldNames.Count; i++)
for (int i = 0; i < _fieldNames.Count; i++)
{
if (_fieldNames[i] == _selectedFieldName)
{

View File

@@ -0,0 +1,42 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class AddResolverEditor : NumericVectorOrQuaternionBinaryResolverEditorBase<AddResolverResource>
{
public override string DisplayName => "Add";
public override string ResolverTypeId => "Add";
protected override Type[] GetFactoryExpectedTypes(Type expectedType)
{
return expectedType == typeof(ForgeVariant128)
? [
typeof(int),
typeof(float),
typeof(double),
typeof(SysVector2),
typeof(SysVector3),
typeof(SysVector4),
typeof(System.Numerics.Quaternion)
]
: [expectedType];
}
protected override Type GetNestedExpectedType(Type expectedType)
{
return expectedType;
}
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class AndResolverEditor : BooleanBinaryResolverEditorBase<AndResolverResource>
{
public override string DisplayName => "And";
public override string ResolverTypeId => "And";
}
#endif

View File

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

View File

@@ -0,0 +1,37 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class AngleResolverEditor
: VectorOrQuaternionBinaryFloatResolverEditorBase<AngleResolverResource>
{
public override string DisplayName => "Angle";
public override string ResolverTypeId => "Angle";
protected override string LeftTitle => "From:";
protected override string RightTitle => "To:";
protected override Type[] GetFactoryExpectedTypes(Type expectedType)
{
if (expectedType == typeof(ForgeVariant128) || ResolverEditorCompatibility.IsFloatType(expectedType))
{
return [typeof(SysVector2), typeof(SysVector3), typeof(SysVector4), typeof(System.Numerics.Quaternion)];
}
return [expectedType];
}
}
#endif

View File

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

View File

@@ -0,0 +1,85 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ArrayVariableResolverEditor : NodeEditorProperty
{
private VariantResolverEditor? _innerEditor;
public override string DisplayName => "Array";
public override string ResolverTypeId => "ArrayVariable";
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType.IsArray;
}
public override void Setup(
StatescriptGraph graph,
StatescriptNodeProperty? property,
Type expectedType,
Action onChanged,
bool isArray)
{
_innerEditor = new VariantResolverEditor();
Type elementExpectedType = expectedType.IsArray ? expectedType.GetElementType() ?? typeof(int) : expectedType;
VariantResolverResource? tempResolver = null;
if (property?.Resolver is ArrayVariableResolverResource existing)
{
tempResolver = new VariantResolverResource
{
IsArray = true,
ValueType = existing.ValueType,
ArrayValues = [.. existing.ArrayValues],
IsArrayExpanded = existing.IsArrayExpanded,
};
}
StatescriptNodeProperty? tempProperty = tempResolver is null
? null
: new StatescriptNodeProperty { Resolver = tempResolver };
_innerEditor.Setup(graph, tempProperty, elementExpectedType, onChanged, true);
_innerEditor.LayoutSizeChanged += RaiseLayoutSizeChanged;
AddChild(_innerEditor);
}
public override void SaveTo(StatescriptNodeProperty property)
{
if (_innerEditor is null)
{
return;
}
var tempProperty = new StatescriptNodeProperty();
_innerEditor.SaveTo(tempProperty);
if (tempProperty.Resolver is not VariantResolverResource variant)
{
return;
}
property.Resolver = new ArrayVariableResolverResource
{
ValueType = variant.ValueType,
ArrayValues = [.. variant.ArrayValues],
IsArrayExpanded = variant.IsArrayExpanded,
};
}
public override void ClearCallbacks()
{
base.ClearCallbacks();
_innerEditor?.ClearCallbacks();
}
}
#endif

View File

@@ -0,0 +1 @@
uid://30cl3jyw0pvd

View File

@@ -135,7 +135,7 @@ internal sealed partial class AttributeResolverEditor : NodeEditorProperty
_setDropdown.Clear();
foreach (var option in EditorUtils.GetAttributeSetOptions())
foreach (string option in EditorUtils.GetAttributeSetOptions())
{
_setDropdown.AddItem(option);
}
@@ -143,7 +143,7 @@ internal sealed partial class AttributeResolverEditor : NodeEditorProperty
// Restore selection.
if (!string.IsNullOrEmpty(_selectedSetClass))
{
for (var i = 0; i < _setDropdown.GetItemCount(); i++)
for (int i = 0; i < _setDropdown.GetItemCount(); i++)
{
if (_setDropdown.GetItemText(i) == _selectedSetClass)
{
@@ -170,14 +170,14 @@ internal sealed partial class AttributeResolverEditor : NodeEditorProperty
_attributeDropdown.Clear();
foreach (var option in EditorUtils.GetAttributeOptions(_selectedSetClass))
foreach (string option in EditorUtils.GetAttributeOptions(_selectedSetClass))
{
_attributeDropdown.AddItem(option);
}
if (!string.IsNullOrEmpty(_selectedAttribute))
{
for (var i = 0; i < _attributeDropdown.GetItemCount(); i++)
for (int i = 0; i < _attributeDropdown.GetItemCount(); i++)
{
if (_attributeDropdown.GetItemText(i) == _selectedAttribute)
{

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class CbrtResolverEditor : ScalarUnaryResolverEditorBase<CbrtResolverResource>
{
public override string DisplayName => "Cbrt";
public override string ResolverTypeId => "Cbrt";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class CeilResolverEditor : FloatUnaryResolverEditorBase<CeilResolverResource>
{
public override string DisplayName => "Ceil";
public override string ResolverTypeId => "Ceil";
}
#endif

View File

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

View File

@@ -0,0 +1,47 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ClampMagnitudeResolverEditor
: AsymmetricBinaryNestedResolverEditorBase<ClampMagnitudeResolverResource>
{
public override string DisplayName => "Clamp Magnitude";
public override string ResolverTypeId => "ClampMagnitude";
protected override Type[] LeftFactoryExpectedTypes => [typeof(SysVector2), typeof(SysVector3), typeof(SysVector4)];
protected override Type[] RightFactoryExpectedTypes => ResolverEditorCompatibility.FloatOperandExpectedTypes;
protected override Type LeftNestedExpectedType => typeof(ForgeVariant128);
protected override Type RightNestedExpectedType => typeof(float);
protected override string LeftTitle => "Value:";
protected override string RightTitle => "Max Length:";
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(ForgeVariant128) || ResolverEditorCompatibility.IsVectorType(expectedType);
}
protected override Type[] GetLeftFactoryExpectedTypes(Type expectedType)
{
return expectedType == typeof(ForgeVariant128)
? [typeof(SysVector2), typeof(SysVector3), typeof(SysVector4)]
: [expectedType];
}
}
#endif

View File

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

View File

@@ -0,0 +1,40 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ClampResolverEditor : NumericOrVectorTernaryResolverEditorBase<ClampResolverResource>
{
public override string DisplayName => "Clamp";
public override string ResolverTypeId => "Clamp";
protected override string FirstTitle => "Value:";
protected override string SecondTitle => "Min:";
protected override string ThirdTitle => "Max:";
protected override Type[] GetFirstFactoryExpectedTypes(Type expectedType)
{
return expectedType == typeof(ForgeVariant128)
? [typeof(int), typeof(float), typeof(double), typeof(SysVector2), typeof(SysVector3), typeof(SysVector4)]
: [expectedType];
}
protected override Type[] GetSecondFactoryExpectedTypes(Type expectedType)
{
return GetFirstFactoryExpectedTypes(expectedType);
}
}
#endif

View File

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

View File

@@ -3,6 +3,7 @@
#if TOOLS
using System;
using System.Collections.Generic;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Gamesmiths.Forge.Statescript.Properties;
@@ -18,12 +19,16 @@ namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
{
private static readonly Type[] _numericExpectedTypes = [typeof(int), typeof(float), typeof(double)];
private StatescriptGraph? _graph;
private Action? _onChanged;
private OptionButton? _operationDropdown;
private VBoxContainer? _leftContainer;
private VBoxContainer? _rightContainer;
private FoldableContainer? _leftFoldable;
private FoldableContainer? _rightFoldable;
private OptionButton? _leftResolverDropdown;
private OptionButton? _rightResolverDropdown;
@@ -63,7 +68,7 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(vBox);
_numericFactories = StatescriptResolverRegistry.GetCompatibleFactories(typeof(ForgeVariant128));
_numericFactories = ResolverEditorFactoryCatalog.GetCompatibleFactories(_numericExpectedTypes);
_numericFactories.RemoveAll(x =>
{
@@ -78,12 +83,17 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
_operation = comparisonResolver.Operation;
}
var leftFoldable = new FoldableContainer { Title = "Left:" };
leftFoldable.FoldingChanged += OnFoldingChanged;
vBox.AddChild(leftFoldable);
_leftFoldable = new FoldableContainer
{
Title = "Left:",
Folded = comparisonResolver?.LeftFolded ?? true,
};
_leftFoldable.FoldingChanged += OnFoldingChanged;
vBox.AddChild(_leftFoldable);
_leftContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
leftFoldable.AddChild(_leftContainer);
_leftFoldable.AddChild(_leftContainer);
_leftEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_leftResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Left);
@@ -117,12 +127,16 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
opRow.AddChild(_operationDropdown);
var rightFoldable = new FoldableContainer { Title = "Right:" };
rightFoldable.FoldingChanged += OnFoldingChanged;
vBox.AddChild(rightFoldable);
_rightFoldable = new FoldableContainer
{
Title = "Right:",
Folded = comparisonResolver?.RightFolded ?? true,
};
_rightFoldable.FoldingChanged += OnFoldingChanged;
vBox.AddChild(_rightFoldable);
_rightContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
rightFoldable.AddChild(_rightContainer);
_rightFoldable.AddChild(_rightContainer);
_rightEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_rightResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Right);
@@ -135,12 +149,18 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
x => _rightEditor = x);
_rightResolverDropdown.ItemSelected += OnRightResolverDropdownItemSelected;
UpdateFoldableTitles();
}
/// <inheritdoc/>
public override void SaveTo(StatescriptNodeProperty property)
{
var comparisonResolver = new ComparisonResolverResource { Operation = _operation };
var comparisonResolver = new ComparisonResolverResource
{
Operation = _operation,
LeftFolded = _leftFoldable?.Folded ?? false,
RightFolded = _rightFoldable?.Folded ?? false,
};
if (_leftEditor is not null)
{
@@ -167,10 +187,23 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
_leftEditor?.ClearCallbacks();
_rightEditor?.ClearCallbacks();
_operationDropdown = null;
_leftContainer = null;
_rightContainer = null;
_leftFoldable = null;
_rightFoldable = null;
_leftResolverDropdown = null;
_rightResolverDropdown = null;
_leftEditor = null;
_rightEditor = null;
_leftEditorContainer = null;
_rightEditorContainer = null;
}
private void OnFoldingChanged(bool isFolded)
{
UpdateFoldableTitles();
_onChanged?.Invoke();
RaiseLayoutSizeChanged();
}
@@ -208,31 +241,14 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
setEditor(null);
ShowNestedEditor(selectedIndex, null, editorContainer, setEditor);
UpdateFoldableTitles();
_onChanged?.Invoke();
RaiseLayoutSizeChanged();
}
private int GetSelectedIndex(StatescriptResolverResource? existingResolver)
{
var selectedIndex = 0;
if (existingResolver is not null)
{
var existingTypeId = existingResolver.ResolverTypeId;
for (var i = 0; i < _numericFactories.Count; i++)
{
using NodeEditorProperty temp = _numericFactories[i]();
if (temp.ResolverTypeId == existingTypeId)
{
selectedIndex = i;
break;
}
}
}
return selectedIndex;
return ResolverEditorFactoryCatalog.GetDefaultFactoryIndex(_numericFactories, existingResolver, "Variant");
}
private OptionButton CreateResolverDropdownControl(StatescriptResolverResource? existingResolver)
@@ -241,8 +257,7 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
foreach (Func<NodeEditorProperty> factory in _numericFactories)
{
using NodeEditorProperty temp = factory();
dropdown.AddItem(temp.DisplayName);
dropdown.AddItem(StatescriptResolverRegistry.GetDisplayName(factory));
}
dropdown.Selected = GetSelectedIndex(existingResolver);
@@ -261,26 +276,41 @@ internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
return;
}
NodeEditorProperty editor = _numericFactories[factoryIndex]();
NodeEditorProperty? editor = NestedResolverEditorUtilities.CreateNestedEditor(
_graph,
_numericFactories,
factoryIndex,
existingResolver,
_numericExpectedTypes,
OnNestedEditorChanged,
RaiseLayoutSizeChanged);
StatescriptNodeProperty? tempProperty = null;
if (existingResolver is not null)
if (editor is null)
{
tempProperty = new StatescriptNodeProperty { Resolver = existingResolver };
return;
}
editor.Setup(_graph, tempProperty, typeof(ForgeVariant128), OnNestedEditorChanged, false);
editor.LayoutSizeChanged += RaiseLayoutSizeChanged;
container.AddChild(editor);
setEditor(editor);
}
private void OnNestedEditorChanged()
{
UpdateFoldableTitles();
_onChanged?.Invoke();
}
private void UpdateFoldableTitles()
{
if (_leftFoldable is not null)
{
InlineConstantSummaryFormatter.ApplyFoldableTitle("Left:", _leftFoldable, _leftEditor);
}
if (_rightFoldable is not null)
{
InlineConstantSummaryFormatter.ApplyFoldableTitle("Right:", _rightFoldable, _rightEditor);
}
}
}
#endif

View File

@@ -0,0 +1,29 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysQuaternion = System.Numerics.Quaternion;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ConcatenateResolverEditor : BinaryNestedResolverEditorBase<ConcatenateResolverResource>
{
public override string DisplayName => "Concatenate";
public override string ResolverTypeId => "Concatenate";
protected override Type[] FactoryExpectedTypes => [typeof(SysQuaternion)];
protected override Type NestedExpectedType => typeof(SysQuaternion);
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(SysQuaternion) || expectedType == typeof(ForgeVariant128);
}
}
#endif

View File

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

View File

@@ -0,0 +1,29 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysQuaternion = System.Numerics.Quaternion;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class ConjugateResolverEditor : UnaryNestedResolverEditorBase<ConjugateResolverResource>
{
public override string DisplayName => "Conjugate";
public override string ResolverTypeId => "Conjugate";
protected override Type[] FactoryExpectedTypes => [typeof(SysQuaternion)];
protected override Type NestedExpectedType => typeof(SysQuaternion);
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(SysQuaternion) || expectedType == typeof(ForgeVariant128);
}
}
#endif

View File

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

View File

@@ -0,0 +1,21 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class CopySignResolverEditor : ScalarBinaryResolverEditorBase<CopySignResolverResource>
{
public override string DisplayName => "Copy Sign";
public override string ResolverTypeId => "CopySign";
protected override string LeftTitle => "Magnitude:";
protected override string RightTitle => "Sign:";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class CosHResolverEditor : ScalarUnaryResolverEditorBase<CosHResolverResource>
{
public override string DisplayName => "CosH";
public override string ResolverTypeId => "CosH";
}
#endif

View File

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

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class CosResolverEditor : ScalarUnaryResolverEditorBase<CosResolverResource>
{
public override string DisplayName => "Cos";
public override string ResolverTypeId => "Cos";
}
#endif

View File

@@ -0,0 +1 @@
uid://3iou2ih6sonq

View File

@@ -0,0 +1,25 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector3 = System.Numerics.Vector3;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class CrossResolverEditor : VectorBinaryResolverEditorBase<CrossResolverResource>
{
public override string DisplayName => "Cross";
public override string ResolverTypeId => "Cross";
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(SysVector3) || expectedType == typeof(ForgeVariant128);
}
}
#endif

View File

@@ -0,0 +1 @@
uid://00ltb2mbsruh

View File

@@ -0,0 +1,17 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class DegToRadResolverEditor : NumericOrVectorUnaryResolverEditorBase<DegToRadResolverResource>
{
public override string DisplayName => "Deg To Rad";
public override string ResolverTypeId => "DegToRad";
}
#endif

View File

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

View File

@@ -0,0 +1,33 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class DistanceResolverEditor
: VectorOrQuaternionBinaryFloatResolverEditorBase<DistanceResolverResource>
{
public override string DisplayName => "Distance";
public override string ResolverTypeId => "Distance";
protected override Type[] GetFactoryExpectedTypes(Type expectedType)
{
if (expectedType == typeof(ForgeVariant128) || ResolverEditorCompatibility.IsFloatType(expectedType))
{
return [typeof(SysVector2), typeof(SysVector3), typeof(SysVector4), typeof(System.Numerics.Quaternion)];
}
return [expectedType];
}
}
#endif

View File

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

View File

@@ -0,0 +1,33 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class DistanceSquaredResolverEditor
: VectorOrQuaternionBinaryFloatResolverEditorBase<DistanceSquaredResolverResource>
{
public override string DisplayName => "Distance Squared";
public override string ResolverTypeId => "DistanceSquared";
protected override Type[] GetFactoryExpectedTypes(Type expectedType)
{
if (expectedType == typeof(ForgeVariant128) || ResolverEditorCompatibility.IsFloatType(expectedType))
{
return [typeof(SysVector2), typeof(SysVector3), typeof(SysVector4), typeof(System.Numerics.Quaternion)];
}
return [expectedType];
}
}
#endif

View File

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

View File

@@ -0,0 +1,43 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysVector2 = System.Numerics.Vector2;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class DivideResolverEditor
: NumericVectorOrQuaternionBinaryResolverEditorBase<DivideResolverResource>
{
public override string DisplayName => "Divide";
public override string ResolverTypeId => "Divide";
protected override Type[] GetFactoryExpectedTypes(Type expectedType)
{
return expectedType == typeof(ForgeVariant128)
? [
typeof(int),
typeof(float),
typeof(double),
typeof(SysVector2),
typeof(SysVector3),
typeof(SysVector4),
typeof(System.Numerics.Quaternion)
]
: [expectedType];
}
protected override Type GetNestedExpectedType(Type expectedType)
{
return expectedType;
}
}
#endif

View File

@@ -0,0 +1 @@
uid://2gj0lejcirb6

View File

@@ -0,0 +1,39 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysPlane = System.Numerics.Plane;
using SysVector3 = System.Numerics.Vector3;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class DotCoordinateResolverEditor
: AsymmetricBinaryNestedResolverEditorBase<DotCoordinateResolverResource>
{
public override string DisplayName => "Dot Coordinate";
public override string ResolverTypeId => "DotCoordinate";
protected override Type[] LeftFactoryExpectedTypes => [typeof(SysPlane)];
protected override Type[] RightFactoryExpectedTypes => [typeof(SysVector3)];
protected override Type LeftNestedExpectedType => typeof(SysPlane);
protected override Type RightNestedExpectedType => typeof(SysVector3);
protected override string LeftTitle => "Plane:";
protected override string RightTitle => "Coordinate:";
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(float) || expectedType == typeof(ForgeVariant128);
}
}
#endif

View File

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

View File

@@ -0,0 +1,39 @@
// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers.Bases;
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
using Godot;
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
using SysPlane = System.Numerics.Plane;
using SysVector3 = System.Numerics.Vector3;
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
[Tool]
internal sealed partial class DotNormalResolverEditor
: AsymmetricBinaryNestedResolverEditorBase<DotNormalResolverResource>
{
public override string DisplayName => "Dot Normal";
public override string ResolverTypeId => "DotNormal";
protected override Type[] LeftFactoryExpectedTypes => [typeof(SysPlane)];
protected override Type[] RightFactoryExpectedTypes => [typeof(SysVector3)];
protected override Type LeftNestedExpectedType => typeof(SysPlane);
protected override Type RightNestedExpectedType => typeof(SysVector3);
protected override string LeftTitle => "Plane:";
protected override string RightTitle => "Normal:";
public override bool IsCompatibleWith(Type expectedType)
{
return expectedType == typeof(float) || expectedType == typeof(ForgeVariant128);
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More