Initial Commit
This commit is contained in:
commit
7eb2e750e9
9 changed files with 1081 additions and 0 deletions
241
ResourceGenerator.cs
Normal file
241
ResourceGenerator.cs
Normal file
|
|
@ -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 = """
|
||||
// <auto-generated />
|
||||
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<Storage> storages)
|
||||
{
|
||||
Dictionary<string, ClassDefinition> 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 =
|
||||
$$"""
|
||||
// <auto-generated />
|
||||
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<string> UsingDefinitionSet { get; private set; } = [];
|
||||
public string NamespaceName { get; set; } = "";
|
||||
public string ClassName { get; set; } = "";
|
||||
public List<ResourceDefinition> 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue