From 7eb2e750e91ee9e702b5e8fd72ed32f7f54e998e Mon Sep 17 00:00:00 2001 From: Mario Steele Date: Fri, 26 Sep 2025 03:13:44 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 4 + ActionGenerator.cs | 71 +++++++++ Godot.Sharp.Extended.csproj | 24 +++ Godot.Sharp.Extended.sln | 24 +++ NodeBindGenerator.cs | 168 ++++++++++++++++++++ NodePropBindGenerator.cs | 180 +++++++++++++++++++++ ResourceGenerator.cs | 241 ++++++++++++++++++++++++++++ SceneGenerator.cs | 64 ++++++++ StringExtensionMethods.cs | 305 ++++++++++++++++++++++++++++++++++++ 9 files changed, 1081 insertions(+) create mode 100644 .gitignore create mode 100644 ActionGenerator.cs create mode 100644 Godot.Sharp.Extended.csproj create mode 100644 Godot.Sharp.Extended.sln create mode 100644 NodeBindGenerator.cs create mode 100644 NodePropBindGenerator.cs create mode 100644 ResourceGenerator.cs create mode 100644 SceneGenerator.cs create mode 100644 StringExtensionMethods.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e80f887 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +.qodo/ +bin/ +obj/ diff --git a/ActionGenerator.cs b/ActionGenerator.cs new file mode 100644 index 0000000..1d657eb --- /dev/null +++ b/ActionGenerator.cs @@ -0,0 +1,71 @@ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class ActionGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var projectFile = context.AdditionalTextsProvider + .Where(file => file.Path.EndsWith("project.godot")) + .Select((file, token) => file.GetText()) + .Collect(); + + context.RegisterSourceOutput(projectFile, GenerateCode); + } + + private void GenerateCode(SourceProductionContext ctx, ImmutableArray texts) + { + var memberCode = new StringBuilder(); + foreach (var text in texts) + { + if (text == null) continue; + var content = text.ToString(); + var inputs = GetActions(content); + var fields = inputs.Select(keyData => $$"""\tpublic static StringName {{keyData.ToPascalCase()}} = "{{keyData}}";"""); + + foreach (var field in fields) memberCode.AppendLine(field); + } + + var code = $$""" + // + + using System; + using Godot; + + namespace Godot.Sharp.Extended; + + public partial class Actions + { + {{memberCode}} + } + """; + ctx.AddSource("Godot.Sharp.Extended.Actions.g.cs", SourceText.From(code, Encoding.UTF8)); + } + + public List GetActions(string content) + { + var results = new List(); + var inputSection = false; + foreach (var line in content.Split(['\n'])) + { + if (line.Equals("[input]")) + inputSection = true; + else if (line.StartsWith("[") && inputSection) break; + + if (!inputSection) continue; + + var kv = line.Split(['='], 2); + if (kv.Length == 2) results.Add(kv[0].Trim()); + } + + return results; + } +} \ No newline at end of file diff --git a/Godot.Sharp.Extended.csproj b/Godot.Sharp.Extended.csproj new file mode 100644 index 0000000..8f7100f --- /dev/null +++ b/Godot.Sharp.Extended.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + false + latest + + true + true + + Godot.Sharp.Extended + Godot.Sharp.Extended + Godot.Sharp.Extended + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Godot.Sharp.Extended.sln b/Godot.Sharp.Extended.sln new file mode 100644 index 0000000..4ba4e2c --- /dev/null +++ b/Godot.Sharp.Extended.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.Sharp.Extended", "Godot.Sharp.Extended.csproj", "{4D64B952-274C-14C1-D766-218DC5756B49}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4D64B952-274C-14C1-D766-218DC5756B49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D64B952-274C-14C1-D766-218DC5756B49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D64B952-274C-14C1-D766-218DC5756B49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D64B952-274C-14C1-D766-218DC5756B49}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2935E47E-E0E5-4C65-9EE5-E6945505387D} + EndGlobalSection +EndGlobal diff --git a/NodeBindGenerator.cs b/NodeBindGenerator.cs new file mode 100644 index 0000000..59e4be1 --- /dev/null +++ b/NodeBindGenerator.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class NodeBindGenerator : IIncrementalGenerator +{ + private const string AttributeSourceCode = """ + // + namespace Godot.Sharp.Extended; + + [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)] + public class NodeBindAttribute : Attribute + { + } + """; + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "NodeBindAttribute.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + var propertyProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: (ctx, _) => GetDeclarationForSourceGen(ctx)) + .Where(t => t.attributeFound) + .SelectMany((t, _) => t.Item1); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(propertyProvider.Collect()), + (ctx, value) => GenerateCode(ctx, value.Left, value.Right)); + } + + private (IEnumerable, bool attributeFound) GetDeclarationForSourceGen( + GeneratorSyntaxContext context) + { + var classSyntax = (ClassDeclarationSyntax)context.Node; + var members = classSyntax.Members; + var foundMembers = members.Select(member => IsNodeBindAttributeOnMember(context.SemanticModel, member)) + .Where(syntax => syntax is not null) + .ToList(); + return (foundMembers!, foundMembers.Count > 0); + } + + private MemberDeclarationSyntax? IsNodeBindAttributeOnMember(SemanticModel model, + MemberDeclarationSyntax declarationSyntax) + { + foreach (var attributeSyntax in declarationSyntax + .AttributeLists + .SelectMany(attributeList => attributeList.Attributes)) + { + if (model.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol symbol) + continue; + + var attributeName = symbol.ContainingType.ToDisplayString(); + if (attributeName == $"Godot.Sharp.Extended.NodeBind") + return declarationSyntax; + } + + return null; + } + + private void GenerateCode(SourceProductionContext ctx, Compilation compilation, + ImmutableArray members) + { + Dictionary classDefinitions = []; + + foreach (var member in members) + { + if (member.Parent is ClassDeclarationSyntax classDeclaration) + { + var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol) + continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classDeclaration.Identifier.Text; + var key = $"{namespaceName}.{className}"; + if (!classDefinitions.ContainsKey(key)) classDefinitions[key] = new ClassDefinition(); + + var classDefinition = classDefinitions[key]; + classDefinition.ClassName = className; + classDefinition.Namespace = namespaceName; + if (member is FieldDeclarationSyntax field) + { + var variable = field.Declaration.Variables.First(); + + if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) continue; + + var memberDefinition = new MemberDefinition + { + Name = fieldSymbol.Name, + Type = fieldSymbol.Type.Name + }; + classDefinition.MemberDefinitions.Add(memberDefinition); + classDefinition.UsingNamespaceName.Add(fieldSymbol.Type.ContainingNamespace.ToDisplayString()); + } + else if (member is PropertyDeclarationSyntax property) + { + if (semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol propertySymbol) continue; + + var memberDefinition = new MemberDefinition + { + Name = propertySymbol.Name, + Type = propertySymbol.Type.Name + }; + classDefinition.MemberDefinitions.Add(memberDefinition); + classDefinition.UsingNamespaceName.Add(propertySymbol.Type.ContainingNamespace.ToDisplayString()); + } + } + } + + foreach (var definition in classDefinitions.Values) GenerateClass(ctx, definition); + } + + private void GenerateClass(SourceProductionContext ctx, ClassDefinition definition) + { + var memberCode = new StringBuilder(); + foreach (var memberDefinition in definition.MemberDefinitions) + { + var nodeName = memberDefinition.Name.TrimStart('_').ToPascalCase(); + + memberCode.AppendLine($$""" + {{memberDefinition.Name}} = GetNode<{{memberDefinition.Type}}>("%{{nodeName}}"); + """); + } + + var usingCode = new StringBuilder(); + foreach (var namespaceName in definition.UsingNamespaceName) usingCode.AppendLine($"using {namespaceName};"); + + var code = $$""" + // + + using System; + {{usingCode}} + + namespace {{definition.Namespace}}; + + partial class {{definition.ClassName}} + { + private void BindNodes() { + {{memberCode}} + } + } + """; + ctx.AddSource($"Godot.Sharp.Extended.{definition.ClassName}.NodeBind.g.cs", SourceText.From(code, Encoding.UTF8)); + } + + private class ClassDefinition + { + public string Namespace { get; set; } = ""; + public string ClassName { get; set; } = ""; + public HashSet UsingNamespaceName { get; set; } = []; + public List MemberDefinitions { get; } = []; + } + + private class MemberDefinition + { + public string Type { get; set; } = ""; + public string Name { get; set; } = ""; + } +} \ No newline at end of file diff --git a/NodePropBindGenerator.cs b/NodePropBindGenerator.cs new file mode 100644 index 0000000..976fecd --- /dev/null +++ b/NodePropBindGenerator.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Net.Mime; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class NodePropBindGenerator : IIncrementalGenerator +{ + private const string AttributeSourceCode = """ + // + namespace Godot.Sharp.Extended; + + [System.AttributeUsage(System.AttributeTargets.Member)] + public class NodePropBindAttribute : System.Attribute + { + public string TargetNodeName { get; private set; } + public string GodotPropertyName { get; private set; } + + public NodePropBindAttribute(string targetNodeName, string godotPropertyName) + { + TargetNodeName = targetNodeName; + GodotPropertyName = godotPropertyName; + } + } + """; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "NodePropBind.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + var propertyProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: (ctx, _) => GetDeclarationForSourceGen(ctx)) + .Where(t => t.attributeFound) + .SelectMany((t, _) => t.members); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(propertyProvider.Collect()), + (ctx, value) => GenerateCode(ctx, value.Left, value.Right)); + } + + private (IEnumerable members, bool attributeFound) GetDeclarationForSourceGen( + GeneratorSyntaxContext context) + { + var classSyntax = (ClassDeclarationSyntax)context.Node; + var members = classSyntax.Members; + var foundMembers = members.Select(member => IsNodePropBindAttributeOnMember(context.SemanticModel, member)) + .Where(syntax => syntax is not null) + .ToList(); + return (foundMembers!, foundMembers.Count > 0); + } + + private MemberDeclarationSyntax? IsNodePropBindAttributeOnMember(SemanticModel model, + MemberDeclarationSyntax declarationSyntax) + { + foreach (var attributeSyntax in declarationSyntax + .AttributeLists + .SelectMany(attributeList => attributeList.Attributes)) + { + if (model.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol symbol) + continue; + + var attributeName = symbol.ContainingType.ToDisplayString(); + if (attributeName == "Godot.Sharp.Extended.NodePropBind") + return declarationSyntax; + } + + return null; + } + + private void GenerateCode(SourceProductionContext context, Compilation compilation, + ImmutableArray members) + { + Dictionary classDefinitions = []; + + foreach (var member in members) + { + if (member.Parent is ClassDeclarationSyntax classDeclaration) + { + var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol) + continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classDeclaration.Identifier.Text; + var key = $"{namespaceName}.{className}"; + if (!classDefinitions.ContainsKey(key)) + classDefinitions[key] = new ClassDefinition(namespaceName, className, new HashSet(), + new List()); + + var classDefinition = classDefinitions[key]; + if (member is FieldDeclarationSyntax field) + { + var variable = field.Declaration.Variables.First(); + + if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) continue; + + var attributeData = fieldSymbol.GetAttributes() + .FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Godot.Sharp.Extended.NodePropBindAttribute"); + + if (attributeData is null) continue; + + var nodeProp = attributeData.ConstructorArguments[0].Value as string; + var godotProp = attributeData.ConstructorArguments[1].Value as string; + + var memberDefinition = new MemberDefinition(fieldSymbol.Type.Name, fieldSymbol.Name, nodeProp, godotProp); + classDefinition.MemberDefinitions.Add(memberDefinition); + classDefinition.UsingNamespaceName.Add(fieldSymbol.ContainingNamespace.ToDisplayString()); + } + } + } + + foreach (var definition in classDefinitions.Values) GenerateClass(context, definition); + } + + private void GenerateClass(SourceProductionContext context, ClassDefinition definition) + { + var propCode = new StringBuilder(); + foreach (var memberDefinition in definition.MemberDefinitions) + { + var propName = memberDefinition.Name.TrimStart('_').ToPascalCase(); + var godotProp = memberDefinition.GodotProp.ToPascalCase(); + + propCode.AppendLine($$""" + public {{memberDefinition.Type}} {{propName}} + { + get => {{memberDefinition.Name}}; + set { + {{memberDefinition.Name}} = value; + if ({{memberDefinition.NodeProp}} != null) + {{memberDefinition.NodeProp}}.{{godotProp}} = value; + } + } + """); + } + + var usingCode = new StringBuilder(); + foreach (var namespaceName in definition.UsingNamespaceName) usingCode.AppendLine($"using {namespaceName};"); + + var code = $$""" + // + + using System; + using Godot; + {{usingCode}} + + namespace {{definition.Namespace}}; + + partial class {{definition.ClassName}} + { + {{propCode}} + } + """; + context.AddSource($"Godot.Sharp.Extended.{definition.ClassName}.NodeBind.g.cs", SourceText.From(code, Encoding.UTF8)); + } + + private class ClassDefinition(string theNamespace, string className, HashSet usingNamespaceName, List memberDefinitions) + { + public string Namespace => theNamespace; + public string ClassName => className; + public HashSet UsingNamespaceName => usingNamespaceName; + public List MemberDefinitions => memberDefinitions; + } + + private class MemberDefinition(string type, string name, string nodeProp, string godotProp) + { + public string Type => type; + public string Name => name; + public string NodeProp => nodeProp; + public string GodotProp => godotProp; + } +} \ No newline at end of file diff --git a/ResourceGenerator.cs b/ResourceGenerator.cs new file mode 100644 index 0000000..be0799e --- /dev/null +++ b/ResourceGenerator.cs @@ -0,0 +1,241 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class ResourceGenerator : IIncrementalGenerator +{ + private const string AttributeSourceCode = """ + // + namespace Godot.Sharp.Extended; + + [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)] + public class ResourcePathAttribute : System.Attribute + { + public string Path { get; private set; } + + public ResourcePathAttribute(string path) { + Path = path; + } + } + """; + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "ResourcePathAttribute.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + var classPaths = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax syntax, + transform: (ctx, _) => GetClassDeclaration(ctx)) + .Where(t => t.reportAttributeFound) + .Select((t, _) => (t.Item1, t.path)) + .Collect(); + + var provider2 = context.AdditionalTextsProvider.Combine(classPaths) + .Where(CheckResourcePath) + .Select((t1, _) => + { + var classDeclarationForPath = GetClassDeclarationForPath(t1); + return new Storage(t1.Left, classDeclarationForPath.Item1, classDeclarationForPath.path); + }); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(provider2.Collect()), + (ctx, t) => GenerateCode(ctx, t.Left, t.Right)); + } + + private (ClassDeclarationSyntax, string path) GetClassDeclarationForPath((AdditionalText file, + ImmutableArray<(ClassDeclarationSyntax, string path)> ClassDeclarationSyntax) tuple) + { + var realFilePath = tuple.file.Path; + var relativeFilePath = Path.GetDirectoryName(realFilePath)?.Replace('\\', '/'); + return tuple.ClassDeclarationSyntax.First(data => relativeFilePath!.EndsWith(data.path)); + } + + private bool CheckResourcePath( + (AdditionalText file, ImmutableArray<(ClassDeclarationSyntax, string path)> Right) tuple) + { + var realFilePath = tuple.file.Path; + var filePathToCheck = tuple.Right.Select(data => data.path); + var relativeFilePath = Path.GetDirectoryName(realFilePath)?.Replace('\\', '/'); + return filePathToCheck.Any(path => relativeFilePath?.EndsWith(path) ?? false); + } + + private (ClassDeclarationSyntax classDeclarationSyntax, bool reportAttributeFound, string path) GetClassDeclaration( + GeneratorSyntaxContext ctx) + { + var classDeclarationSyntax = (ClassDeclarationSyntax)ctx.Node; + + foreach (var attributeSyntax in classDeclarationSyntax + .AttributeLists + .SelectMany(attributeListSyntax => attributeListSyntax.Attributes)) + { + var symbolInfo = ctx.SemanticModel.GetSymbolInfo(attributeSyntax); + if (symbolInfo.Symbol is not IMethodSymbol attributeSymbol) continue; + if (attributeSymbol.ContainingType.ToDisplayString() != "Godot.Sharp.Extended.ResourcePath") continue; + var parameter = attributeSyntax.ArgumentList; + var pathArgument = parameter?.Arguments[0]; + + var path = ""; + if (pathArgument?.Expression is LiteralExpressionSyntax literal) path = literal.Token.ValueText; + + return (classDeclarationSyntax, true, path); + } + + return (classDeclarationSyntax, false, ""); + } + + private void GenerateCode( + SourceProductionContext context, + Compilation compilation, + IEnumerable storages) + { + Dictionary classDefinitions = new(); + + foreach (var storage in storages) + { + var path = storage.Path; + var text = storage.Text; + var classDeclarationSyntax = storage.ClassDeclarationSyntax; + + var firstLine = text.GetText()?.Lines[0].ToString(); + if (firstLine == null) continue; + var scriptNameMatch = Regex.Match(firstLine, @"script_class=""(.+?)"""); + if (!scriptNameMatch.Success) scriptNameMatch = Regex.Match(firstLine, @"type=""(.+?)"""); + + var scriptName = scriptNameMatch.Groups[1].Value; + var fileName = Path.GetFileNameWithoutExtension(path); + + var scriptNamespaceType = FindClassSymbol(compilation.GlobalNamespace, scriptName); + var scriptNamespace = scriptNamespaceType?.ContainingNamespace?.ToDisplayString(); + + var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classDeclarationSyntax.Identifier.Text; + + var key = $"{namespaceName}.{className}"; + if (!classDefinitions.ContainsKey(key)) + classDefinitions[key] = new ClassDefinition + { + ClassName = className, + NamespaceName = namespaceName, + }; + + var def = classDefinitions[key]; + def.Resources.Add(new ResourceDefinition + { + Type = scriptName, + FieldName = fileName, + FilePath = path + "/" + fileName + ".tres" + }); + + if (scriptNamespace != null) def.UsingDefinitionSet.Add(scriptNamespace); + } + + foreach (var classDefinition in classDefinitions.Values) + { + var resources = new StringBuilder(); + var allDefinition = new StringBuilder(); + var usingDefinition = new StringBuilder(); + + foreach(var def in classDefinition.UsingDefinitionSet) usingDefinition.AppendLine($"using {def};"); + + if (!classDefinition.UsingDefinitionSet.Contains("Godot")) usingDefinition.AppendLine("using Godot;"); + + foreach(var resource in classDefinition.Resources) + resources.AppendLine( + $""" + public static readonly {resource.Type} {resource.FieldName} = + GD.Load<{resource.Type}>(ProjectSettings.GlobalizePath("res://{resource.FilePath}")); + """); + + var resourceType = ""; + var generateAll = true; + foreach (var resource in classDefinition.Resources) + { + if (resourceType != "" && resourceType != resource.Type) generateAll = false; + + resourceType = resource.Type; + allDefinition.AppendLine( + $" {resource.FieldName},"); + } + + var allMethod = ""; + if (generateAll) + allMethod = + $""" + public static {resourceType}[] All => [ + {allDefinition}]; + """; + else + allMethod = + $""" + public static Resource[] All => [ + {allDefinition}]; + """; + + var code = + $$""" + // + using System; + {{usingDefinition}} + + namespace {{classDefinition.NamespaceName}}; + + public partial class {{classDefinition.ClassName}} + { + {{resources}} + {{allMethod}} + } + """; + context.AddSource($"Godot.Sharp.Extended.{classDefinition.ClassName}.Res.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + + private INamedTypeSymbol? FindClassSymbol(INamespaceSymbol namespaceSymbol, string className) + { + var classSymbol = namespaceSymbol.GetTypeMembers().FirstOrDefault(m => m.Name == className); + if (classSymbol != null) return classSymbol; + + foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers()) + { + classSymbol = FindClassSymbol(childNamespace, className); + if (classSymbol != null) return classSymbol; + } + + return null; + } + + private class ClassDefinition + { + public HashSet UsingDefinitionSet { get; private set; } = []; + public string NamespaceName { get; set; } = ""; + public string ClassName { get; set; } = ""; + public List Resources { get; private set; } = []; + } + + private class ResourceDefinition + { + public string Type { get; set; } = ""; + public string FilePath { get; set; } = ""; + public string FieldName { get; set; } = ""; + } + + private class Storage(AdditionalText text, ClassDeclarationSyntax classDeclarationSyntax, string path) + { + public AdditionalText Text => text; + public ClassDeclarationSyntax ClassDeclarationSyntax => classDeclarationSyntax; + public string Path => path; + } +} \ No newline at end of file diff --git a/SceneGenerator.cs b/SceneGenerator.cs new file mode 100644 index 0000000..b3ad16d --- /dev/null +++ b/SceneGenerator.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class SceneGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var scenes = context.AdditionalTextsProvider + .Where(text => text.Path.EndsWith(".tscn")); + + context.RegisterSourceOutput( + context.CompilationProvider.Combine(scenes.Collect()).Combine(context.AnalyzerConfigOptionsProvider), + (ctx, t) => GenerateCode(ctx, t.Left.Left, t.Left.Right, t.Right)); + } + + private void GenerateCode( + SourceProductionContext context, + Compilation compilation, + IEnumerable scenes, + AnalyzerConfigOptionsProvider optionsProvider) + { + optionsProvider.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDirectory); + + var allMethods = new StringBuilder(); + foreach (var scene in scenes) + { + var directory = Path.GetDirectoryName(scene.Path); + var name = Path.GetFileNameWithoutExtension(scene.Path); + var relativePath = scene.Path.Replace(projectDirectory ?? "", "").Replace("\\", "/"); + allMethods.AppendLine( + $$""" + public static T New{{name.ToPascalCase()}}() where T : Node { + var packedScene = GD.Load("res://{{relativePath}}"); + return packedScene.Instance(); + } + """); + + var code = + $$""" + // + + using System; + using Godot; + + namespace Godot.Sharp.Extended; + + public class Scenes + { + {{allMethods}} + } + """; + + context.AddSource("Godot.Sharp.Extended.Scenes.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + +} \ No newline at end of file diff --git a/StringExtensionMethods.cs b/StringExtensionMethods.cs new file mode 100644 index 0000000..19f63bc --- /dev/null +++ b/StringExtensionMethods.cs @@ -0,0 +1,305 @@ +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Godot.Sharp.Extended.Generators; + +public static class StringExtensionMethods +{ + public static string ToPascalCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + newString[newIndex] = insertSeparator + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToCamelCase(this string value) + { + int newIndex = 0; + bool insertSeparator = false; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + newString[newIndex] = insertSeparator && !isFirstCharacter + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToUnderscoreCamelCase(this string value) + { + int newIndex = 1; + bool insertSeparator = false; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value) + 1]; + newString[0] = '_'; + for (int i = 1; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + newString[newIndex] = insertSeparator && !isFirstCharacter + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToKebabCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '-'; + newIndex++; + } + + newString[newIndex] = char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToSnakeCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '_'; + newIndex++; + } + + newString[newIndex] = char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToMacroCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '_'; + newIndex++; + } + + newString[newIndex] = char.ToUpperInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToTrainCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '-'; + newIndex++; + } + + newString[newIndex] = insertSeparator + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToTitleCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = ' '; + newIndex++; + } + + newString[newIndex] = insertSeparator + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateSpanSizeForKebabOrSnakeCase(string text) + { + UnicodeCategory previous = char.GetUnicodeCategory(text[0]); + UnicodeCategory current; + int skips = IsSpecialCharacter(previous) ? 1 : 0; + int divs = 0; + for (int i = 1; i < text.Length; i++) + { + current = char.GetUnicodeCategory(text[i]); + skips += IsSpecialCharacter(current) ? 1 : 0; + divs += previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber) + ? 1 + : 0; + previous = current; + } + + return divs - skips; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateSpanSizeForPascalOrCamelCase(string text) + { + UnicodeCategory current; + int skips = 0; + for (int i = 0; i < text.Length - 1; i++) + { + current = char.GetUnicodeCategory(text[i]); + skips -= IsSpecialCharacter(current) ? 1 : 0; + } + + return skips; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSpecialCharacter(UnicodeCategory category) + { + return category is not UnicodeCategory.UppercaseLetter + and not UnicodeCategory.LowercaseLetter + and not UnicodeCategory.DecimalDigitNumber; + } +} \ No newline at end of file