-
-
Save cgbeutler/c4f00b98d744ac438b84e8840bbe1740 to your computer and use it in GitHub Desktop.
| using System; | |
| using System.Runtime.CompilerServices; | |
| using Godot.Collections; | |
| using Array = Godot.Collections.Array; | |
| /* Example Usage: | |
| // Declare a class with the attribute | |
| [CSharpScript] | |
| public class CustomResource : Resource { ... } | |
| // Later, create new resources with | |
| CSharpScript<CustomResource>.New() | |
| // Report issues to the gist at: https://gist.github.com/cgbeutler/c4f00b98d744ac438b84e8840bbe1740 | |
| */ | |
| namespace Godot | |
| { | |
| [AttributeUsage( AttributeTargets.Class, Inherited = false, AllowMultiple = false )] | |
| public sealed class CSharpScriptAttribute : Attribute | |
| { | |
| public CSharpScriptAttribute( [CallerFilePath] string path = "" ) | |
| { | |
| FilePath = path; | |
| } | |
| public string FilePath { get; set; } | |
| }; | |
| public static class CSharpScript<T> | |
| where T : class | |
| { | |
| // TODO: Edit this string to be the project-path of THIS file (backlashes and no '.' prefixes) | |
| private const string __THIS_FILE_PROJ_PATH = "path/to/this/CSharpScript.cs"; | |
| private static string __projectPath = null; | |
| private static string __ProjectPath => __projectPath ??= __InitProjectPath(); | |
| private static string __InitProjectPath( [CallerFilePath] string callerPath = "" ) | |
| { | |
| callerPath = callerPath.Replace( System.IO.Path.DirectorySeparatorChar, '/' ); | |
| if (!callerPath.EndsWith( "/" + __THIS_FILE_PROJ_PATH )) | |
| { | |
| GD.PushError( "Failed to get project path. 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 - __THIS_FILE_PROJ_PATH.Length ); | |
| } | |
| public static string FilePath; | |
| public static string Filename; | |
| public static string ResourcePath => FilePath; | |
| static CSharpScript() | |
| { | |
| if (Attribute.GetCustomAttribute( typeof( T ), typeof( CSharpScriptAttribute ) ) is CSharpScriptAttribute attr) | |
| { | |
| var tmpFilePath = attr.FilePath.Replace( System.IO.Path.DirectorySeparatorChar, '/' ); | |
| if (!tmpFilePath.BeginsWith( __ProjectPath )) | |
| { | |
| GD.PushError( $"Can't get script 'res' path: Raw path didn't start with project path" ); | |
| FilePath = Filename = ""; | |
| return; | |
| } | |
| if (!tmpFilePath.EndsWith( ".cs" )) | |
| { | |
| GD.PushError( $"Can't get scritp 'res' path: Raw path didn't end with '.cs'" ); | |
| FilePath = Filename = ""; | |
| return; | |
| } | |
| FilePath = "res://" + tmpFilePath.Substring( __ProjectPath.Length ); | |
| Filename = FilePath.GetFile(); | |
| if (Filename.BaseName() != typeof( T ).Name) | |
| { | |
| GD.PushError( $"Class name '{ typeof( T ).Name }' doesn't match filename '{ Filename }'" ); | |
| } | |
| } | |
| else | |
| { | |
| FilePath = Filename = typeof(T).Name; | |
| GD.PushError( $"'{nameof( CSharpScriptAttribute )}' missing from the class '{typeof( T ).Name}'." ); | |
| } | |
| } | |
| private static WeakRef __csharpScript = null; //<CSharpScript> | |
| /// Get the CSharpScript Godot Resource | |
| public static CSharpScript GetCSharpScript() | |
| { | |
| if (__csharpScript?.GetRef() is CSharpScript scr) { return scr; } | |
| scr = GD.Load<CSharpScript>( ResourcePath ); | |
| if (scr != null) { __csharpScript = Godot.Object.WeakRef( scr ); } | |
| else { throw new Exception( "Can't load CSharp Script" ); } | |
| return scr; | |
| } | |
| /// Returns a new instance of the script. | |
| public static T New() | |
| { | |
| var script = GetCSharpScript(); | |
| try | |
| { | |
| var o = script.New(); | |
| var t = (T) o; | |
| return t; | |
| } | |
| catch (Exception e) | |
| { | |
| GD.PrintErr( "Exception in New(): " + e.ToString() ); | |
| GD.PrintStack(); | |
| } | |
| return null!; | |
| } | |
| /// Returns the default value of the specified property. | |
| public static object GetPropertyDefaultValue( string property ) | |
| { | |
| var script = GetCSharpScript(); | |
| return script.GetPropertyDefaultValue( property ); | |
| } | |
| /// Returns a dictionary containing constant names and their values. | |
| public static Dictionary GetScriptConstantMap() | |
| { | |
| var script = GetCSharpScript(); | |
| return script.GetScriptConstantMap(); | |
| } | |
| /// Returns the list of methods in this Godot.Script. | |
| public static Array GetScriptMethodList() | |
| { | |
| var script = GetCSharpScript(); | |
| return script.GetScriptMethodList(); | |
| } | |
| /// Returns the list of properties in this Godot.Script. | |
| public static Array GetScriptPropertyList() | |
| { | |
| var script = GetCSharpScript(); | |
| return script.GetScriptPropertyList(); | |
| } | |
| /// Returns the list of user signals defined in this Godot.Script. | |
| public static Array GetScriptSignalList() | |
| { | |
| var script = GetCSharpScript(); | |
| return script.GetScriptSignalList(); | |
| } | |
| /// Returns true if the script, or a base class, defines a signal with the given | |
| /// name. | |
| public static bool HasScriptSignal( string signalName ) | |
| { | |
| var script = GetCSharpScript(); | |
| return script?.HasScriptSignal( signalName ) ?? false; | |
| } | |
| /// Returns true if the script is a tool script. A tool script can run in the editor. | |
| public static bool IsTool() | |
| { | |
| var script = GetCSharpScript(); | |
| return script?.IsTool() ?? false; | |
| } | |
| }; | |
| public static class CSharpScriptExt | |
| { | |
| public static string ResourcePath( this Type t ) | |
| { | |
| var sourceInfo = (CSharpScriptAttribute) Attribute.GetCustomAttribute( t, typeof( CSharpScriptAttribute ) ); | |
| if (sourceInfo == null) | |
| { | |
| GD.PushError( $"Could not file script info. Did you add '{nameof( CSharpScriptAttribute )}' to the class '{t.Name}'?" ); | |
| return ""; | |
| } | |
| if (sourceInfo?.FilePath.GetFile().BaseName() != t.Name) | |
| { | |
| GD.PushError( $"Class and script name mismatch. Class name is '{ t.Name }' for script '{ sourceInfo?.FilePath }'" ); | |
| return ""; | |
| } | |
| return sourceInfo?.FilePath ?? ""; | |
| } | |
| public static CSharpScript AsCSharpScript( this Type t ) | |
| { | |
| var scriptPath = ResourcePath( t ); | |
| if (scriptPath.Empty()) { throw new Exception( "Can't load CSharp Script" ); } | |
| // Don't worry, it will usually be a cached load | |
| // Also, in tool mode it can get scrapped randomly, so we kinda need to use load each time | |
| return GD.Load<CSharpScript>( scriptPath ); | |
| } | |
| /// Returns a new instance of the script. | |
| public static object New( this Type t ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| // if ( Engine.EditorHint && ! script.IsTool() ) | |
| // { GD.PushWarning($"Script is not in tool mode: '{ typeof( T ).Name }'"); } | |
| return script.New(); | |
| } | |
| /// Returns the default value of the specified property. | |
| public static object GetPropertyDefaultValue( this Type t, string property ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script.GetPropertyDefaultValue( property ); | |
| } | |
| /// Returns a dictionary containing constant names and their values. | |
| public static Dictionary GetScriptConstantMap( this Type t ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script.GetScriptConstantMap(); | |
| } | |
| /// Returns the list of methods in this Godot.Script. | |
| public static Array GetScriptMethodList( this Type t ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script.GetScriptMethodList(); | |
| } | |
| /// Returns the list of properties in this Godot.Script. | |
| public static Array GetScriptPropertyList( this Type t ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script.GetScriptPropertyList(); | |
| } | |
| /// Returns the list of user signals defined in this Godot.Script. | |
| public static Array GetScriptSignalList( this Type t ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script.GetScriptSignalList(); | |
| } | |
| /// Returns true if the script, or a base class, defines a signal with the given | |
| /// name. | |
| public static bool HasScriptSignal( this Type t, string signalName ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script?.HasScriptSignal( signalName ) ?? false; | |
| } | |
| /// Returns true if the script is a tool script. A tool script can run in the editor. | |
| public static bool IsTool( this Type t ) | |
| { | |
| var script = AsCSharpScript( t ); | |
| return script?.IsTool() ?? false; | |
| } | |
| }; | |
| } |
Ok, I tested it in a sterile environment. Seems to be working fine. You may also need to enable C# v8 by adding <LangVersion>8.0</LangVersion> to the 'PropertyGroup' section of your csproj file, or replace ??= with a long-hand coalesce.
If folks are interested, I could wrap this and my IO stuff together into an addon. Then the relative path should always be fixed in "addons/..." and wouldn't need this special treatment.
Nice, I'll give this a spin in my project. The hack isn't super nice, but it's pretty clever and it's the sort of hack you can set and forget, so I can live with that. Now I just need to remember to use the New function for every custom C# script i use instead of just doing new MyClass. Thanks a lot!
Ok, I wrapped up this attribute and my relative-path file IO stuff into one addon. For those interested, you can try it out or steal whatever code you want from it here: https://github.com/cgbeutler/com.monstervial.io_tools
Note that I use the 'Nullable' compile flag, so you may need to remove some nullable refs if you don't use that.