Skip to content

Instantly share code, notes, and snippets.

@derkork
Created June 22, 2022 13:00
Show Gist options
  • Save derkork/e21ca4eaf8b6e7bf9249fc09d2eaa635 to your computer and use it in GitHub Desktop.
Save derkork/e21ca4eaf8b6e7bf9249fc09d2eaa635 to your computer and use it in GitHub Desktop.
using JetBrains.Annotations;
namespace YoHDot.Addons.CustomScripts
{
using System;
using System.Runtime.CompilerServices;
/// <summary>
/// Marks the script as used (because Rider cannot see usages in TSCN files).
/// If <see cref="Icon"/> is given or <see cref="RegisterAsNode"/> is set to true,
/// will also register the script as a custom node in the editor.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[MeansImplicitUse]
public sealed class CustomScriptAttribute : Attribute
{
public CustomScriptAttribute(string icon = null, bool registerAsNode = false, [CallerFilePath] string path = "")
{
Icon = icon;
RegisterAsNode = registerAsNode || icon != null;
Path = path;
}
public string Icon { get; }
public bool RegisterAsNode { get; }
public string Path { get; }
}
}
#if TOOLS
using System;
using System.Linq;
using System.Reflection;
using Godot;
using Godot.Collections;
using YoHDot.Utilities;
namespace YoHDot.Addons.CustomScripts
{
[Tool]
public class CustomScriptsPlugin : EditorPlugin, ISerializationListener
{
// this is a godot array to survive reloads and not get any errors about unmarshallable types
private Array<string> _loadedNodes;
public override void _EnterTree()
{
RegisterCustomNodes();
}
public override void _ExitTree()
{
UnloadCustomNodes();
}
private void UnloadCustomNodes()
{
if (_loadedNodes == null)
{
return;
}
foreach (var type in _loadedNodes)
{
RemoveCustomType(type);
}
GD.Print($"Unloaded {_loadedNodes.Count} custom nodes.");
_loadedNodes = null;
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
// Refresh custom nodes
UnloadCustomNodes();
RegisterCustomNodes();
}
private void RegisterCustomNodes()
{
try
{
var customNodes = Assembly.GetAssembly(typeof(CustomScriptsPlugin)).GetExportedTypes()
.Select(it => (it, it.GetCustomAttribute<CustomScriptAttribute>()))
.Where(it => it.Item2 is {RegisterAsNode: true}).ToList();
_loadedNodes = new Array<string>();
foreach (var (type, attribute) in customNodes)
{
GD.Print("Loading " + type.Name);
if (string.IsNullOrEmpty(attribute.Path))
{
GD.PushError(
$"In class '{type.Name}': script path is missing" +
" in CustomNode attribute. I will not register this custom node.");
}
var script = CustomScript.GetScript(type);
if (!(script is Script theScript))
{
GD.PushError(
$"In class '{type.Name}': given path '{attribute.Path}' is is not " +
"a valid script. I will not register this custom node.");
continue;
}
Resource theIcon = null;
if (!string.IsNullOrEmpty(attribute.Icon))
{
theIcon = GD.Load(attribute.Icon);
if (!(theIcon is Texture))
{
GD.PushError(
$"In class '{type.Name}': given icon '{attribute.Icon}' is is not " +
"a valid texture. I will register this custom node without an icon.");
}
}
var baseType = type.BaseType;
Debug.Assert(baseType != null, "type.BaseType != null");
// workaround for https://github.com/godotengine/godot/issues/41211
// go up the hierarchy until you find something built-in into godot.
while ( baseType != null && (!baseType.Namespace?.StartsWith("Godot") ?? false))
{
baseType = baseType.BaseType;
}
// worst case, go with "Node"
var baseTypeName = baseType?.Name ?? "Node";
AddCustomType(type.Name, baseTypeName, theScript, (Texture) theIcon);
_loadedNodes.Add(type.Name);
}
GD.Print($"Loaded {_loadedNodes.Count} custom nodes.");
}
catch (Exception e)
{
GD.PushError("Problem when loading custom nodes.");
GD.PushError(e.Message);
}
}
}
}
#endif
[plugin]
name="CustomScripts"
description="Custom Scripts for C#"
author=""
version=""
script="CustomScriptsPlugin.cs"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment