Skip to content

Instantly share code, notes, and snippets.

@derkork
Last active September 1, 2022 13:36
Show Gist options
  • Select an option

  • Save derkork/faef2ddaec0ac089768b6be59619f019 to your computer and use it in GitHub Desktop.

Select an option

Save derkork/faef2ddaec0ac089768b6be59619f019 to your computer and use it in GitHub Desktop.
Godot C# Registering custom node types with an editor plugin
using YoHDot.Utilities;
namespace YoHDot.Addons.CustomScripts
{
using System.Runtime.CompilerServices;
using System;
using Godot;
// This is mostly a workaround for https://github.com/godotengine/godot/issues/38191
// Basically - annotate a custom script with [CustomScript] and then instantiate it with
// CustomScript.New<MyScript>() instead of new MyScript().
// based on https://gist.github.com/cgbeutler/c4f00b98d744ac438b84e8840bbe1740
public static class CustomScript
{
private const string ThisFileProjPath = "Addons/CustomScripts/" + nameof(CustomScript) + ".cs";
private static string _projectPath;
private static string ProjectPath => _projectPath ??= __InitProjectPath();
private static string __InitProjectPath([CallerFilePath] string callerPath = "")
{
callerPath = callerPath.Replace('\\', '/');
if (!callerPath.EndsWith("/" + ThisFileProjPath))
{
GD.PushError(
$"Failed to get project path from '{callerPath}'. Project-path of this file may have changed.");
throw new Exception("Failed to get project path. Project-path of this file may have changed.");
}
return callerPath.Remove(callerPath.Length - ThisFileProjPath.Length);
}
private static string ResourcePath(Type type)
{
var sourceInfo =
(CustomScriptAttribute) Attribute.GetCustomAttribute(type, typeof(CustomScriptAttribute));
if (sourceInfo == null)
{
GD.PushError(
$"Could not file script info. Did you add '{nameof(CustomScriptAttribute)}' to the class '{type.Name}'?");
return "";
}
if (sourceInfo.Path.GetFile().BaseName() != type.Name)
{
GD.PushError(
$"Class and script name mismatch. Class name is '{type.Name}' for script '{sourceInfo.Path}'");
return "";
}
// convert into resource-relative path
return "res://" + sourceInfo.Path.Substring(ProjectPath.Length).Replace("\\", "/");
}
public static CSharpScript GetScript(Type type)
{
var scriptPath = ResourcePath(type);
Debug.Assert(!scriptPath.Empty(), "!scriptPath.Empty()");
return GD.Load<CSharpScript>(scriptPath);
}
/// Returns a new instance of the script.
/// workaround for https://github.com/godotengine/godot/issues/38191
public static T New<T>() where T : Godot.Object
{
return (T) GetScript(typeof(T))?.New();
}
}
}
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="Jan Thomä"
version=""
script="CustomScriptsPlugin.cs"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment