Last active
September 1, 2022 13:36
-
-
Save derkork/faef2ddaec0ac089768b6be59619f019 to your computer and use it in GitHub Desktop.
Godot C# Registering custom node types with an editor plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [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