From eabc5c15c76267e2416d9caf813e027a91cf76e3 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 26 Jun 2020 11:58:35 -0400 Subject: [PATCH] Codegen for an inheritance tree --- .../mindustry/annotations/Annotations.java | 3 + .../mindustry/annotations/BaseProcessor.java | 19 ++- .../annotations/entity/EntityIO.java | 4 +- .../annotations/entity/EntityProcess.java | 152 ++++++++++++++---- .../src/main/resources/classids.properties | 1 + build.gradle | 1 - core/src/mindustry/ClientLauncher.java | 2 +- core/src/mindustry/entities/GroupDefs.java | 2 +- .../comp/{TileComp.java => BuildingComp.java} | 6 +- .../mindustry/entities/comp/BulletComp.java | 2 +- .../mindustry/entities/comp/PlayerComp.java | 2 +- .../src/mindustry/entities/comp/UnitComp.java | 2 +- core/src/mindustry/mod/Mods.java | 2 +- 13 files changed, 150 insertions(+), 48 deletions(-) rename core/src/mindustry/entities/comp/{TileComp.java => BuildingComp.java} (99%) diff --git a/annotations/src/main/java/mindustry/annotations/Annotations.java b/annotations/src/main/java/mindustry/annotations/Annotations.java index 70f99f4f90..4ccfbf74be 100644 --- a/annotations/src/main/java/mindustry/annotations/Annotations.java +++ b/annotations/src/main/java/mindustry/annotations/Annotations.java @@ -50,6 +50,9 @@ public class Annotations{ @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Component{ + /** Whether to generate a base class for this components. + * An entity cannot have two base classes, so only one component can have base be true. */ + boolean base() default false; } /** Indicates that a method is implemented by the annotation processor. */ diff --git a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java index 46c4b92f73..710bef22ca 100644 --- a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java @@ -2,9 +2,9 @@ package mindustry.annotations; import arc.files.*; import arc.struct.*; -import arc.util.*; import arc.util.Log; import arc.util.Log.*; +import arc.util.*; import com.squareup.javapoet.*; import com.sun.source.util.*; import com.sun.tools.javac.model.*; @@ -22,9 +22,8 @@ import javax.tools.Diagnostic.*; import javax.tools.*; import java.io.*; import java.lang.annotation.*; -import java.lang.reflect.*; -import java.util.*; import java.util.List; +import java.util.*; @SupportedSourceVersion(SourceVersion.RELEASE_8) public abstract class BaseProcessor extends AbstractProcessor{ @@ -100,10 +99,16 @@ public abstract class BaseProcessor extends AbstractProcessor{ return str.contains(".") ? str.substring(str.lastIndexOf('.') + 1) : str; } - public static TypeName tname(String name) throws Exception{ - Constructor cons = TypeName.class.getDeclaredConstructor(String.class); - cons.setAccessible(true); - return cons.newInstance(name); + public static TypeName tname(String pack, String simple){ + return ClassName.get(pack, simple ); + } + + public static TypeName tname(String name){ + if(!name.contains(".")) return ClassName.get(packageName, name); + + String pack = name.substring(0, name.lastIndexOf(".")); + String simple = name.substring(name.lastIndexOf(".") + 1); + return ClassName.get(pack, simple); } public static TypeName tname(Class c){ diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java b/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java index 4ecad7faaf..7a552ead20 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java @@ -32,7 +32,7 @@ public class EntityIO{ MethodSpec.Builder method; ObjectSet presentFields = new ObjectSet<>(); - EntityIO(String name, TypeSpec.Builder type, ClassSerializer serializer, Fi directory){ + EntityIO(String name, TypeSpec.Builder type, Seq typeFields, ClassSerializer serializer, Fi directory){ this.directory = directory; this.type = type; this.serializer = serializer; @@ -49,7 +49,7 @@ public class EntityIO{ int nextRevision = revisions.isEmpty() ? 0 : revisions.max(r -> r.version).version + 1; //resolve preferred field order based on fields that fit - Seq fields = Seq.with(type.fieldSpecs).select(spec -> + Seq fields = typeFields.select(spec -> !spec.hasModifier(Modifier.TRANSIENT) && !spec.hasModifier(Modifier.STATIC) && !spec.hasModifier(Modifier.FINAL)/* && diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java index a9ae65c448..f0e224375a 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java @@ -38,10 +38,12 @@ public class EntityProcess extends BaseProcessor{ ObjectMap> defComponents = new ObjectMap<>(); ObjectMap varInitializers = new ObjectMap<>(); ObjectMap methodBlocks = new ObjectMap<>(); + ObjectMap> baseClassDeps = new ObjectMap<>(); ObjectSet imports = new ObjectSet<>(); Seq allGroups = new Seq<>(); Seq allDefs = new Seq<>(); Seq allInterfaces = new Seq<>(); + Seq baseClasses = new Seq<>(); ClassSerializer serializer; { @@ -151,12 +153,53 @@ public class EntityProcess extends BaseProcessor{ write(inter); + //generate base class if necessary + //SPECIAL CASE: components with EntityDefs don't get a base class! the generated class becomes the base class itself + if(component.annotation(Component.class).base()){ + + Seq deps = depends.copy().and(component); + baseClassDeps.get(component, ObjectSet::new).addAll(deps); + + //do not generate base classes when the component will generate one itself + if(!component.has(EntityDef.class)){ + TypeSpec.Builder base = TypeSpec.classBuilder(baseName(component)).addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT); + + //go through all the fields. + for(Stype type : deps){ + //add public fields + for(Svar field : type.fields().select(e -> !e.is(Modifier.STATIC) && !e.is(Modifier.PRIVATE) && !e.has(Import.class) && !e.has(ReadOnly.class))){ + FieldSpec.Builder builder = FieldSpec.builder(field.tname(),field.name(), Modifier.PUBLIC); + + //keep transience + if(field.is(Modifier.TRANSIENT)) builder.addModifiers(Modifier.TRANSIENT); + //keep all annotations + builder.addAnnotations(field.annotations().map(AnnotationSpec::get)); + + //add initializer if it exists + if(varInitializers.containsKey(field.descString())){ + builder.initializer(varInitializers.get(field.descString())); + } + + base.addField(builder.build()); + } + } + + //add interfaces + for(Stype type : depends){ + base.addSuperinterface(tname(packageName, interfaceName(type))); + } + + //add to queue to be written later + baseClasses.add(base); + } + } + //LOGGING Log.debug("&gGenerating interface for " + component.name()); for(TypeName tn : inter.superinterfaces){ - Log.debug("&g> &lbextends @", simpleName(tn.toString())); + Log.debug("&g> &lbimplements @", simpleName(tn.toString())); } //log methods generated @@ -173,7 +216,12 @@ public class EntityProcess extends BaseProcessor{ //this needs to be done before the entity interfaces are generated, as the entity classes need to know which groups to add themselves to for(Selement group : allGroups){ GroupDef an = group.annotation(GroupDef.class); - Seq types = types(an, GroupDef::value).map(this::interfaceToComp); + Seq types = types(an, GroupDef::value).map(stype -> { + Stype result = interfaceToComp(stype); + if(result == null) throw new IllegalArgumentException("Interface " + stype + " does not have an associated component!"); + return result; + }); + boolean collides = an.collide(); groupDefs.add(new GroupDefinition(group.name().startsWith("g") ? group.name().substring(1) : group.name(), ClassName.bestGuess(packageName + "." + interfaceName(types.first())), types, an.spatial(), an.mapping(), collides)); @@ -211,29 +259,45 @@ public class EntityProcess extends BaseProcessor{ TypeSpec.Builder builder = TypeSpec.classBuilder(name).addModifiers(Modifier.PUBLIC); if(isFinal) builder.addModifiers(Modifier.FINAL); + //all component classes (not interfaces) Seq components = allComponents(type); Seq groups = groupDefs.select(g -> (!g.components.isEmpty() && !g.components.contains(s -> !components.contains(s))) || g.manualInclusions.contains(type)); ObjectMap> methods = new ObjectMap<>(); ObjectMap specVariables = new ObjectMap<>(); ObjectSet usedFields = new ObjectSet<>(); + //make sure there's less than 2 base classes + Seq baseClasses = components.select(s -> s.annotation(Component.class).base()); + if(baseClasses.size > 2){ + err("No entity may have more than 2 base classes. Base classes: " + baseClasses, type); + } + + //get base class type name for extension + Stype baseClassType = baseClasses.any() ? baseClasses.first() : null; + @Nullable TypeName baseClass = baseClasses.any() ? tname(packageName + "." + baseName(baseClassType)) : null; + //whether the main class is the base itself + boolean typeIsBase = baseClassType != null && type.has(Component.class) && type.annotation(Component.class).base(); + //add serialize() boolean builder.addMethod(MethodSpec.methodBuilder("serialize").addModifiers(Modifier.PUBLIC, Modifier.FINAL).returns(boolean.class).addStatement("return " + ann.serialize()).build()); //all SyncField fields Seq syncedFields = new Seq<>(); Seq allFields = new Seq<>(); + Seq allFieldSpecs = new Seq<>(); boolean isSync = components.contains(s -> s.name().contains("Sync")); //add all components for(Stype comp : components){ + //whether this component's fields are defined in the base class + boolean isShadowed = baseClass != null && !typeIsBase && baseClassDeps.get(baseClassType).contains(comp); //write fields to the class; ignoring transient/imported ones Seq fields = comp.fields().select(f -> !f.has(Import.class)); for(Svar f : fields){ if(!usedFields.add(f.name())){ - err("Field '" + f.name() + "' of component '" + comp.name() + "' re-defines a field in entity '" + type.name() + "'"); + err("Field '" + f.name() + "' of component '" + comp.name() + "' redefines a field in entity '" + type.name() + "'"); continue; } @@ -255,9 +319,19 @@ public class EntityProcess extends BaseProcessor{ fbuilder.addModifiers(f.has(ReadOnly.class) ? Modifier.PROTECTED : Modifier.PUBLIC); fbuilder.addAnnotations(f.annotations().map(AnnotationSpec::get)); - builder.addField(fbuilder.build()); - specVariables.put(builder.fieldSpecs.get(builder.fieldSpecs.size() - 1), f); + FieldSpec spec = fbuilder.build(); + //whether this field would be added to the superclass + boolean isVisible = !f.is(Modifier.STATIC) && !f.is(Modifier.PRIVATE) && !f.has(ReadOnly.class); + + //add the field only if it isn't visible or it wasn't implemented by the base class + if(!isShadowed || !isVisible){ + builder.addField(spec); + } + + specVariables.put(spec, f); + + allFieldSpecs.add(spec); allFields.add(f); //add extra sync fields @@ -294,7 +368,7 @@ public class EntityProcess extends BaseProcessor{ .addModifiers(Modifier.PUBLIC) .addStatement("return $S + $L", name + "#", "id").build()); - EntityIO io = new EntityIO(type.name(), builder, serializer, rootDirectory.child("annotations/src/main/resources/revisions").child(name)); + EntityIO io = new EntityIO(type.name(), builder, allFieldSpecs, serializer, rootDirectory.child("annotations/src/main/resources/revisions").child(name)); //entities with no sync comp and no serialization gen no code boolean hasIO = ann.genio() && (components.contains(s -> s.name().contains("Sync")) || ann.serialize()); @@ -435,7 +509,7 @@ public class EntityProcess extends BaseProcessor{ builder.addSuperinterface(Poolable.class); //implement reset() MethodSpec.Builder resetBuilder = MethodSpec.methodBuilder("reset").addModifiers(Modifier.PUBLIC); - for(FieldSpec spec : builder.fieldSpecs){ + for(FieldSpec spec : allFieldSpecs){ @Nullable Svar variable = specVariables.get(spec); if(variable != null && variable.isAny(Modifier.STATIC, Modifier.FINAL)) continue; String desc = variable.descString(); @@ -462,7 +536,7 @@ public class EntityProcess extends BaseProcessor{ .returns(tname(packageName + "." + name)) .addStatement(ann.pooled() ? "return Pools.obtain($L.class, " +name +"::new)" : "return new $L()", name).build()); - definitions.add(new EntityDefinition(packageName + "." + name, builder, type, components, groups)); + definitions.add(new EntityDefinition(packageName + "." + name, builder, type, baseClass, components, groups, allFieldSpecs)); } //generate groups @@ -522,9 +596,9 @@ public class EntityProcess extends BaseProcessor{ int maxID = max == null ? 0 : max + 1; //assign IDs - definitions.sort(Structs.comparing(t -> t.base.toString())); + definitions.sort(Structs.comparing(t -> t.naming.toString())); for(EntityDefinition def : definitions){ - String name = def.base.fullName(); + String name = def.naming.fullName(); if(map.containsKey(name)){ def.classID = map.getInt(name); }else{ @@ -557,7 +631,7 @@ public class EntityProcess extends BaseProcessor{ for(EntityDefinition def : definitions){ //store mapping idStore.addStatement("idMap[$L] = $L::new", def.classID, def.name); - extraNames.get(def.base).each(extra -> { + extraNames.get(def.naming).each(extra -> { idStore.addStatement("nameMap.put($S, $L::new)", extra, def.name); if(!Strings.camelToKebab(extra).equals(extra)){ idStore.addStatement("nameMap.put($S, $L::new)", Strings.camelToKebab(extra), def.name); @@ -576,11 +650,21 @@ public class EntityProcess extends BaseProcessor{ }else{ //round 3: generate actual classes and implement interfaces + //write base classes + for(TypeSpec.Builder b : baseClasses){ + write(b, imports.asArray()); + } + //implement each definition for(EntityDefinition def : definitions){ ObjectSet methodNames = def.components.flatMap(type -> type.methods().map(Smethod::simpleString)).as().asSet(); + //add base class extension if it exists + if(def.extend != null){ + def.builder.superclass(def.extend); + } + //get interface for each component for(Stype comp : def.components){ @@ -596,7 +680,7 @@ public class EntityProcess extends BaseProcessor{ //generate getter/setter for each method for(Smethod method : inter.methods()){ String var = method.name(); - FieldSpec field = Seq.with(def.builder.fieldSpecs).find(f -> f.name.equals(var)); + FieldSpec field = Seq.with(def.fieldSpecs).find(f -> f.name.equals(var)); //make sure it's a real variable AND that the component doesn't already implement it somewhere with custom logic if(field == null || methodNames.contains(method.simpleString())) continue; @@ -681,12 +765,27 @@ public class EntityProcess extends BaseProcessor{ /** @return interface for a component type */ String interfaceName(Stype comp){ String suffix = "Comp"; - if(!comp.name().endsWith(suffix)){ - err("All components must have names that end with 'Comp'", comp.e); - } + if(!comp.name().endsWith(suffix)) err("All components must have names that end with 'Comp'", comp.e); + + //example: BlockComp -> IBlock return comp.name().substring(0, comp.name().length() - suffix.length()) + "c"; } + /** @return base class name for a component type */ + String baseName(Stype comp){ + String suffix = "Comp"; + if(!comp.name().endsWith(suffix)) err("All components must have names that end with 'Comp'", comp.e); + boolean isConcrete = comp.has(EntityDef.class); //concrete base implementations have no "Base" suffix + + return comp.name().substring(0, comp.name().length() - suffix.length()) + (isConcrete ? "" : "Base"); + } + + @Nullable Stype interfaceToComp(Stype type){ + //example: IBlock -> BlockComp + String name = type.name().substring(0, type.name().length() - 1) + "Comp"; + return componentNames.get(name); + } + /** @return all components that a entity def has */ Seq allComponents(Selement type){ if(!defComponents.containsKey(type)){ @@ -746,19 +845,10 @@ public class EntityProcess extends BaseProcessor{ return interfaceToComp(type) != null; } - @Nullable Stype interfaceToComp(Stype type){ - String name = type.name().substring(0, type.name().length() - 1) + "Comp"; - return componentNames.get(name); - } - String createName(Selement elem){ Seq comps = types(elem.annotation(EntityDef.class), EntityDef::value).map(this::interfaceToComp);; comps.sortComparing(Selement::name); - return comps.toString("", s -> s.name().replace("Comp", "")) + "Entity"; - } - - boolean isComponent(Stype type){ - return type.annotation(Component.class) != null; + return comps.toString("", s -> s.name().replace("Comp", "")); } Seq types(T t, Cons consumer){ @@ -795,17 +885,21 @@ public class EntityProcess extends BaseProcessor{ class EntityDefinition{ final Seq groups; final Seq components; + final Seq fieldSpecs; final TypeSpec.Builder builder; - final Selement base; + final Selement naming; final String name; + final @Nullable TypeName extend; int classID; - public EntityDefinition(String name, Builder builder, Selement base, Seq components, Seq groups){ + public EntityDefinition(String name, Builder builder, Selement naming, TypeName extend, Seq components, Seq groups, Seq fieldSpec){ this.builder = builder; this.name = name; - this.base = base; + this.naming = naming; this.groups = groups; this.components = components; + this.extend = extend; + this.fieldSpecs = fieldSpec; } @Override @@ -813,7 +907,7 @@ public class EntityProcess extends BaseProcessor{ return "Definition{" + "groups=" + groups + "components=" + components + - ", base=" + base + + ", base=" + naming + '}'; } } diff --git a/annotations/src/main/resources/classids.properties b/annotations/src/main/resources/classids.properties index 85e912ce07..9643661825 100644 --- a/annotations/src/main/resources/classids.properties +++ b/annotations/src/main/resources/classids.properties @@ -5,6 +5,7 @@ block=1 cix=2 draug=3 mace=4 +mindustry.entities.comp.BuildingComp=22 mindustry.entities.comp.BulletComp=5 mindustry.entities.comp.DecalComp=6 mindustry.entities.comp.EffectComp=7 diff --git a/build.gradle b/build.gradle index ce1b985c77..8411b695c1 100644 --- a/build.gradle +++ b/build.gradle @@ -183,7 +183,6 @@ project(":desktop"){ apply plugin: "java" compileJava.options.fork = true - compileJava.options.compilerArgs += ["-XDignore.symbol.file"] dependencies{ implementation project(":core") diff --git a/core/src/mindustry/ClientLauncher.java b/core/src/mindustry/ClientLauncher.java index 27be26e953..9c42f6d9ea 100644 --- a/core/src/mindustry/ClientLauncher.java +++ b/core/src/mindustry/ClientLauncher.java @@ -43,7 +43,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform //debug GL information Log.info("[GL] Version: @", graphics.getGLVersion()); Log.info("[GL] Max texture size: @", Gl.getInt(Gl.maxTextureSize)); - Log.info("[GL] OpenGL 3.0 context: @", gl30 != null); + Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2"); Log.info("[JAVA] Version: @", System.getProperty("java.version")); Time.setDeltaProvider(() -> { diff --git a/core/src/mindustry/entities/GroupDefs.java b/core/src/mindustry/entities/GroupDefs.java index f91be60928..4c477fdd91 100644 --- a/core/src/mindustry/entities/GroupDefs.java +++ b/core/src/mindustry/entities/GroupDefs.java @@ -8,7 +8,7 @@ class GroupDefs{ @GroupDef(value = Playerc.class, mapping = true) G player; @GroupDef(value = Bulletc.class, spatial = true, collide = true) G bullet; @GroupDef(value = Unitc.class, spatial = true, mapping = true) G unit; - @GroupDef(value = Tilec.class) G tile; + @GroupDef(value = Buildingc.class) G tile; @GroupDef(value = Syncc.class, mapping = true) G sync; @GroupDef(value = Drawc.class) G draw; @GroupDef(value = Weatherc.class) G weather; diff --git a/core/src/mindustry/entities/comp/TileComp.java b/core/src/mindustry/entities/comp/BuildingComp.java similarity index 99% rename from core/src/mindustry/entities/comp/TileComp.java rename to core/src/mindustry/entities/comp/BuildingComp.java index 5399ce9ab9..9e26d2eb06 100644 --- a/core/src/mindustry/entities/comp/TileComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -35,9 +35,9 @@ import mindustry.world.modules.*; import static mindustry.Vars.*; -@EntityDef(value = {Tilec.class}, isFinal = false, genio = false, serialize = false) -@Component -abstract class TileComp implements Posc, Teamc, Healthc, Tilec, Timerc, QuadTreeObject, Displayable{ +@EntityDef(value = {Buildingc.class}, isFinal = false, genio = false, serialize = false) +@Component(base = true) +abstract class BuildingComp implements Posc, Teamc, Healthc, Tilec, Timerc, QuadTreeObject, Displayable{ //region vars and initialization static final float timeToSleep = 60f * 1; static final ObjectSet tmpTiles = new ObjectSet<>(); diff --git a/core/src/mindustry/entities/comp/BulletComp.java b/core/src/mindustry/entities/comp/BulletComp.java index 934224b898..4dcb4f98b9 100644 --- a/core/src/mindustry/entities/comp/BulletComp.java +++ b/core/src/mindustry/entities/comp/BulletComp.java @@ -15,7 +15,7 @@ import mindustry.graphics.*; import static mindustry.Vars.*; @EntityDef(value = {Bulletc.class}, pooled = true, serialize = false) -@Component +@Component(base = true) abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Drawc, Shielderc, Ownerc, Velc, Bulletc, Timerc{ @Import Team team; @Import Entityc owner; diff --git a/core/src/mindustry/entities/comp/PlayerComp.java b/core/src/mindustry/entities/comp/PlayerComp.java index abd7a428c4..4bf2e1293a 100644 --- a/core/src/mindustry/entities/comp/PlayerComp.java +++ b/core/src/mindustry/entities/comp/PlayerComp.java @@ -27,7 +27,7 @@ import mindustry.world.blocks.storage.CoreBlock.*; import static mindustry.Vars.*; @EntityDef(value = {Playerc.class}, serialize = false) -@Component +@Component(base = true) abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Drawc{ static final float deathDelay = 30f; diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 0c9763a9bd..dd148abcc1 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -20,7 +20,7 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; -@Component +@Component(base = true) abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Boundedc, Syncc, Shieldc, Displayable{ @Import float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health; diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index 5f54e05ae9..7d00599884 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -69,7 +69,7 @@ public class Mods implements Loadable{ /** @return the loaded mod found by class, or null if not found. */ public @Nullable LoadedMod getMod(Class type){ - return mods.find(m -> m.enabled() && m.main != null && m.main.getClass() == type);//loaded.find(l -> l.mod != null && l.mod.getClass() == type); + return mods.find(m -> m.enabled() && m.main != null && m.main.getClass() == type); } /** Imports an external mod file.*/