added forge addon
This commit is contained in:
210
addons/forge/editor/AssetRepairTool.cs
Normal file
210
addons/forge/editor/AssetRepairTool.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor;
|
||||
|
||||
[Tool]
|
||||
public partial class AssetRepairTool : EditorPlugin
|
||||
{
|
||||
public static void RepairAllAssetsTags()
|
||||
{
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
var tagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
|
||||
List<string> scenes = GetScenePaths("res://");
|
||||
GD.Print($"Found {scenes.Count} scene(s) to process.");
|
||||
|
||||
var openedScenes = EditorInterface.Singleton.GetOpenScenes();
|
||||
|
||||
foreach (var 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://");
|
||||
|
||||
GD.Print($"Processing scene: {scenePath}.");
|
||||
PackedScene? packedScene = ResourceLoader.Load<PackedScene>(scenePath);
|
||||
|
||||
if (packedScene is null)
|
||||
{
|
||||
GD.PrintErr($"Failed to load scene: {scenePath}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Node sceneInstance = packedScene.Instantiate();
|
||||
var modified = ProcessNode(sceneInstance, tagsManager);
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
GD.Print($"No changes needed for {scenePath}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 'sceneInstance' is the modified scene instance in memory, need to save to disk and reload if needed.
|
||||
var newScene = new PackedScene();
|
||||
Error error = newScene.Pack(sceneInstance);
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
GD.PrintErr($"Failed to pack scene: {error}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
error = ResourceSaver.Save(newScene, scenePath);
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
GD.PrintErr($"Failed to save scene: {error}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (openedScenes.Contains(scenePath))
|
||||
{
|
||||
GD.Print($"Scene was opened, reloading background scene: {scenePath}.");
|
||||
EditorInterface.Singleton.ReloadSceneFromPath(scenePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively get scene files from a folder.
|
||||
/// </summary>
|
||||
/// <param name="basePath">Current path iteration.</param>
|
||||
/// <returns>List of scenes found.</returns>
|
||||
private static List<string> GetScenePaths(string basePath)
|
||||
{
|
||||
var scenePaths = new List<string>();
|
||||
var dir = DirAccess.Open(basePath);
|
||||
|
||||
if (dir is null)
|
||||
{
|
||||
GD.PrintErr($"Failed to open directory: {basePath}");
|
||||
return scenePaths;
|
||||
}
|
||||
|
||||
// Start listing directory entries; skip navigational and hidden files.
|
||||
dir.ListDirBegin();
|
||||
while (true)
|
||||
{
|
||||
var fileName = dir.GetNext();
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var filePath = $"{basePath}/{fileName}";
|
||||
if (dir.CurrentIsDir())
|
||||
{
|
||||
// Recursively scan subdirectories.
|
||||
scenePaths.AddRange(GetScenePaths(filePath));
|
||||
}
|
||||
else if (fileName.EndsWith(".tscn", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fileName.EndsWith(".scn", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
scenePaths.Add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
dir.ListDirEnd();
|
||||
return scenePaths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively process nodes; returns true if any ForgeEntity was modified.
|
||||
/// </summary>
|
||||
/// <param name="node">Current node iteration.</param>
|
||||
/// <param name="tagsManager">The tags manager used to validate tags.</param>
|
||||
/// <returns><see langword="true"/> if any ForgeEntity was modified.</returns>
|
||||
private static bool ProcessNode(Node node, TagsManager tagsManager)
|
||||
{
|
||||
var modified = ValidateNode(node, tagsManager);
|
||||
|
||||
foreach (Node child in node.GetChildren())
|
||||
{
|
||||
modified |= ProcessNode(child, tagsManager);
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
private static bool ValidateNode(Node node, TagsManager tagsManager)
|
||||
{
|
||||
var modified = false;
|
||||
foreach (Dictionary propertyInfo in node.GetPropertyList())
|
||||
{
|
||||
if (!propertyInfo.TryGetValue("class_name", out Variant className))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (className.AsString() != "TagContainer")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!propertyInfo.TryGetValue("name", out Variant nameObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyName = nameObj.AsString();
|
||||
Variant value = node.Get(propertyName);
|
||||
|
||||
if (value.VariantType != Variant.Type.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.As<Resource>() is ForgeTagContainer tagContainer)
|
||||
{
|
||||
modified |= ValidateTagContainerProperty(tagContainer, node.Name, tagsManager);
|
||||
}
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
private static bool ValidateTagContainerProperty(
|
||||
ForgeTagContainer container,
|
||||
string nodeName,
|
||||
TagsManager tagsManager)
|
||||
{
|
||||
if (container.ContainerTags is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Array<string> originalTags = container.ContainerTags;
|
||||
var newTags = new Array<string>();
|
||||
var modified = false;
|
||||
|
||||
foreach (var tag in originalTags)
|
||||
{
|
||||
try
|
||||
{
|
||||
Tag.RequestTag(tagsManager, tag);
|
||||
newTags.Add(tag);
|
||||
}
|
||||
catch (TagNotRegisteredException)
|
||||
{
|
||||
GD.PrintRich(
|
||||
$"[color=LIGHT_STEEL_BLUE][RepairTool] Removing invalid tag [{tag}] from node {nodeName}.");
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified)
|
||||
{
|
||||
container.ContainerTags = newTags;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/AssetRepairTool.cs.uid
Normal file
1
addons/forge/editor/AssetRepairTool.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://1runivyr5don
|
||||
62
addons/forge/editor/EditorUtils.cs
Normal file
62
addons/forge/editor/EditorUtils.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Attributes;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor;
|
||||
|
||||
internal static class EditorUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses reflection to gather all classes inheriting from AttributeSet and their fields of type Attribute.
|
||||
/// </summary>
|
||||
/// <returns>An array with the available attributes.</returns>
|
||||
public static string[] GetAttributeSetOptions()
|
||||
{
|
||||
var options = new List<string>();
|
||||
|
||||
// Get all types in the current assembly
|
||||
Type[] allTypes = Assembly.GetExecutingAssembly().GetTypes();
|
||||
|
||||
// Find all types that subclass AttributeSet
|
||||
foreach (Type attributeSetType in allTypes.Where(x => x.IsSubclassOf(typeof(AttributeSet))))
|
||||
{
|
||||
options.Add(attributeSetType.Name);
|
||||
}
|
||||
|
||||
return [.. options];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses reflection to gather all classes inheriting from AttributeSet and their fields of type Attribute.
|
||||
/// </summary>
|
||||
/// <param name="attributeSet">The attribute set used to search for the attributes.</param>
|
||||
/// <returns>An array with the available attributes.</returns>
|
||||
public static string[] GetAttributeOptions(string? attributeSet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(attributeSet))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
Type? type = Array.Find(
|
||||
asm.GetTypes(),
|
||||
x => x.IsSubclassOf(typeof(AttributeSet)) && x.Name == attributeSet);
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
IEnumerable<PropertyInfo> properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
return [.. properties.Select(x => $"{x.Name}")];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/EditorUtils.cs.uid
Normal file
1
addons/forge/editor/EditorUtils.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dcvmf0r1f43m6
|
||||
14
addons/forge/editor/ForgeEditor.tscn
Normal file
14
addons/forge/editor/ForgeEditor.tscn
Normal file
@@ -0,0 +1,14 @@
|
||||
[gd_scene format=3 uid="uid://pjscvogl6jak"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c17f812by5x23" path="res://addons/forge/editor/tags/TagsEditor.tscn" id="1_bxwfw"]
|
||||
|
||||
[node name="Forge" type="PanelContainer" unique_id=249446352]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Tags" parent="." unique_id=654228508 instance=ExtResource("1_bxwfw")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
34
addons/forge/editor/attributes/AttributeEditorPlugin.cs
Normal file
34
addons/forge/editor/attributes/AttributeEditorPlugin.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeEditorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is Resources.ForgeModifier || @object is Resources.ForgeCue;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name == "Attribute" || name == "CapturedAttribute" || name == "MagnitudeAttribute")
|
||||
{
|
||||
AddPropertyEditor(name, new AttributeEditorProperty());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bl2w0vp6b5p8k
|
||||
101
addons/forge/editor/attributes/AttributeEditorProperty.cs
Normal file
101
addons/forge/editor/attributes/AttributeEditorProperty.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeEditorProperty : EditorProperty
|
||||
{
|
||||
private const int ButtonSize = 26;
|
||||
private const int PopupSize = 300;
|
||||
|
||||
private Label _label = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Texture2D dropdownIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiDropdown", "EditorIcons");
|
||||
|
||||
var hbox = new HBoxContainer();
|
||||
_label = new Label { Text = "None", SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
var button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) };
|
||||
|
||||
hbox.AddChild(_label);
|
||||
hbox.AddChild(button);
|
||||
AddChild(hbox);
|
||||
|
||||
var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) };
|
||||
var tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
AnchorRight = 1,
|
||||
AnchorBottom = 1,
|
||||
};
|
||||
popup.AddChild(tree);
|
||||
|
||||
var bg = new StyleBoxFlat
|
||||
{
|
||||
BgColor = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetColor("dark_color_2", "Editor"),
|
||||
};
|
||||
tree.AddThemeStyleboxOverride("panel", bg);
|
||||
|
||||
AddChild(popup);
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
var value = GetEditedObject().Get(GetEditedProperty()).AsString();
|
||||
_label.Text = string.IsNullOrEmpty(value) ? "None" : value;
|
||||
}
|
||||
|
||||
private static void BuildAttributeTree(Tree tree)
|
||||
{
|
||||
TreeItem root = tree.CreateItem();
|
||||
|
||||
foreach (var attributeSet in EditorUtils.GetAttributeSetOptions())
|
||||
{
|
||||
TreeItem setItem = tree.CreateItem(root);
|
||||
setItem.SetText(0, attributeSet);
|
||||
setItem.Collapsed = true;
|
||||
|
||||
foreach (var attribute in EditorUtils.GetAttributeOptions(attributeSet))
|
||||
{
|
||||
TreeItem attributeItem = tree.CreateItem(setItem);
|
||||
var attributePath = $"{attributeSet}.{attribute}";
|
||||
attributeItem.SetText(0, attribute);
|
||||
attributeItem.SetMeta("attribute_path", attributePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvjqj637kfav
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Attributes;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetClassEditorProperty : EditorProperty
|
||||
{
|
||||
private OptionButton _optionButton = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_optionButton = new OptionButton();
|
||||
AddChild(_optionButton);
|
||||
|
||||
_optionButton.AddItem("Select AttributeSet Class");
|
||||
foreach (var option in EditorUtils.GetAttributeSetOptions())
|
||||
{
|
||||
_optionButton.AddItem(option);
|
||||
}
|
||||
|
||||
_optionButton.ItemSelected += x =>
|
||||
{
|
||||
var className = _optionButton.GetItemText((int)x);
|
||||
EmitChanged(GetEditedProperty(), className);
|
||||
|
||||
GodotObject obj = GetEditedObject();
|
||||
if (obj is not null)
|
||||
{
|
||||
var dict = new Dictionary<string, AttributeValues>();
|
||||
|
||||
var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet));
|
||||
Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className);
|
||||
if (targetType is not null)
|
||||
{
|
||||
System.Collections.Generic.IEnumerable<PropertyInfo> attrProps = targetType
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
foreach (PropertyInfo? pi in attrProps)
|
||||
{
|
||||
dict[pi.Name] = new AttributeValues(0, 0, int.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
EmitChanged("InitialAttributeValues", dict);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bumuxlivyt66b
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetInspectorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
private PackedScene? _inspectorScene;
|
||||
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is ForgeAttributeSet;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name == "AttributeSetClass")
|
||||
{
|
||||
AddPropertyEditor(name, new AttributeSetClassEditorProperty());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == "InitialAttributeValues")
|
||||
{
|
||||
AddPropertyEditor(name, new AttributeSetValuesEditorProperty());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://t3gpjlcyqor
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Attributes;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetValuesEditorProperty : EditorProperty
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var attributesRoot = new VBoxContainer { Name = "AttributesRoot" };
|
||||
AddChild(attributesRoot);
|
||||
SetBottomEditor(attributesRoot);
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
VBoxContainer attributesRoot = GetNodeOrNull<VBoxContainer>("AttributesRoot");
|
||||
|
||||
if (attributesRoot is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FreeAllChildren(attributesRoot);
|
||||
|
||||
if (GetEditedObject() is not ForgeAttributeSet obj
|
||||
|| string.IsNullOrEmpty(obj.AttributeSetClass)
|
||||
|| obj.InitialAttributeValues is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var className = obj.AttributeSetClass;
|
||||
var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet));
|
||||
Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className);
|
||||
|
||||
if (targetType is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Collections.Generic.IEnumerable<PropertyInfo> attributeProperties = targetType
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
foreach (var attributeName in attributeProperties.Select(x => x.Name))
|
||||
{
|
||||
var groupVBox = new VBoxContainer();
|
||||
|
||||
groupVBox.AddChild(AttributeHeader(attributeName));
|
||||
|
||||
AttributeValues value = obj.InitialAttributeValues.TryGetValue(attributeName, out AttributeValues? v)
|
||||
? v
|
||||
: new AttributeValues(0, 0, int.MaxValue);
|
||||
|
||||
SpinBox spinDefault = CreateSpinBox(value.Min, value.Max, value.Default);
|
||||
SpinBox spinMin = CreateSpinBox(int.MinValue, value.Max, value.Min);
|
||||
SpinBox spinMax = CreateSpinBox(value.Min, int.MaxValue, value.Max);
|
||||
|
||||
groupVBox.AddChild(AttributeFieldRow("Default", spinDefault));
|
||||
groupVBox.AddChild(AttributeFieldRow("Min", spinMin));
|
||||
groupVBox.AddChild(AttributeFieldRow("Max", spinMax));
|
||||
|
||||
spinDefault.ValueChanged += x =>
|
||||
{
|
||||
UpdateAndEmit(obj, attributeName, (int)x, (int)spinMin.Value, (int)spinMax.Value);
|
||||
};
|
||||
|
||||
spinMin.ValueChanged += x =>
|
||||
{
|
||||
spinDefault.MinValue = x;
|
||||
spinMax.MinValue = x;
|
||||
UpdateAndEmit(obj, attributeName, (int)spinDefault.Value, (int)x, (int)spinMax.Value);
|
||||
};
|
||||
|
||||
spinMax.ValueChanged += x =>
|
||||
{
|
||||
spinDefault.MaxValue = x;
|
||||
spinMin.MaxValue = x;
|
||||
UpdateAndEmit(obj, attributeName, (int)spinDefault.Value, (int)spinMin.Value, (int)x);
|
||||
};
|
||||
|
||||
attributesRoot.AddChild(groupVBox);
|
||||
}
|
||||
}
|
||||
|
||||
private static PanelContainer AttributeHeader(string text)
|
||||
{
|
||||
var headerPanel = new PanelContainer
|
||||
{
|
||||
CustomMinimumSize = new Vector2(0, 28),
|
||||
};
|
||||
|
||||
var style = new StyleBoxFlat
|
||||
{
|
||||
BgColor = new Color(0.16f, 0.17f, 0.20f),
|
||||
};
|
||||
|
||||
headerPanel.AddThemeStyleboxOverride("panel", style);
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = text,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
SizeFlagsHorizontal = (SizeFlags)(int)SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(0, 22),
|
||||
AutowrapMode = TextServer.AutowrapMode.Off,
|
||||
};
|
||||
|
||||
headerPanel.AddChild(label);
|
||||
return headerPanel;
|
||||
}
|
||||
|
||||
private static HBoxContainer AttributeFieldRow(string label, SpinBox spinBox)
|
||||
{
|
||||
var hbox = new HBoxContainer();
|
||||
|
||||
hbox.AddChild(new Label
|
||||
{
|
||||
Text = label,
|
||||
CustomMinimumSize = new Vector2(80, 0),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
hbox.AddChild(spinBox);
|
||||
return hbox;
|
||||
}
|
||||
|
||||
private static SpinBox CreateSpinBox(int min, int max, int value)
|
||||
{
|
||||
return new SpinBox
|
||||
{
|
||||
MinValue = min,
|
||||
MaxValue = max,
|
||||
Value = value,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
}
|
||||
|
||||
private static void FreeAllChildren(Node node)
|
||||
{
|
||||
for (var i = node.GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
node.GetChild(i).QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAndEmit(ForgeAttributeSet obj, string name, int def, int min, int max)
|
||||
{
|
||||
Debug.Assert(obj.InitialAttributeValues is not null, "InitialAttributeValues should not be null here.");
|
||||
|
||||
var dict = new Dictionary<string, AttributeValues>(obj.InitialAttributeValues)
|
||||
{
|
||||
[name] = new AttributeValues(def, min, max),
|
||||
};
|
||||
|
||||
EmitChanged(nameof(ForgeAttributeSet.InitialAttributeValues), dict);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdj20gbpxkda1
|
||||
29
addons/forge/editor/attributes/AttributeValues.cs
Normal file
29
addons/forge/editor/attributes/AttributeValues.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeValues : Resource
|
||||
{
|
||||
[Export]
|
||||
public int Default { get; set; }
|
||||
|
||||
[Export]
|
||||
public int Min { get; set; }
|
||||
|
||||
[Export]
|
||||
public int Max { get; set; }
|
||||
|
||||
public AttributeValues()
|
||||
{
|
||||
}
|
||||
|
||||
public AttributeValues(int @default, int min, int max)
|
||||
{
|
||||
Default = @default;
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
}
|
||||
1
addons/forge/editor/attributes/AttributeValues.cs.uid
Normal file
1
addons/forge/editor/attributes/AttributeValues.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccovd5i0wr3kk
|
||||
53
addons/forge/editor/cues/CueHandlerInspectorPlugin.cs
Normal file
53
addons/forge/editor/cues/CueHandlerInspectorPlugin.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Cues;
|
||||
|
||||
[Tool]
|
||||
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.
|
||||
if (@object?.GetScript().As<CSharpScript>() is CSharpScript script)
|
||||
{
|
||||
StringName className = script.GetGlobalName();
|
||||
|
||||
Type baseType = typeof(ForgeCueHandler);
|
||||
System.Reflection.Assembly assembly = baseType.Assembly;
|
||||
|
||||
Type? implementationType =
|
||||
Array.Find(assembly.GetTypes(), x =>
|
||||
x.Name == className &&
|
||||
baseType.IsAssignableFrom(x));
|
||||
|
||||
return implementationType is not null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name == "CueTag")
|
||||
{
|
||||
var cueKeyEditorProperty = new CueKeyEditorProperty();
|
||||
AddPropertyEditor(name, cueKeyEditorProperty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base._ParseProperty(@object, type, name, hintType, hintString, usageFlags, wide);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dattkelp87mhv
|
||||
106
addons/forge/editor/cues/CueKeyEditorProperty.cs
Normal file
106
addons/forge/editor/cues/CueKeyEditorProperty.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Cues;
|
||||
|
||||
[Tool]
|
||||
public partial class CueKeyEditorProperty : EditorProperty
|
||||
{
|
||||
private const int ButtonSize = 26;
|
||||
private const int PopupSize = 300;
|
||||
|
||||
private Label _label = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Texture2D dropdownIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiDropdown", "EditorIcons");
|
||||
|
||||
var hbox = new HBoxContainer();
|
||||
_label = new Label { Text = "None", SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
var button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) };
|
||||
|
||||
hbox.AddChild(_label);
|
||||
hbox.AddChild(button);
|
||||
AddChild(hbox);
|
||||
|
||||
var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) };
|
||||
var tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
AnchorRight = 1,
|
||||
AnchorBottom = 1,
|
||||
};
|
||||
popup.AddChild(tree);
|
||||
|
||||
var backgroundStyle = new StyleBoxFlat
|
||||
{
|
||||
BgColor = EditorInterface.Singleton.GetEditorTheme().GetColor("base_color", "Editor"),
|
||||
};
|
||||
tree.AddThemeStyleboxOverride("panel", backgroundStyle);
|
||||
|
||||
AddChild(popup);
|
||||
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
var tagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
var property = GetEditedObject().Get(GetEditedProperty()).AsString();
|
||||
_label.Text = string.IsNullOrEmpty(property) ? "None" : property;
|
||||
}
|
||||
|
||||
private static void BuildTreeRecursively(Tree tree, TreeItem currentTreeItem, TagNode currentNode)
|
||||
{
|
||||
foreach (TagNode childTagNode in currentNode.ChildTags)
|
||||
{
|
||||
TreeItem childTreeNode = tree.CreateItem(currentTreeItem);
|
||||
childTreeNode.SetText(0, childTagNode.TagKey);
|
||||
childTreeNode.Collapsed = true;
|
||||
BuildTreeRecursively(tree, childTreeNode, childTagNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/cues/CueKeyEditorProperty.cs.uid
Normal file
1
addons/forge/editor/cues/CueKeyEditorProperty.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://csmr2puffid4k
|
||||
1
addons/forge/editor/cues/CueKeysEditor.cs.uid
Normal file
1
addons/forge/editor/cues/CueKeysEditor.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dnsy7p8h1ujjv
|
||||
160
addons/forge/editor/tags/TagContainerEditorProperty.cs
Normal file
160
addons/forge/editor/tags/TagContainerEditorProperty.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
using GodotStringArray = Godot.Collections.Array<string>;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
[Tool]
|
||||
public partial class TagContainerEditorProperty : EditorProperty
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
private VBoxContainer _root = null!;
|
||||
private Button _containerButton = null!;
|
||||
private ScrollContainer _scroll = null!;
|
||||
private Tree _tree = null!;
|
||||
|
||||
private Texture2D _checkedIcon = null!;
|
||||
private Texture2D _uncheckedIcon = null!;
|
||||
|
||||
private GodotStringArray _currentValue = [];
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_root = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_containerButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
_containerButton.Toggled += OnToggled;
|
||||
|
||||
_scroll = new ScrollContainer
|
||||
{
|
||||
Visible = false,
|
||||
CustomMinimumSize = new Vector2(0, 220),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_scroll.AddChild(_tree);
|
||||
|
||||
_root.AddChild(_containerButton);
|
||||
_root.AddChild(_scroll);
|
||||
|
||||
AddChild(_root);
|
||||
SetBottomEditor(_root);
|
||||
|
||||
_checkedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiChecked", "EditorIcons");
|
||||
|
||||
_uncheckedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiUnchecked", "EditorIcons");
|
||||
|
||||
_tree.ButtonClicked += OnTreeButtonClicked;
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
GodotObject obj = GetEditedObject();
|
||||
string propertyName = GetEditedProperty();
|
||||
|
||||
_currentValue =
|
||||
obj.Get(propertyName).AsGodotArray<string>() ?? [];
|
||||
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
private void RebuildTree()
|
||||
{
|
||||
_tree.Clear();
|
||||
_treeItemToNode.Clear();
|
||||
|
||||
_containerButton.Text =
|
||||
$"Container (size: {_currentValue.Count})";
|
||||
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
ForgeData forgePluginData =
|
||||
ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
|
||||
var tagsManager =
|
||||
new TagsManager([.. forgePluginData.RegisteredTags]);
|
||||
|
||||
BuildTreeRecursive(root, tagsManager.RootNode);
|
||||
|
||||
UpdateMinimumSize();
|
||||
NotifyPropertyListChanged();
|
||||
}
|
||||
|
||||
private void BuildTreeRecursive(TreeItem parent, TagNode node)
|
||||
{
|
||||
foreach (TagNode child in node.ChildTags)
|
||||
{
|
||||
TreeItem item = _tree.CreateItem(parent);
|
||||
item.SetText(0, child.TagKey);
|
||||
|
||||
var checkedState =
|
||||
_currentValue.Contains(child.CompleteTagKey);
|
||||
|
||||
item.AddButton(
|
||||
0,
|
||||
checkedState ? _checkedIcon : _uncheckedIcon);
|
||||
|
||||
_treeItemToNode[item] = child;
|
||||
BuildTreeRecursive(item, child);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTreeButtonClicked(
|
||||
TreeItem item,
|
||||
long column,
|
||||
long id,
|
||||
long mouseButtonIndex)
|
||||
{
|
||||
if (mouseButtonIndex != 1 || id != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string tag = _treeItemToNode[item].CompleteTagKey;
|
||||
|
||||
var newValue = new GodotStringArray();
|
||||
newValue.AddRange(_currentValue);
|
||||
|
||||
if (!newValue.Remove(tag))
|
||||
{
|
||||
newValue.Add(tag);
|
||||
}
|
||||
|
||||
EmitChanged(GetEditedProperty(), newValue);
|
||||
}
|
||||
|
||||
private void OnToggled(bool toggled)
|
||||
{
|
||||
_scroll.Visible = toggled;
|
||||
|
||||
UpdateMinimumSize();
|
||||
NotifyPropertyListChanged();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dppi5lmv8q5ti
|
||||
35
addons/forge/editor/tags/TagContainerInspectorPlugin.cs
Normal file
35
addons/forge/editor/tags/TagContainerInspectorPlugin.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
public partial class TagContainerInspectorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is ForgeTagContainer;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name != "ContainerTags")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prop = new TagContainerEditorProperty();
|
||||
AddPropertyEditor(name, prop);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://8g56j8vs35mn
|
||||
149
addons/forge/editor/tags/TagEditorProperty.cs
Normal file
149
addons/forge/editor/tags/TagEditorProperty.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
[Tool]
|
||||
public partial class TagEditorProperty : EditorProperty
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
private VBoxContainer _root = null!;
|
||||
private Button _containerButton = null!;
|
||||
private ScrollContainer _scroll = null!;
|
||||
private Tree _tree = null!;
|
||||
|
||||
private Texture2D _checkedIcon = null!;
|
||||
private Texture2D _uncheckedIcon = null!;
|
||||
|
||||
private string _currentValue = string.Empty;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_root = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_containerButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
_containerButton.Toggled += OnToggled;
|
||||
|
||||
_scroll = new ScrollContainer
|
||||
{
|
||||
Visible = false,
|
||||
CustomMinimumSize = new Vector2(0, 220),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_scroll.AddChild(_tree);
|
||||
|
||||
_root.AddChild(_containerButton);
|
||||
_root.AddChild(_scroll);
|
||||
|
||||
AddChild(_root);
|
||||
SetBottomEditor(_root);
|
||||
|
||||
_checkedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiRadioChecked", "EditorIcons");
|
||||
|
||||
_uncheckedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiRadioUnchecked", "EditorIcons");
|
||||
|
||||
_tree.ButtonClicked += OnTreeButtonClicked;
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
GodotObject obj = GetEditedObject();
|
||||
string propertyName = GetEditedProperty();
|
||||
|
||||
_currentValue = obj.Get(propertyName).AsString();
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
private void RebuildTree()
|
||||
{
|
||||
_tree.Clear();
|
||||
_treeItemToNode.Clear();
|
||||
|
||||
_containerButton.Text =
|
||||
string.IsNullOrEmpty(_currentValue) ? "(none)" : _currentValue;
|
||||
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
ForgeData forgePluginData =
|
||||
ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
|
||||
var tagsManager =
|
||||
new TagsManager([.. forgePluginData.RegisteredTags]);
|
||||
|
||||
BuildTreeRecursive(root, tagsManager.RootNode);
|
||||
|
||||
UpdateMinimumSize();
|
||||
NotifyPropertyListChanged();
|
||||
}
|
||||
|
||||
private void BuildTreeRecursive(TreeItem parent, TagNode node)
|
||||
{
|
||||
foreach (TagNode child in node.ChildTags)
|
||||
{
|
||||
TreeItem item = _tree.CreateItem(parent);
|
||||
item.SetText(0, child.TagKey);
|
||||
|
||||
var selected = _currentValue == child.CompleteTagKey;
|
||||
item.AddButton(0, selected ? _checkedIcon : _uncheckedIcon);
|
||||
|
||||
_treeItemToNode[item] = child;
|
||||
BuildTreeRecursive(item, child);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTreeButtonClicked(
|
||||
TreeItem item,
|
||||
long column,
|
||||
long id,
|
||||
long mouseButtonIndex)
|
||||
{
|
||||
if (mouseButtonIndex != 1 || id != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string newValue = _treeItemToNode[item].CompleteTagKey;
|
||||
|
||||
if (newValue == _currentValue)
|
||||
{
|
||||
newValue = string.Empty;
|
||||
}
|
||||
|
||||
EmitChanged(GetEditedProperty(), newValue);
|
||||
}
|
||||
|
||||
private void OnToggled(bool toggled)
|
||||
{
|
||||
_scroll.Visible = toggled;
|
||||
|
||||
UpdateMinimumSize();
|
||||
NotifyPropertyListChanged();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/tags/TagEditorProperty.cs.uid
Normal file
1
addons/forge/editor/tags/TagEditorProperty.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bc4vhfbuyp7xd
|
||||
35
addons/forge/editor/tags/TagInspectorPlugin.cs
Normal file
35
addons/forge/editor/tags/TagInspectorPlugin.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
public partial class TagInspectorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is ForgeTag;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name != "Tag")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prop = new TagEditorProperty();
|
||||
AddPropertyEditor(name, prop);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/tags/TagInspectorPlugin.cs.uid
Normal file
1
addons/forge/editor/tags/TagInspectorPlugin.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cx3reriadfsnh
|
||||
192
addons/forge/editor/tags/TagsEditor.cs
Normal file
192
addons/forge/editor/tags/TagsEditor.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
[Tool]
|
||||
public partial class TagsEditor : VBoxContainer, ISerializationListener
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
private TagsManager _tagsManager = null!;
|
||||
|
||||
private ForgeData? _forgePluginData;
|
||||
|
||||
private Tree? _tree;
|
||||
private LineEdit? _tagNameTextField;
|
||||
private Button? _addTagButton;
|
||||
|
||||
private Texture2D? _addIcon;
|
||||
private Texture2D? _removeIcon;
|
||||
|
||||
public bool IsPluginInstance { get; set; }
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
if (!IsPluginInstance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_forgePluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
_tagsManager = new TagsManager([.. _forgePluginData.RegisteredTags]);
|
||||
|
||||
_addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
|
||||
_removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
|
||||
|
||||
_tree = GetNode<Tree>("%Tree");
|
||||
_tagNameTextField = GetNode<LineEdit>("%TagNameField");
|
||||
_addTagButton = GetNode<Button>("%AddTagButton");
|
||||
|
||||
ConstructTagTree();
|
||||
|
||||
_tree.ButtonClicked += TreeButtonClicked;
|
||||
_addTagButton.Pressed += AddTagButton_Pressed;
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
// This method was intentionally left empty.
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
_tagsManager = new TagsManager([.. _forgePluginData.RegisteredTags]);
|
||||
ReconstructTreeNode();
|
||||
}
|
||||
|
||||
private void AddTagButton_Pressed()
|
||||
{
|
||||
EnsureInitialized();
|
||||
Debug.Assert(
|
||||
_forgePluginData.RegisteredTags is not null,
|
||||
$"{_forgePluginData.RegisteredTags} should have been initialized by the Forge plugin.");
|
||||
|
||||
if (!Tag.IsValidKey(_tagNameTextField.Text, out var _, out var fixedTag))
|
||||
{
|
||||
_tagNameTextField.Text = fixedTag;
|
||||
}
|
||||
|
||||
if (_forgePluginData.RegisteredTags.Contains(_tagNameTextField.Text))
|
||||
{
|
||||
GD.PushWarning($"Tag [{_tagNameTextField.Text}] is already present in the manager.");
|
||||
return;
|
||||
}
|
||||
|
||||
_forgePluginData.RegisteredTags.Add(_tagNameTextField.Text);
|
||||
ResourceSaver.Save(_forgePluginData);
|
||||
|
||||
ReconstructTreeNode();
|
||||
}
|
||||
|
||||
private void ReconstructTreeNode()
|
||||
{
|
||||
EnsureInitialized();
|
||||
Debug.Assert(
|
||||
_forgePluginData.RegisteredTags is not null,
|
||||
$"{_forgePluginData.RegisteredTags} should have been initialized by the Forge plugin.");
|
||||
|
||||
_tagsManager.DestroyTagTree();
|
||||
_tagsManager = new TagsManager([.. _forgePluginData.RegisteredTags]);
|
||||
|
||||
_tree.Clear();
|
||||
ConstructTagTree();
|
||||
}
|
||||
|
||||
private void ConstructTagTree()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
TreeItem rootTreeNode = _tree.CreateItem();
|
||||
_tree.HideRoot = true;
|
||||
|
||||
if (_tagsManager.RootNode.ChildTags.Count == 0)
|
||||
{
|
||||
TreeItem childTreeNode = _tree.CreateItem(rootTreeNode);
|
||||
childTreeNode.SetText(0, "No tag has been registered yet.");
|
||||
childTreeNode.SetCustomColor(0, Color.FromHtml("EED202"));
|
||||
return;
|
||||
}
|
||||
|
||||
BuildTreeRecursively(_tree, rootTreeNode, _tagsManager.RootNode);
|
||||
}
|
||||
|
||||
private void BuildTreeRecursively(Tree tree, TreeItem currentTreeItem, TagNode currentNode)
|
||||
{
|
||||
foreach (TagNode childTagNode in currentNode.ChildTags)
|
||||
{
|
||||
TreeItem childTreeNode = tree.CreateItem(currentTreeItem);
|
||||
childTreeNode.SetText(0, childTagNode.TagKey);
|
||||
childTreeNode.AddButton(0, _addIcon);
|
||||
childTreeNode.AddButton(0, _removeIcon);
|
||||
|
||||
_treeItemToNode.Add(childTreeNode, childTagNode);
|
||||
|
||||
BuildTreeRecursively(tree, childTreeNode, childTagNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void TreeButtonClicked(TreeItem item, long column, long id, long mouseButtonIndex)
|
||||
{
|
||||
EnsureInitialized();
|
||||
Debug.Assert(
|
||||
_forgePluginData.RegisteredTags is not null,
|
||||
$"{_forgePluginData.RegisteredTags} should have been initialized by the Forge plugin.");
|
||||
|
||||
if (mouseButtonIndex == 1)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
_tagNameTextField.Text = $"{_treeItemToNode[item].CompleteTagKey}.";
|
||||
_tagNameTextField.GrabFocus();
|
||||
_tagNameTextField.CaretColumn = _tagNameTextField.Text.Length;
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
TagNode selectedTag = _treeItemToNode[item];
|
||||
|
||||
for (var i = _forgePluginData.RegisteredTags.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var tag = _forgePluginData.RegisteredTags[i];
|
||||
|
||||
if (string.Equals(tag, selectedTag.CompleteTagKey, StringComparison.OrdinalIgnoreCase) ||
|
||||
tag.StartsWith(selectedTag.CompleteTagKey + ".", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_forgePluginData.RegisteredTags.Remove(tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTag.ParentTagNode is not null
|
||||
&& !_forgePluginData.RegisteredTags.Contains(selectedTag.ParentTagNode.CompleteTagKey))
|
||||
{
|
||||
_forgePluginData.RegisteredTags.Add(selectedTag.ParentTagNode.CompleteTagKey);
|
||||
}
|
||||
|
||||
ResourceSaver.Save(_forgePluginData);
|
||||
ReconstructTreeNode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(_tree), nameof(_tagNameTextField), nameof(_forgePluginData))]
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
Debug.Assert(_tree is not null, $"{_tree} should have been initialized on _Ready().");
|
||||
Debug.Assert(_tagNameTextField is not null, $"{_tagNameTextField} should have been initialized on _Ready().");
|
||||
Debug.Assert(_forgePluginData is not null, $"{_forgePluginData} should have been initialized on _Ready().");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/tags/TagsEditor.cs.uid
Normal file
1
addons/forge/editor/tags/TagsEditor.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://do8jplrf64p5f
|
||||
33
addons/forge/editor/tags/TagsEditor.tscn
Normal file
33
addons/forge/editor/tags/TagsEditor.tscn
Normal file
@@ -0,0 +1,33 @@
|
||||
[gd_scene format=3 uid="uid://c17f812by5x23"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://do8jplrf64p5f" path="res://addons/forge/editor/tags/TagsEditor.cs" id="1_7jg4t"]
|
||||
|
||||
[node name="GameplayTagsEditor" type="VBoxContainer" unique_id=1724725192]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_7jg4t")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=613044022]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="HBoxContainer" unique_id=2121441014]
|
||||
layout_mode = 2
|
||||
text = "Tag Name:"
|
||||
|
||||
[node name="TagNameField" type="LineEdit" parent="HBoxContainer" unique_id=337174471]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="AddTagButton" type="Button" parent="HBoxContainer" unique_id=19730410]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Add Tag"
|
||||
|
||||
[node name="Tree" type="Tree" parent="." unique_id=1467435445]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
Reference in New Issue
Block a user