diff --git a/annotations/src/main/java/mindustry/annotations/Annotations.java b/annotations/src/main/java/mindustry/annotations/Annotations.java index 8124930825..b30581b431 100644 --- a/annotations/src/main/java/mindustry/annotations/Annotations.java +++ b/annotations/src/main/java/mindustry/annotations/Annotations.java @@ -4,9 +4,28 @@ import java.lang.annotation.*; public class Annotations{ + /** Indicates an entity definition. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) - public @interface StyleDefaults { + public @interface EntityDef{ + Class[] value(); + } + + /** Indicates an internal interface for entity components. */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.SOURCE) + public @interface EntityInterface{ + } + + /** Indicates that a component logic method should be merged. */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.SOURCE) + public @interface Merge{ + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.SOURCE) + public @interface StyleDefaults{ } /** Indicates that a method should always call its super version. */ @@ -16,10 +35,10 @@ public class Annotations{ } - /** Annotation that allows overriding CallSuper annotation. To be used on method that overrides method with CallSuper annotation from parent class.*/ + /** Annotation that allows overriding CallSuper annotation. To be used on method that overrides method with CallSuper annotation from parent class. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) - public @interface OverrideCallSuper { + public @interface OverrideCallSuper{ } /** Marks a class as serializable. */ diff --git a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java index effbb72400..809882a8dd 100644 --- a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java @@ -1,9 +1,15 @@ package mindustry.annotations; +import arc.struct.*; +import com.squareup.javapoet.*; +import com.sun.source.util.*; +import mindustry.annotations.util.*; + import javax.annotation.processing.*; import javax.lang.model.*; import javax.lang.model.element.*; import javax.lang.model.util.*; +import java.lang.annotation.*; import java.util.*; @SupportedSourceVersion(SourceVersion.RELEASE_8) @@ -15,8 +21,11 @@ public abstract class BaseProcessor extends AbstractProcessor{ public static Elements elementu; public static Filer filer; public static Messager messager; + public static Trees trees; protected int round; + protected int rounds = 1; + protected RoundEnvironment env; public static String getMethodName(Element element){ return ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString() + "." + element.getSimpleName(); @@ -27,19 +36,40 @@ public abstract class BaseProcessor extends AbstractProcessor{ || type.equals("long") || type.equals("float") || type.equals("double") || type.equals("char"); } - @Override - public synchronized void init(ProcessingEnvironment processingEnv){ - super.init(processingEnv); + public static void write(TypeSpec.Builder builder) throws Exception{ + JavaFile.builder(packageName, builder.build()).build().writeTo(BaseProcessor.filer); + } - typeu = processingEnv.getTypeUtils(); - elementu = processingEnv.getElementUtils(); - filer = processingEnv.getFiler(); - messager = processingEnv.getMessager(); + public Array types(Class type){ + return Array.with(env.getElementsAnnotatedWith(type)).select(e -> e instanceof TypeElement) + .map(e -> new Stype((TypeElement)e)); + } + + public Array fields(Class type){ + return Array.with(env.getElementsAnnotatedWith(type)).select(e -> e instanceof VariableElement) + .map(e -> new Svar((VariableElement)e)); + } + + public Array methods(Class type){ + return Array.with(env.getElementsAnnotatedWith(type)).select(e -> e instanceof ExecutableElement) + .map(e -> new Smethod((ExecutableElement)e)); + } + + @Override + public synchronized void init(ProcessingEnvironment env){ + super.init(env); + + trees = Trees.instance(env); + typeu = env.getTypeUtils(); + elementu = env.getElementUtils(); + filer = env.getFiler(); + messager = env.getMessager(); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv){ - if(round++ != 0) return false; //only process 1 round + if(round++ >= rounds) return false; //only process 1 round + this.env = roundEnv; try{ process(roundEnv); }catch(Exception e){ diff --git a/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java new file mode 100644 index 0000000000..6ab0dcf1a8 --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java @@ -0,0 +1,183 @@ +package mindustry.annotations.impl; + +import arc.struct.*; +import arc.util.*; +import com.squareup.javapoet.*; +import com.squareup.javapoet.TypeSpec.*; +import com.sun.source.tree.*; +import mindustry.annotations.Annotations.*; +import mindustry.annotations.*; +import mindustry.annotations.util.*; + +import javax.annotation.processing.*; +import javax.lang.model.element.*; +import javax.lang.model.type.*; +import java.util.*; + +@SupportedAnnotationTypes({ +"mindustry.annotations.Annotations.EntityDef", +"mindustry.annotations.Annotations.EntityInterface" +}) +public class EntityProcess extends BaseProcessor{ + Array definitions = new Array<>(); + + { + rounds = 2; + } + + @Override + public void process(RoundEnvironment env) throws Exception{ + + //round 1: get component classes and generate interfaces for them + if(round == 1){ + Array allDefs = types(EntityDef.class); + + ObjectSet allComponents = new ObjectSet<>(); + + //find all components used... + for(Stype type : allDefs){ + allComponents.addAll(allComponents(type)); + } + + //create component interfaces + for(Stype component : allComponents){ + TypeSpec.Builder inter = TypeSpec.interfaceBuilder(component.name() + "c").addModifiers(Modifier.PUBLIC).addAnnotation(EntityInterface.class); + + for(Svar field : component.fields().select(e -> !e.is(Modifier.STATIC) && !e.is(Modifier.PRIVATE))){ + String cname = Strings.capitalize(field.name()); + //getter + inter.addMethod(MethodSpec.methodBuilder("get" + cname).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC).returns(field.tname()).build()); + //setter + inter.addMethod(MethodSpec.methodBuilder("set" + cname).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC).addParameter(field.tname(), field.name()).build()); + } + + write(inter); + } + + //look at each definition + for(Stype type : allDefs){ + String name = type.name().replace("Def", "Gen"); + TypeSpec.Builder builder = TypeSpec.classBuilder(name).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + Array components = allComponents(type); + ObjectMap> methods = new ObjectMap<>(); + + //add all components + for(Stype comp : components){ + + //write fields + Array fields = comp.fields(); + for(Svar f : fields){ + VariableTree tree = f.tree(); + FieldSpec.Builder fbuilder = FieldSpec.builder(f.tname(), f.name(), Modifier.PUBLIC); + //add initializer if it exists + if(tree.getInitializer() != null){ + fbuilder.initializer(tree.getInitializer().toString()); + } + builder.addField(fbuilder.build()); + } + + //get all utility methods from components + for(Smethod elem : comp.methods()){ + methods.getOr(elem.toString(), Array::new).add(elem); + } + } + + //add all methods from components + for(ObjectMap.Entry> entry : methods){ + //representative method + Smethod first = entry.value.first(); + //build method using same params/returns + MethodSpec.Builder mbuilder = MethodSpec.methodBuilder(first.name()).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + mbuilder.returns(TypeName.get(first.ret())); + for(Svar var : first.params()){ + mbuilder.addParameter(var.tname(), var.name()); + } + + boolean returns = !first.ret().toString().equals("void"); + + for(Smethod elem : entry.value){ + //wrap scope to prevent variable leakage + if(!returns) mbuilder.beginControlFlow(""); + + //get all statements in the method, copy them over + MethodTree methodTree = elem.tree(); + BlockTree blockTree = methodTree.getBody(); + for(StatementTree st : blockTree.getStatements()){ + String state = st.toString(); + mbuilder.addStatement(state.substring(0, state.length() - 1)); + } + + //end scope + if(!returns) mbuilder.endControlFlow(); + } + + builder.addMethod(mbuilder.build()); + } + + definitions.add(new Definition(builder, type)); + + } + }else{ + //round 2: generate actual classes and implement interfaces + Array interfaces = types(EntityInterface.class); + + //implement each definition + for(Definition def : definitions){ + Array components = allComponents(def.base); + + //get interface for each component + for(Stype comp : components){ + //implement the interface + Stype inter = interfaces.find(i -> i.name().equals(comp.name() + "c")); + def.builder.addSuperinterface(inter.tname()); + + //generate getter/setter for each method + for(Smethod method : inter.methods()){ + String var = Strings.camelize(method.name().substring(3)); + if(method.name().startsWith("get")){ + def.builder.addMethod(MethodSpec.overriding(method.e).addStatement("return " + var).build()); + }else if(method.name().startsWith("set")){ + def.builder.addMethod(MethodSpec.overriding(method.e).addStatement("this." + var + " = " + var).build()); + } + } + } + + write(def.builder); + } + } + } + + /** @return all components that a entity def has */ + Array allComponents(Stype type){ + ObjectSet mirrors = ObjectSet.with(mirrors(type)); + for(TypeMirror curr : mirrors){ + List sub = typeu.directSupertypes(curr); + while(!sub.isEmpty() && !sub.get(0).toString().equals("java.lang.Object")){ + mirrors.add(sub.get(0)); + curr = sub.get(0); + sub = typeu.directSupertypes(curr); + } + } + return mirrors.asArray().map(m -> new Stype((TypeElement)typeu.asElement(m))); + } + + TypeMirror[] mirrors(Stype type){ + try{ + type.annotation(EntityDef.class).value(); + }catch(MirroredTypesException e){ + return e.getTypeMirrors().toArray(new TypeMirror[0]); + } + throw new IllegalArgumentException("Missing components: " + type); + } + + class Definition{ + final TypeSpec.Builder builder; + final Stype base; + + public Definition(Builder builder, Stype base){ + this.builder = builder; + this.base = base; + } + } +} diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java index b54db3ee15..8abd0b82b8 100644 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java +++ b/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java @@ -42,96 +42,83 @@ public class RemoteProcess extends BaseProcessor{ //list of all method entries private ArrayList classes; + { + rounds = 2; + } + @Override - public boolean process(Set annotations, RoundEnvironment roundEnv){ - if(round > 1) return false; //only process 2 rounds + public void process(RoundEnvironment roundEnv) throws Exception{ + //round 1: find all annotations, generate *writers* + if(round == 1){ + //get serializers + serializers = new IOFinder().findSerializers(roundEnv); + //last method ID used + int lastMethodID = 0; + //find all elements with the Remote annotation + elements = roundEnv.getElementsAnnotatedWith(Remote.class); + //map of all classes to generate by name + classMap = new HashMap<>(); + //list of all method entries + methods = new ArrayList<>(); + //list of all method entries + classes = new ArrayList<>(); - round++; + List orderedElements = new ArrayList<>(elements); + orderedElements.sort(Comparator.comparing(Object::toString)); - try{ + //create methods + for(Element element : orderedElements){ + Remote annotation = element.getAnnotation(Remote.class); - //round 1: find all annotations, generate *writers* - if(round == 1){ - //get serializers - serializers = new IOFinder().findSerializers(roundEnv); - //last method ID used - int lastMethodID = 0; - //find all elements with the Remote annotation - elements = roundEnv.getElementsAnnotatedWith(Remote.class); - //map of all classes to generate by name - classMap = new HashMap<>(); - //list of all method entries - methods = new ArrayList<>(); - //list of all method entries - classes = new ArrayList<>(); - - List orderedElements = new ArrayList<>(elements); - orderedElements.sort(Comparator.comparing(Object::toString)); - - //create methods - for(Element element : orderedElements){ - Remote annotation = element.getAnnotation(Remote.class); - - //check for static - if(!element.getModifiers().contains(Modifier.STATIC) || !element.getModifiers().contains(Modifier.PUBLIC)){ - BaseProcessor.messager.printMessage(Kind.ERROR, "All @Remote methods must be public and static: ", element); - } - - //can't generate none methods - if(annotation.targets() == Loc.none){ - BaseProcessor.messager.printMessage(Kind.ERROR, "A @Remote method's targets() cannot be equal to 'none':", element); - } - - //get and create class entry if needed - if(!classMap.containsKey(callLocation)){ - ClassEntry clas = new ClassEntry(callLocation); - classMap.put(callLocation, clas); - classes.add(clas); - } - - ClassEntry entry = classMap.get(callLocation); - - //create and add entry - MethodEntry method = new MethodEntry(entry.name, BaseProcessor.getMethodName(element), annotation.targets(), annotation.variants(), - annotation.called(), annotation.unreliable(), annotation.forward(), lastMethodID++, (ExecutableElement)element, annotation.priority()); - - entry.methods.add(method); - methods.add(method); + //check for static + if(!element.getModifiers().contains(Modifier.STATIC) || !element.getModifiers().contains(Modifier.PUBLIC)){ + BaseProcessor.messager.printMessage(Kind.ERROR, "All @Remote methods must be public and static: ", element); } - //create read/write generators - RemoteWriteGenerator writegen = new RemoteWriteGenerator(serializers); + //can't generate none methods + if(annotation.targets() == Loc.none){ + BaseProcessor.messager.printMessage(Kind.ERROR, "A @Remote method's targets() cannot be equal to 'none':", element); + } - //generate the methods to invoke (write) - writegen.generateFor(classes, packageName); + //get and create class entry if needed + if(!classMap.containsKey(callLocation)){ + ClassEntry clas = new ClassEntry(callLocation); + classMap.put(callLocation, clas); + classes.add(clas); + } - return true; - }else if(round == 2){ //round 2: generate all *readers* - RemoteReadGenerator readgen = new RemoteReadGenerator(serializers); + ClassEntry entry = classMap.get(callLocation); - //generate server readers - readgen.generateFor(methods.stream().filter(method -> method.where.isClient).collect(Collectors.toList()), readServerName, packageName, true); - //generate client readers - readgen.generateFor(methods.stream().filter(method -> method.where.isServer).collect(Collectors.toList()), readClientName, packageName, false); + //create and add entry + MethodEntry method = new MethodEntry(entry.name, BaseProcessor.getMethodName(element), annotation.targets(), annotation.variants(), + annotation.called(), annotation.unreliable(), annotation.forward(), lastMethodID++, (ExecutableElement)element, annotation.priority()); - //create class for storing unique method hash - TypeSpec.Builder hashBuilder = TypeSpec.classBuilder("MethodHash").addModifiers(Modifier.PUBLIC); - hashBuilder.addJavadoc(autogenWarning); - hashBuilder.addField(FieldSpec.builder(int.class, "HASH", Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL) - .initializer("$1L", Objects.hash(methods)).build()); - - //build and write resulting hash class - TypeSpec spec = hashBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); - - return true; + entry.methods.add(method); + methods.add(method); } - }catch(Exception e){ - e.printStackTrace(); - throw new RuntimeException(e); - } + //create read/write generators + RemoteWriteGenerator writegen = new RemoteWriteGenerator(serializers); - return false; + //generate the methods to invoke (write) + writegen.generateFor(classes, packageName); + }else if(round == 2){ //round 2: generate all *readers* + RemoteReadGenerator readgen = new RemoteReadGenerator(serializers); + + //generate server readers + readgen.generateFor(methods.stream().filter(method -> method.where.isClient).collect(Collectors.toList()), readServerName, packageName, true); + //generate client readers + readgen.generateFor(methods.stream().filter(method -> method.where.isServer).collect(Collectors.toList()), readClientName, packageName, false); + + //create class for storing unique method hash + TypeSpec.Builder hashBuilder = TypeSpec.classBuilder("MethodHash").addModifiers(Modifier.PUBLIC); + hashBuilder.addJavadoc(autogenWarning); + hashBuilder.addField(FieldSpec.builder(int.class, "HASH", Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL) + .initializer("$1L", Objects.hash(methods)).build()); + + //build and write resulting hash class + TypeSpec spec = hashBuilder.build(); + JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); + } } } diff --git a/annotations/src/main/java/mindustry/annotations/util/Selement.java b/annotations/src/main/java/mindustry/annotations/util/Selement.java new file mode 100644 index 0000000000..aca793cc72 --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/util/Selement.java @@ -0,0 +1,36 @@ +package mindustry.annotations.util; + +import com.squareup.javapoet.*; +import mindustry.annotations.*; + +import javax.lang.model.element.*; +import javax.lang.model.type.*; + +public class Selement{ + public final T e; + + public Selement(T e){ + this.e = e; + } + + public TypeMirror mirror(){ + return e.asType(); + } + + public TypeName tname(){ + return TypeName.get(mirror()); + } + + public ClassName cname(){ + return ClassName.get((TypeElement)BaseProcessor.typeu.asElement(mirror())); + } + + public String name(){ + return e.getSimpleName().toString(); + } + + @Override + public String toString(){ + return e.toString(); + } +} diff --git a/annotations/src/main/java/mindustry/annotations/util/Smethod.java b/annotations/src/main/java/mindustry/annotations/util/Smethod.java new file mode 100644 index 0000000000..b21a25a040 --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/util/Smethod.java @@ -0,0 +1,27 @@ +package mindustry.annotations.util; + +import arc.struct.*; +import com.sun.source.tree.*; +import mindustry.annotations.*; + +import javax.lang.model.element.*; +import javax.lang.model.type.*; + +public class Smethod extends Selement{ + + public Smethod(ExecutableElement executableElement){ + super(executableElement); + } + + public Array params(){ + return Array.with(e.getParameters()).map(Svar::new); + } + + public TypeMirror ret(){ + return e.getReturnType(); + } + + public MethodTree tree(){ + return BaseProcessor.trees.getTree(e); + } +} diff --git a/annotations/src/main/java/mindustry/annotations/util/Stype.java b/annotations/src/main/java/mindustry/annotations/util/Stype.java new file mode 100644 index 0000000000..5fe641afd3 --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/util/Stype.java @@ -0,0 +1,37 @@ +package mindustry.annotations.util; + +import arc.struct.*; + +import javax.lang.model.element.*; +import javax.lang.model.type.*; +import java.lang.annotation.*; + +public class Stype extends Selement{ + + public Stype(TypeElement typeElement){ + super(typeElement); + } + + public A annotation(Class annotation){ + return e.getAnnotation(annotation); + } + + public Array fields(){ + return Array.with(e.getEnclosedElements()).select(e -> e instanceof VariableElement).map(e -> new Svar((VariableElement)e)); + } + + public Array methods(){ + return Array.with(e.getEnclosedElements()).select(e -> e instanceof ExecutableElement + && !e.getSimpleName().toString().contains("<")).map(e -> new Smethod((ExecutableElement)e)); + } + + public Array constructors(){ + return Array.with(e.getEnclosedElements()).select(e -> e instanceof ExecutableElement + && e.getSimpleName().toString().contains("<")).map(e -> new Smethod((ExecutableElement)e)); + } + + @Override + public TypeMirror mirror(){ + return e.asType(); + } +} diff --git a/annotations/src/main/java/mindustry/annotations/util/Svar.java b/annotations/src/main/java/mindustry/annotations/util/Svar.java new file mode 100644 index 0000000000..cf58d7a6f5 --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/util/Svar.java @@ -0,0 +1,21 @@ +package mindustry.annotations.util; + +import com.sun.source.tree.*; +import mindustry.annotations.*; + +import javax.lang.model.element.*; + +public class Svar extends Selement{ + + public Svar(VariableElement e){ + super(e); + } + + public boolean is(Modifier mod){ + return e.getModifiers().contains(mod); + } + + public VariableTree tree(){ + return (VariableTree)BaseProcessor.trees.getTree(e); + } +} diff --git a/core/src/mindustry/entities/def/EntityDefs.java b/core/src/mindustry/entities/def/EntityDefs.java new file mode 100644 index 0000000000..55c4d3cd3b --- /dev/null +++ b/core/src/mindustry/entities/def/EntityDefs.java @@ -0,0 +1,48 @@ +package mindustry.entities.def; + +import arc.math.geom.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.units.*; +import mindustry.net.*; + +public class EntityDefs{ + + @EntityDef({Health.class, Vel.class, Status.class, Connection.class}) + class PlayerDef{} + + class Health{ + float health, maxHealth; + boolean dead; + + float healthf(){ + return health / maxHealth; + } + } + + class Pos{ + float x, y; + } + + class Vel extends Pos{ + Vec2 vel = new Vec2(); + + void update(){ + x += vel.x; + y += vel.y; + vel.scl(0.9f); + } + } + + class Status{ + Statuses statuses = new Statuses(); + + void update(){ + statuses.update(null); + } + } + + class Connection{ + NetConnection connection; + } + +}