Skip to content

Commit

Permalink
Feature/entity metadata (#334)
Browse files Browse the repository at this point in the history
* Create types and properties

* Add entity data file

* Implement initial attribute and generator stuff

* Add attribute to most entity classes

* Implement IEquatable

* Add final properties and refactor IEntity & Entity

* Oop fix error
  • Loading branch information
Tides authored Mar 11, 2023
1 parent c5b5c29 commit 2714f23
Show file tree
Hide file tree
Showing 33 changed files with 3,116 additions and 54 deletions.
9 changes: 9 additions & 0 deletions Obsidian.API/_Attributes/MinecraftEntityAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Obsidian.API;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class MinecraftEntityAttribute : Attribute
{
public string ResourceLocation { get; init; }

public MinecraftEntityAttribute(string resourceLocation) => this.ResourceLocation = resourceLocation;
}
14 changes: 14 additions & 0 deletions Obsidian.API/_Interfaces/IEntity.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Obsidian.API.AI;
using System.Collections.Concurrent;

namespace Obsidian.API;

Expand All @@ -16,17 +17,22 @@ public interface IEntity
public VectorF Position { get; set; }
public Angle Pitch { get; set; }
public Angle Yaw { get; set; }

public int EntityId { get; }

public Pose Pose { get; set; }
public EntityType Type { get; }

public BoundingBox BoundingBox { get; }
public EntityDimension Dimension { get; }
public int Air { get; set; }

public float Health { get; set; }

public ChatMessage CustomName { get; set; }

public string TranslationKey { get; }

public bool CustomNameVisible { get; }
public bool Silent { get; }
public bool NoGravity { get; }
Expand All @@ -38,6 +44,8 @@ public interface IEntity
public bool Burning { get; }
public bool Swimming { get; }
public bool FlyingWithElytra { get; }
public bool Summonable { get; }
public bool IsFireImmune { get; }

public Task RemoveAsync();
public Task TickAsync();
Expand All @@ -53,4 +61,10 @@ public interface IEntity
public IEnumerable<IEntity> GetEntitiesNear(float distance);

public VectorF GetLookDirection();

public bool HasAttribute(string attributeResourceName);
public bool TryAddAttribute(string attributeResourceName, float value);
public bool TryUpdateAttribute(string attributeResourceName, float newValue);

public float GetAttributeValue(string attributeResourceName);
}
9 changes: 5 additions & 4 deletions Obsidian.API/_Types/BoundingBox.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
namespace Obsidian.API;

public struct BoundingBox : IEquatable<BoundingBox>
public readonly struct BoundingBox : IEquatable<BoundingBox>
{
public const int CornerCount = 8;
public VectorF Max;
public VectorF Min;

public readonly VectorF Max;
public readonly VectorF Min;

public BoundingBox(VectorF min, VectorF max)
{
Expand Down Expand Up @@ -74,7 +75,7 @@ public static BoundingBox CreateFromPoints(IEnumerable<VectorF> points)
}

if (empty)
throw new ArgumentException();
throw new ArgumentException("Invalid points specified.");

return new BoundingBox(pos2, pos1);
}
Expand Down
22 changes: 22 additions & 0 deletions Obsidian.API/_Types/EntityDimensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Obsidian.API;
public readonly record struct EntityDimension : IEquatable<EntityDimension>
{
public static readonly EntityDimension Zero = new() { Height = 0, Width = 0 };

public required float Width { get; init; }

public required float Height { get; init; }

public BoundingBox CreateBBFromPosition(VectorF position)
{
var updatedWidth = this.Width / 2.0f;

return new(new VectorF(position.X - updatedWidth, position.Y, position.Z - updatedWidth),
new VectorF(position.X + updatedWidth, position.Y + this.Height, position.Z + updatedWidth));
}

public bool Equals(EntityDimension other) =>
other.Height == this.Height && other.Width == this.Width;

public override int GetHashCode() => HashCode.Combine(this.Height, this.Width);
}
166 changes: 166 additions & 0 deletions Obsidian.SourceGenerators/Registry/EntityGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using Microsoft.CodeAnalysis.CSharp;
using Obsidian.SourceGenerators.Registry.Models;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Xml.Schema;

namespace Obsidian.SourceGenerators.Registry;

[Generator]
public sealed partial class EntityGenerator : IIncrementalGenerator
{
private const string AttributeName = "MinecraftEntityAttribute";
private const string CleanedAttributeName = "MinecraftEntity";

public void Initialize(IncrementalGeneratorInitializationContext ctx)
{
var jsonFiles = ctx.AdditionalTextsProvider
.Where(file => Path.GetFileNameWithoutExtension(file.Path) == "entities")
.Select(static (file, ct) => file.GetText(ct)!.ToString());

IncrementalValuesProvider<ClassDeclarationSyntax> classDeclarations = ctx.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax syntax,
static (context, _) => TransformData(context.Node as ClassDeclarationSyntax, context))
.Where(static m => m is not null)!;

var compilation = ctx.CompilationProvider.Combine(classDeclarations.Collect()).Combine(jsonFiles.Collect());

ctx.RegisterSourceOutput(compilation,
(spc, src) => this.Generate(spc, src.Left.Left, src.Left.Right, src.Right.First()));
}

private static ClassDeclarationSyntax? TransformData(ClassDeclarationSyntax? syntax, GeneratorSyntaxContext ctx)
{
if (syntax is null)
return null;

var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node);

if (symbol == null)
return null;

return symbol.GetAttributes().Any(x => x.AttributeClass?.Name == AttributeName) ? syntax : null;
}

private void Generate(SourceProductionContext context, Compilation compilation, ImmutableArray<ClassDeclarationSyntax> typeList, string entitiesJson)
{
using var document = JsonDocument.Parse(entitiesJson);

var elements = document.RootElement;

var asm = compilation.AssemblyName;

if (asm != "Obsidian")
return;

var classes = new List<EntityClass>();

foreach (var @class in typeList)
{
var model = compilation.GetSemanticModel(@class.SyntaxTree);
var symbol = model.GetDeclaredSymbol(@class);

if (symbol is null)
continue;

var attribute = @class.AttributeLists.SelectMany(x => x.Attributes).FirstOrDefault(x => x.Name.ToString() == CleanedAttributeName);

if (attribute is null)
continue;

if (!@class.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)))
{
context.ReportDiagnostic(DiagnosticSeverity.Error, $"Type {symbol.Name} must be marked as partial in order to generate required properties.", @class);
continue;
}

var arg = attribute.ArgumentList!.Arguments[0];
var expression = arg.Expression;
var value = model.GetConstantValue(expression).ToString();

classes.Add(new EntityClass(symbol, value));
}

this.GenerateClasses(classes, document, context);
}

private void GenerateClasses(List<EntityClass> classes, JsonDocument document, SourceProductionContext context)
{
var element = document.RootElement;

foreach (var @class in classes)
{
if (!element.TryGetProperty(@class.EntityResourceLocation, out var entityElement))
{
context.ReportDiagnostic(DiagnosticSeverity.Warning, $"Failed to find valid entity {@class.EntityResourceLocation}");
continue;
}

if (!entityElement.TryGetProperty("width", out var widthElement))
{
context.ReportDiagnostic(DiagnosticSeverity.Error, $"Failed to find valid width for {@class.EntityResourceLocation}");
continue;
}

if (!entityElement.TryGetProperty("height", out var heightElement))
{
context.ReportDiagnostic(DiagnosticSeverity.Error, $"Failed to find valid height for {@class.EntityResourceLocation}");
continue;
}

var builder = new CodeBuilder()
.Namespace("Obsidian.Entities")
.Line()
.Type($"public partial class {@class.Symbol.Name}");

builder.Indent().Append("public override EntityDimension Dimension { get; protected set; } = new() { ")
.Append($"Width = {widthElement.GetSingle()}f, ")
.Append($"Height = {heightElement.GetSingle()}f }}; ")
.Line()
.Line();

if (entityElement.TryGetProperty("is_fire_immune", out var fireImmuneElement))
{
builder.Indent().Append($"public override bool IsFireImmune {{ get; set; }} = {fireImmuneElement.GetBoolean().ToString().ToLowerInvariant()};")
.Line()
.Line();
}

if (entityElement.TryGetProperty("summonable", out var summonableElement))
{
builder.Indent().Append($"public override bool Summonable {{ get; set; }} = {summonableElement.GetBoolean().ToString().ToLowerInvariant()};")
.Line()
.Line();
}

if (entityElement.TryGetProperty("translation_key", out var translationKeyElement))
{
builder.Indent().Append($"public override string TranslationKey {{ get; protected set; }} = \"{translationKeyElement.GetString()}\";")
.Line()
.Line();
}

if (entityElement.TryGetProperty("attributes", out var attributesElement))
{
builder.Method("protected override ConcurrentDictionary<string, float> Attributes { get; } = new(new Dictionary<string, float>");

foreach (var attrElement in attributesElement.EnumerateObject())
{
var name = attrElement.Name;
var value = attrElement.Value.GetSingle();

builder.Line($"{{ \"{name}\", {value}f }}, ");
}

builder.EndScope(")", true);
}

builder.EndScope();

context.AddSource($"{@class.Symbol.Name}.g.cs", builder.ToString());
}
}
}
13 changes: 13 additions & 0 deletions Obsidian.SourceGenerators/Registry/Models/EntityClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Obsidian.SourceGenerators.Registry.Models;
public readonly struct EntityClass
{
public INamedTypeSymbol Symbol { get; }

public string EntityResourceLocation { get; }

public EntityClass(INamedTypeSymbol symbol, string resourceLocation)
{
this.Symbol = symbol;
this.EntityResourceLocation = resourceLocation;
}
}
Loading

0 comments on commit 2714f23

Please sign in to comment.