From 9be1b5fdafd89e0ac7a813286d671341ec6ca7cd Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Wed, 4 Oct 2023 02:36:25 +0200 Subject: [PATCH] Implemented improved ECS class generator based on entity descriptors I can't test it yet because the API is full of errors because of the refactoring --- CodeGenerator/ECSAnalyzer.cs | 65 +++++++++++ CodeGenerator/ECSClassGenerator.cs | 129 +++++++++++++++++++++ CodeGenerator/ECSClassInfo.cs | 10 ++ CodeGenerator/ECSPropertyInfo.cs | 11 ++ CodeGenerator/ECSReflectedPropertyInfo.cs | 7 ++ TechbloxModdingAPI/Cluster.cs | 7 +- TechbloxModdingAPI/Common/EcsObjectBase.cs | 14 --- 7 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 CodeGenerator/ECSAnalyzer.cs create mode 100644 CodeGenerator/ECSClassGenerator.cs create mode 100644 CodeGenerator/ECSClassInfo.cs create mode 100644 CodeGenerator/ECSPropertyInfo.cs create mode 100644 CodeGenerator/ECSReflectedPropertyInfo.cs diff --git a/CodeGenerator/ECSAnalyzer.cs b/CodeGenerator/ECSAnalyzer.cs new file mode 100644 index 0000000..b1de381 --- /dev/null +++ b/CodeGenerator/ECSAnalyzer.cs @@ -0,0 +1,65 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Gamecraft.Tweaks; +using Svelto.ECS; + +namespace CodeGenerator +{ + public class ECSAnalyzer + { + public static ECSClassInfo AnalyzeEntityDescriptor(Type entityDescriptorType) + { + // TODO: Add support for creating/deleting entities (getting an up to date server/client engines root) + var templateType = typeof(EntityDescriptorTemplate<>).MakeGenericType(entityDescriptorType); + var getTemplateClass = Expression.Constant(templateType); + var getDescriptorExpr = Expression.PropertyOrField(getTemplateClass, "descriptor"); + var getTemplateDescriptorExpr = + Expression.Lambda>(getDescriptorExpr); + var getTemplateDescriptor = getTemplateDescriptorExpr.Compile(); + var builders = getTemplateDescriptor().componentsToBuild; + return new ECSClassInfo + { + Name = entityDescriptorType.Name.Replace("EntityComponent", "").Replace("EntityStruct", ""), + Properties = builders.Select(builder => builder.GetEntityComponentType()).SelectMany(AnalyzeFields).ToArray() + }; + } + + private static ECSPropertyInfo[] AnalyzeFields(Type componentType) + { + bool useReflection = componentType.IsNotPublic; + var result = new List(); + foreach (var field in componentType.GetFields()) + { + var attr = field.GetCustomAttribute(); + string propName = field.Name; + if (attr != null) + propName = attr.propertyName; + + propName = char.ToUpper(propName[0]) + propName[1..]; + if (useReflection) + { + result.Add(new ECSReflectedPropertyInfo + { + Name = propName, + Type = field.FieldType, + OriginalClassName = componentType.FullName, + ComponentType = componentType + }); + } + + result.Add(new ECSPropertyInfo + { + Name = propName, + Type = field.FieldType, + ComponentType = componentType + }); + } + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/CodeGenerator/ECSClassGenerator.cs b/CodeGenerator/ECSClassGenerator.cs new file mode 100644 index 0000000..343bf33 --- /dev/null +++ b/CodeGenerator/ECSClassGenerator.cs @@ -0,0 +1,129 @@ +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.IO; +using System.Linq; + +namespace CodeGenerator +{ + public static class ECSClassGenerator + { + public static void Generate(Type entityDescriptorType) + { + var info = ECSAnalyzer.AnalyzeEntityDescriptor(entityDescriptorType); + + var codeUnit = new CodeCompileUnit(); + var ns = new CodeNamespace("TechbloxModdingAPI.Blocks"); + ns.Imports.Add(new CodeNamespaceImport("RobocraftX.Common")); + ns.Imports.Add(new CodeNamespaceImport("Svelto.ECS")); + var cl = new CodeTypeDeclaration(info.Name); + cl.BaseTypes.Add(new CodeTypeReference("SignalingBlock")); + cl.Members.Add(new CodeConstructor + { + Parameters = {new CodeParameterDeclarationExpression("EGID", "egid")}, + Comments = + { + _start, + new CodeCommentStatement($"Constructs a(n) {info.Name} object representing an existing block.", true), + _end + }, + BaseConstructorArgs = {new CodeVariableReferenceExpression("egid")}, + Attributes = MemberAttributes.Public | MemberAttributes.Final + }); + foreach (var propertyInfo in info.Properties) + { + if (propertyInfo is ECSReflectedPropertyInfo reflectedPropertyInfo) + GenerateReflectedProperty(reflectedPropertyInfo, cl); + else + GenerateProperty(propertyInfo, cl); + } + ns.Types.Add(cl); + codeUnit.Namespaces.Add(ns); + + var provider = CodeDomProvider.CreateProvider("CSharp"); + var path = $"../../../../TechbloxModdingAPI/Blocks/{info.Name}.cs"; + using (var sw = new StreamWriter(path)) + { + provider.GenerateCodeFromCompileUnit(codeUnit, sw, new CodeGeneratorOptions {BracingStyle = "C"}); + } + + File.WriteAllLines(path, + File.ReadAllLines(path).SkipWhile(line => line.StartsWith("//") || line.Length == 0)); + } + + private static void GenerateProperty(ECSPropertyInfo info, CodeTypeDeclaration cl) + { + var getStruct = new CodeMethodInvokeExpression( + new CodeMethodReferenceExpression(new CodeSnippetExpression("BlockEngine"), + "GetBlockInfo", new CodeTypeReference(info.ComponentType)), + new CodeThisReferenceExpression()); + CodeExpression structFieldReference = new CodeFieldReferenceExpression(getStruct, info.Name); + cl.Members.Add(new CodeMemberProperty + { + Name = info.Name, + HasGet = true, + HasSet = true, + GetStatements = + { + new CodeMethodReturnStatement(structFieldReference) + }, + SetStatements = + { + new CodeAssignStatement(structFieldReference, new CodePropertySetValueReferenceExpression()) + }, + Type = new CodeTypeReference(info.Type), + Attributes = MemberAttributes.Public | MemberAttributes.Final, + Comments = + { + _start, + new CodeCommentStatement($"Gets or sets the {info.ComponentType.Name}'s {info.Name} property." + + " May not be saved.", // TODO: Doesn't know if tweakable stat + true), + _end + } + }); + } + + private static void GenerateReflectedProperty(ECSReflectedPropertyInfo info, CodeTypeDeclaration cl) + { + var reflectedType = new CodeSnippetExpression($"HarmonyLib.AccessTools.TypeByName(\"{info.OriginalClassName}\")"); + + CodeExpression reflectedGet = new CodeCastExpression(info.Type, new CodeMethodInvokeExpression( + new CodeMethodReferenceExpression(new CodeSnippetExpression("BlockEngine"), + "GetBlockInfo"), + new CodeThisReferenceExpression(), reflectedType, new CodePrimitiveExpression(info.Name))); + CodeExpression reflectedSet = new CodeMethodInvokeExpression( + new CodeMethodReferenceExpression(new CodeSnippetExpression("BlockEngine"), + "SetBlockInfo"), + new CodeThisReferenceExpression(), reflectedType, new CodePrimitiveExpression(info.Name), + new CodePropertySetValueReferenceExpression()); + cl.Members.Add(new CodeMemberProperty + { + Name = info.Name, + HasGet = true, + HasSet = true, + GetStatements = + { + new CodeMethodReturnStatement(reflectedGet) + }, + SetStatements = + { + (CodeStatement)new CodeExpressionStatement(reflectedSet) + }, + Type = new CodeTypeReference(info.Type), + Attributes = MemberAttributes.Public | MemberAttributes.Final, + Comments = + { + _start, + new CodeCommentStatement($"Gets or sets the {info.ComponentType.Name}'s {info.Name} property." + + " May not be saved.", // TODO: Doesn't know if tweakable stat + true), + _end + } + }); + } + + private static readonly CodeCommentStatement _start = new("", true); + private static readonly CodeCommentStatement _end = new("", true); + } +} \ No newline at end of file diff --git a/CodeGenerator/ECSClassInfo.cs b/CodeGenerator/ECSClassInfo.cs new file mode 100644 index 0000000..10fa822 --- /dev/null +++ b/CodeGenerator/ECSClassInfo.cs @@ -0,0 +1,10 @@ +using System; + +namespace CodeGenerator +{ + public class ECSClassInfo + { + public string Name { get; set; } + public ECSPropertyInfo[] Properties { get; set; } + } +} \ No newline at end of file diff --git a/CodeGenerator/ECSPropertyInfo.cs b/CodeGenerator/ECSPropertyInfo.cs new file mode 100644 index 0000000..7469825 --- /dev/null +++ b/CodeGenerator/ECSPropertyInfo.cs @@ -0,0 +1,11 @@ +using System; + +namespace CodeGenerator +{ + public class ECSPropertyInfo + { + public string Name { get; set; } + public Type Type { get; set; } + public Type ComponentType { get; set; } + } +} \ No newline at end of file diff --git a/CodeGenerator/ECSReflectedPropertyInfo.cs b/CodeGenerator/ECSReflectedPropertyInfo.cs new file mode 100644 index 0000000..9122e63 --- /dev/null +++ b/CodeGenerator/ECSReflectedPropertyInfo.cs @@ -0,0 +1,7 @@ +namespace CodeGenerator +{ + public class ECSReflectedPropertyInfo : ECSPropertyInfo + { + public string OriginalClassName { get; set; } + } +} \ No newline at end of file diff --git a/TechbloxModdingAPI/Cluster.cs b/TechbloxModdingAPI/Cluster.cs index 378d4db..f6d9745 100644 --- a/TechbloxModdingAPI/Cluster.cs +++ b/TechbloxModdingAPI/Cluster.cs @@ -1,16 +1,17 @@ using Svelto.ECS; +using Techblox.Destruction; using Techblox.TimeRunning.Clusters; namespace TechbloxModdingAPI { /// - /// Represnts a cluster of blocks in time running mode, meaning blocks that are connected either directly or via joints. + /// Represents a cluster of blocks in time running mode, meaning blocks that are connected either directly or via joints. /// Only exists if a cluster destruction manager is present. Static blocks like grass and dirt aren't part of a cluster. /// public class Cluster : EcsObjectBase { - public Cluster(EGID id) : base(id) - { + public Cluster(EGID id) : base(id, typeof(ResetDestructionUtility)) + { // TODO: Damage has been connection-based for a while } public Cluster(uint id) : this(new EGID(id, ClustersExclusiveGroups.SIMULATION_CLUSTERS_GROUP)) diff --git a/TechbloxModdingAPI/Common/EcsObjectBase.cs b/TechbloxModdingAPI/Common/EcsObjectBase.cs index 4bbfe58..c0f8c80 100644 --- a/TechbloxModdingAPI/Common/EcsObjectBase.cs +++ b/TechbloxModdingAPI/Common/EcsObjectBase.cs @@ -48,20 +48,6 @@ namespace TechbloxModdingAPI Id = id; } - private void AnalyzeEntityDescriptor(Type entityDescriptorType) - { - // TODO: Cache - // TODO: This should be in BlockClassGenerator - // TODO: Add support for creating/deleting entities (getting an up to date server/client engines root) - var templateType = typeof(EntityDescriptorTemplate<>).MakeGenericType(entityDescriptorType); - var getTemplateClass = Expression.Constant(templateType); - var getDescriptorExpr = Expression.PropertyOrField(getTemplateClass, "descriptor"); - var getTemplateDescriptorExpr = - Expression.Lambda>(getDescriptorExpr); - var getTemplateDescriptor = getTemplateDescriptorExpr.Compile(); - var builders = getTemplateDescriptor().componentsToBuild; - } - #region ECS initializer stuff protected internal EcsInitData InitData;