Core annotation system finished
This commit is contained in:
@@ -8,52 +8,30 @@ import java.lang.annotation.Target;
|
|||||||
/**
|
/**
|
||||||
* Goal: To create a system to send events to the server from the client and vice versa, without creating a new packet type each time.<br>
|
* Goal: To create a system to send events to the server from the client and vice versa, without creating a new packet type each time.<br>
|
||||||
* These events may optionally also trigger on the caller client/server as well.<br>
|
* These events may optionally also trigger on the caller client/server as well.<br>
|
||||||
*<br>
|
|
||||||
* Three annotations are used for this purpose.<br>
|
|
||||||
* {@link RemoteClient}: Marks a method as able to be invoked remotely on a client from a server.<br>
|
|
||||||
* {@link RemoteServer}: Marks a method as able to be invoked remotely on a server from a client.<br>
|
|
||||||
* {@link Local}: Makes this method get invoked locally as well as remotely.<br>
|
|
||||||
*<br>
|
|
||||||
* All RemoteClient methods are put in the class io.anuke.mindustry.gen.CallClient.<br>
|
|
||||||
* All RemoteServer methods are put in the class io.anuke.mindustry.gen.CallServer.<br>
|
|
||||||
*/
|
*/
|
||||||
public class Annotations {
|
public class Annotations {
|
||||||
|
|
||||||
/**Marks a method as invokable remotely from a server on a client.*/
|
/**Marks a method as invokable remotely from a server on a client.*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
@Retention(RetentionPolicy.CLASS)
|
@Retention(RetentionPolicy.CLASS)
|
||||||
public @interface RemoteClient {
|
public @interface Remote {
|
||||||
/**Whether a client-specific method is generated that accepts a connecton ID and sends to only one player. Default is false.*/
|
/**Whether this method can be invoked on remote clients.*/
|
||||||
|
boolean client() default true;
|
||||||
|
/**Whether this method can be invoked on the remote server.*/
|
||||||
|
boolean server() default false;
|
||||||
|
/**Whether a client-specific method is generated that accepts a connecton ID and sends to only one player. Default is false.
|
||||||
|
* Only affects client methods.*/
|
||||||
boolean one() default false;
|
boolean one() default false;
|
||||||
/**Whether a 'global' method is generated that sends the event to all players. Default is true.*/
|
/**Whether a 'global' method is generated that sends the event to all players. Default is true.
|
||||||
|
* Only affects client methods.*/
|
||||||
boolean all() default true;
|
boolean all() default true;
|
||||||
}
|
/**Whether this method is invoked locally as well as remotely.*/
|
||||||
|
boolean local() default true;
|
||||||
/**Marks a method as invokable remotely from a client on a server.
|
/**Whether the packet for this method is sent with UDP instead of TCP.
|
||||||
* All RemoteServer methods must have their first formal parameter be of type Player.
|
* UDP is faster, but is prone to packet loss and duplication.*/
|
||||||
* This player is the invoker of the method.*/
|
boolean unreliable() default false;
|
||||||
@Target(ElementType.METHOD)
|
/**The simple class name where this method is placed.*/
|
||||||
@Retention(RetentionPolicy.CLASS)
|
String target() default "Call";
|
||||||
public @interface RemoteServer {}
|
|
||||||
|
|
||||||
/**Marks a method to be locally invoked as well as remotely invoked on the caller
|
|
||||||
* Must be used with {@link RemoteClient}/{@link RemoteServer} annotations.*/
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.CLASS)
|
|
||||||
public @interface Local{}
|
|
||||||
|
|
||||||
/**Marks a method to be invoked unreliably, e.g. with UDP instead of TCP.
|
|
||||||
* This is faster, but is prone to packet loss and duplication.*/
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.CLASS)
|
|
||||||
public @interface Unreliable{}
|
|
||||||
|
|
||||||
/**Specifies that this method will be placed in the class specified by its value.
|
|
||||||
* Only use constants for this value!*/ //TODO enforce this
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.CLASS)
|
|
||||||
public @interface In{
|
|
||||||
String value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Specifies that this method will be used to write classes of the type returned by {@link #value()}.<br>
|
/**Specifies that this method will be used to write classes of the type returned by {@link #value()}.<br>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import java.util.stream.Stream;
|
|||||||
public class IOFinder {
|
public class IOFinder {
|
||||||
|
|
||||||
/**Finds all class serializers for all types and returns them. Logs errors when necessary.
|
/**Finds all class serializers for all types and returns them. Logs errors when necessary.
|
||||||
* Maps full class names to their serializers.*/
|
* Maps fully qualified class names to their serializers.*/
|
||||||
public HashMap<String, ClassSerializer> findSerializers(RoundEnvironment env){
|
public HashMap<String, ClassSerializer> findSerializers(RoundEnvironment env){
|
||||||
HashMap<String, ClassSerializer> result = new HashMap<>();
|
HashMap<String, ClassSerializer> result = new HashMap<>();
|
||||||
|
|
||||||
@@ -44,16 +44,12 @@ public class IOFinder {
|
|||||||
Element reader = stream.findFirst().get();
|
Element reader = stream.findFirst().get();
|
||||||
|
|
||||||
//add to result list
|
//add to result list
|
||||||
result.put(type.getName(), new ClassSerializer(getFullMethod(reader), getFullMethod(writer), type.getName()));
|
result.put(type.getName(), new ClassSerializer(Utils.getMethodName(reader), Utils.getMethodName(writer), type.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFullMethod(Element element){
|
|
||||||
return element.getEnclosingElement().asType().toString() + "." + element.getSimpleName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**Information about read/write methods for a specific class type.*/
|
/**Information about read/write methods for a specific class type.*/
|
||||||
public static class ClassSerializer{
|
public static class ClassSerializer{
|
||||||
/**Fully qualified method name of the reader.*/
|
/**Fully qualified method name of the reader.*/
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package io.anuke.annotations;
|
package io.anuke.annotations;
|
||||||
|
|
||||||
|
import javax.lang.model.element.ExecutableElement;
|
||||||
|
|
||||||
/**Class that repesents a remote method to be constructed and put into a class.*/
|
/**Class that repesents a remote method to be constructed and put into a class.*/
|
||||||
public class MethodEntry {
|
public class MethodEntry {
|
||||||
/**Simple target class name.*/
|
/**Simple target class name.*/
|
||||||
@@ -11,13 +13,31 @@ public class MethodEntry {
|
|||||||
/**Whether an additional 'one' and 'all' method variant is generated. At least one of these must be true.
|
/**Whether an additional 'one' and 'all' method variant is generated. At least one of these must be true.
|
||||||
* Only applicable to client (server-invoked) methods.*/
|
* Only applicable to client (server-invoked) methods.*/
|
||||||
public final boolean allVariant, oneVariant;
|
public final boolean allVariant, oneVariant;
|
||||||
|
/**Whether this method is called locally as well as remotely.*/
|
||||||
|
public final boolean local;
|
||||||
|
/**Whether this method is unreliable and uses UDP.*/
|
||||||
|
public final boolean unreliable;
|
||||||
|
/**Unique method ID.*/
|
||||||
|
public final int id;
|
||||||
|
/**The element method associated with this entry.*/
|
||||||
|
public final ExecutableElement element;
|
||||||
|
|
||||||
public MethodEntry(String className, String targetMethod, boolean client, boolean server, boolean allVariant, boolean oneVariant) {
|
public MethodEntry(String className, String targetMethod, boolean client, boolean server,
|
||||||
|
boolean allVariant, boolean oneVariant, boolean local, boolean unreliable, int id, ExecutableElement element) {
|
||||||
this.className = className;
|
this.className = className;
|
||||||
this.targetMethod = targetMethod;
|
this.targetMethod = targetMethod;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.allVariant = allVariant;
|
this.allVariant = allVariant;
|
||||||
this.oneVariant = oneVariant;
|
this.oneVariant = oneVariant;
|
||||||
|
this.local = local;
|
||||||
|
this.id = id;
|
||||||
|
this.element = element;
|
||||||
|
this.unreliable = unreliable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return targetMethod.hashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
package io.anuke.annotations;
|
package io.anuke.annotations;
|
||||||
|
|
||||||
import com.squareup.javapoet.*;
|
import com.squareup.javapoet.FieldSpec;
|
||||||
import io.anuke.annotations.Annotations.Local;
|
import com.squareup.javapoet.JavaFile;
|
||||||
import io.anuke.annotations.Annotations.RemoteClient;
|
import com.squareup.javapoet.TypeSpec;
|
||||||
import io.anuke.annotations.Annotations.RemoteServer;
|
import io.anuke.annotations.Annotations.Remote;
|
||||||
import io.anuke.annotations.Annotations.Unreliable;
|
|
||||||
import io.anuke.annotations.IOFinder.ClassSerializer;
|
import io.anuke.annotations.IOFinder.ClassSerializer;
|
||||||
|
|
||||||
import javax.annotation.processing.*;
|
import javax.annotation.processing.*;
|
||||||
import javax.lang.model.SourceVersion;
|
import javax.lang.model.SourceVersion;
|
||||||
import javax.lang.model.element.*;
|
import javax.lang.model.element.Element;
|
||||||
import javax.lang.model.util.Elements;
|
import javax.lang.model.element.ExecutableElement;
|
||||||
import javax.lang.model.util.Types;
|
import javax.lang.model.element.Modifier;
|
||||||
|
import javax.lang.model.element.TypeElement;
|
||||||
import javax.tools.Diagnostic.Kind;
|
import javax.tools.Diagnostic.Kind;
|
||||||
import java.lang.annotation.Annotation;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
//TODO document
|
//TODO document
|
||||||
//TODO split up into more classes
|
//TODO split up into more classes
|
||||||
@@ -42,12 +41,15 @@ import java.util.Set;
|
|||||||
})
|
})
|
||||||
public class RemoteMethodAnnotationProcessor extends AbstractProcessor {
|
public class RemoteMethodAnnotationProcessor extends AbstractProcessor {
|
||||||
/**Maximum size of each event packet.*/
|
/**Maximum size of each event packet.*/
|
||||||
private static final int maxPacketSize = 512;
|
public static final int maxPacketSize = 512;
|
||||||
/**Name of the base package to put all the generated classes.*/
|
/**Name of the base package to put all the generated classes.*/
|
||||||
private static final String packageClassName = "io.anuke.mindustry.gen";
|
private static final String packageName = "io.anuke.mindustry.gen";
|
||||||
|
|
||||||
|
/**Name of class that handles reading and invoking packets on the server.*/
|
||||||
|
private static final String readServerName = "RemoteReadServer";
|
||||||
|
/**Name of class that handles reading and invoking packets on the client.*/
|
||||||
|
private static final String readClientName = "RemoteReadClient";
|
||||||
|
|
||||||
/**Maps fully qualified class names to serializers.*/
|
|
||||||
private HashMap<String, ClassSerializer> serializers;
|
|
||||||
/**Whether the initial round is done.*/
|
/**Whether the initial round is done.*/
|
||||||
private boolean done;
|
private boolean done;
|
||||||
|
|
||||||
@@ -63,203 +65,87 @@ public class RemoteMethodAnnotationProcessor extends AbstractProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||||
if(done) return false;
|
if(done) return false; //only process 1 round
|
||||||
done = true;
|
done = true;
|
||||||
|
|
||||||
serializers = new IOFinder().findSerializers(roundEnv);
|
|
||||||
|
|
||||||
writeElements(roundEnv, clientFullClassName, RemoteClient.class);
|
|
||||||
writeElements(roundEnv, serverFullClassName, RemoteServer.class);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeElements(RoundEnvironment env){
|
|
||||||
try {
|
try {
|
||||||
boolean client = annotation == RemoteServer.class;
|
|
||||||
String className = fullClassName.substring(1 + fullClassName.lastIndexOf('.'));
|
|
||||||
String packageName = fullClassName.substring(0, fullClassName.lastIndexOf('.'));
|
|
||||||
|
|
||||||
Constructor<TypeName> cons = TypeName.class.getDeclaredConstructor(String.class);
|
//get serializers
|
||||||
cons.setAccessible(true);
|
HashMap<String, ClassSerializer> serializers = new IOFinder().findSerializers(roundEnv);
|
||||||
|
|
||||||
TypeName playerType = cons.newInstance("io.anuke.mindustry.entities.Player");
|
//last method ID used
|
||||||
|
int lastMethodID = 0;
|
||||||
|
//find all elements with the Remote annotation
|
||||||
|
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Remote.class);
|
||||||
|
//map of all classes to generate by name
|
||||||
|
HashMap<String, ClassEntry> classMap = new HashMap<>();
|
||||||
|
//list of all method entries
|
||||||
|
ArrayList<MethodEntry> methods = new ArrayList<>();
|
||||||
|
//list of all method entries
|
||||||
|
ArrayList<ClassEntry> classes = new ArrayList<>();
|
||||||
|
|
||||||
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC);
|
//create methods
|
||||||
|
for (Element element : elements) {
|
||||||
|
Remote annotation = element.getAnnotation(Remote.class);
|
||||||
|
|
||||||
int id = 0;
|
//check for static
|
||||||
|
if(!element.getModifiers().contains(Modifier.STATIC)) {
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "All Remote methods must be static: ", element);
|
||||||
|
}
|
||||||
|
|
||||||
classBuilder.addField(FieldSpec.builder(ByteBuffer.class, "TEMP_BUFFER", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL)
|
//get and create class entry if needed
|
||||||
.initializer("ByteBuffer.allocate($1L)", maxPacketSize).build());
|
if (!classMap.containsKey(annotation.target())) {
|
||||||
|
ClassEntry clas = new ClassEntry(annotation.target());
|
||||||
|
classMap.put(annotation.target(), clas);
|
||||||
|
classes.add(clas);
|
||||||
|
}
|
||||||
|
|
||||||
MethodSpec.Builder readMethod = MethodSpec.methodBuilder("readPacket")
|
ClassEntry entry = classMap.get(annotation.target());
|
||||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
|
||||||
.addParameter(ByteBuffer.class, "buffer")
|
|
||||||
.addParameter(int.class, "id")
|
|
||||||
.returns(void.class);
|
|
||||||
|
|
||||||
if(client){
|
//make sure that each server annotation has at least one method to generate, otherwise throw an error
|
||||||
readMethod.addParameter(playerType, "player");
|
if (annotation.server() && !annotation.all() && !annotation.one()) {
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "A client method must not have all() and one() both be false!", element);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotation.server() && annotation.client()) {
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "A method cannot be client and server simulatenously!", element);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//create and add entry
|
||||||
|
MethodEntry method = new MethodEntry(entry.name, Utils.getMethodName(element), annotation.client(), annotation.server(),
|
||||||
|
annotation.all(), annotation.one(), annotation.local(), annotation.unreliable(), lastMethodID ++, (ExecutableElement)element);
|
||||||
|
|
||||||
|
entry.methods.add(method);
|
||||||
|
methods.add(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeBlock.Builder writeSwitch = CodeBlock.builder();
|
//create read/write generators
|
||||||
boolean started = false;
|
RemoteReadGenerator readgen = new RemoteReadGenerator(serializers);
|
||||||
|
RemoteWriteGenerator writegen = new RemoteWriteGenerator(serializers);
|
||||||
|
|
||||||
readMethod.addJavadoc("This method reads and executes a method by ID. For internal use only!");
|
//generate client readers
|
||||||
|
readgen.generateFor(methods.stream().filter(method -> method.client).collect(Collectors.toList()), readClientName, packageName, false);
|
||||||
|
//generate server readers
|
||||||
|
readgen.generateFor(methods.stream().filter(method -> method.server).collect(Collectors.toList()), readServerName, packageName, true);
|
||||||
|
|
||||||
for (Element e : env.getElementsAnnotatedWith(annotation)) {
|
//generate the methods to invoke (write)
|
||||||
if(!e.getModifiers().contains(Modifier.STATIC)) {
|
writegen.generateFor(classes, packageName);
|
||||||
messager.printMessage(Kind.ERROR, "All local/remote methods must be static: ", e);
|
|
||||||
}else if(e.getKind() != ElementKind.METHOD){
|
|
||||||
messager.printMessage(Kind.ERROR, "All local/remote annotations must be on methods: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(e.getAnnotation(annotation) == null) continue;
|
//create class for storing unique method hash
|
||||||
boolean local = e.getAnnotation(Local.class) != null;
|
TypeSpec.Builder hashBuilder = TypeSpec.classBuilder("MethodHash").addModifiers(Modifier.PUBLIC);
|
||||||
boolean unreliable = e.getAnnotation(Unreliable.class) != null;
|
hashBuilder.addField(FieldSpec.builder(int.class, "HASH", Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL)
|
||||||
|
.initializer("$1L)", Objects.hash(methods)).build());
|
||||||
|
|
||||||
ExecutableElement exec = (ExecutableElement)e;
|
//build and write resulting hash class
|
||||||
|
TypeSpec spec = hashBuilder.build();
|
||||||
MethodSpec.Builder method = MethodSpec.methodBuilder(e.getSimpleName().toString())
|
JavaFile.builder(packageName, spec).build().writeTo(Utils.filer);
|
||||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
|
||||||
.returns(void.class);
|
|
||||||
|
|
||||||
if(client){
|
|
||||||
if(exec.getParameters().isEmpty()){
|
|
||||||
messager.printMessage(Kind.ERROR, "Client invoke methods must have a first parameter of type Player.", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VariableElement var = exec.getParameters().get(0);
|
|
||||||
|
|
||||||
if(!var.asType().toString().equals("io.anuke.mindustry.entities.Player")){
|
|
||||||
messager.printMessage(Kind.ERROR, "Client invoke methods should have a first parameter of type Player.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(VariableElement var : exec.getParameters()){
|
|
||||||
method.addParameter(TypeName.get(var.asType()), var.getSimpleName().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(local){
|
|
||||||
int index = 0;
|
|
||||||
StringBuilder results = new StringBuilder();
|
|
||||||
for(VariableElement var : exec.getParameters()){
|
|
||||||
results.append(var.getSimpleName());
|
|
||||||
if(index != exec.getParameters().size() - 1) results.append(", ");
|
|
||||||
index ++;
|
|
||||||
}
|
|
||||||
|
|
||||||
method.addStatement("$N." + exec.getSimpleName() + "(" + results.toString() + ")",
|
|
||||||
((TypeElement)e.getEnclosingElement()).getQualifiedName().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!started){
|
|
||||||
writeSwitch.beginControlFlow("if(id == "+id+")");
|
|
||||||
}else{
|
|
||||||
writeSwitch.nextControlFlow("else if(id == "+id+")");
|
|
||||||
}
|
|
||||||
started = true;
|
|
||||||
|
|
||||||
method.addStatement("$1N packet = $2N.obtain($1N.class)", "io.anuke.mindustry.net.Packets.InvokePacket",
|
|
||||||
"com.badlogic.gdx.utils.Pools");
|
|
||||||
method.addStatement("packet.writeBuffer = TEMP_BUFFER");
|
|
||||||
method.addStatement("TEMP_BUFFER.position(0)");
|
|
||||||
|
|
||||||
ArrayList<VariableElement> parameters = new ArrayList<>(exec.getParameters());
|
|
||||||
if(client){
|
|
||||||
parameters.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(VariableElement var : parameters){
|
|
||||||
String varName = var.getSimpleName().toString();
|
|
||||||
String typeName = var.asType().toString();
|
|
||||||
String bufferName = "TEMP_BUFFER";
|
|
||||||
String simpleTypeName = typeName.contains(".") ? typeName.substring(1 + typeName.lastIndexOf('.')) : typeName;
|
|
||||||
String capName = simpleTypeName.equals("byte") ? "" : Character.toUpperCase(simpleTypeName.charAt(0)) + simpleTypeName.substring(1);
|
|
||||||
|
|
||||||
boolean isEnum = typeUtils.directSupertypes(var.asType()).size() > 0
|
|
||||||
&& typeUtils.asElement(typeUtils.directSupertypes(var.asType()).get(0)).getSimpleName().equals("java.lang.Enum");
|
|
||||||
|
|
||||||
if(isEnum) {
|
|
||||||
method.addStatement(bufferName + ".putShort((short)" + varName + ".ordinal())");
|
|
||||||
}else if(isPrimitive(typeName)) {
|
|
||||||
if(simpleTypeName.equals("boolean")){
|
|
||||||
method.addStatement(bufferName + ".put(" + varName + " ? (byte)1 : 0)");
|
|
||||||
}else{
|
|
||||||
method.addStatement(bufferName + ".put" +
|
|
||||||
capName + "(" + varName + ")");
|
|
||||||
}
|
|
||||||
}else if(writeMap.get(simpleTypeName) != null){
|
|
||||||
String[] values = writeMap.get(simpleTypeName)[0];
|
|
||||||
for(String str : values){
|
|
||||||
method.addStatement(str.replaceAll("rbuffer", bufferName)
|
|
||||||
.replaceAll("rvalue", varName));
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
messager.printMessage(Kind.ERROR, "No method for writing type: " + typeName, var);
|
|
||||||
}
|
|
||||||
|
|
||||||
String writeBufferName = "buffer";
|
|
||||||
|
|
||||||
if(isEnum) {
|
|
||||||
writeSwitch.addStatement(typeName + " " + varName + " = " + typeName + ".values()["+writeBufferName +".getShort()]");
|
|
||||||
}else if(isPrimitive(typeName)) {
|
|
||||||
if(simpleTypeName.equals("boolean")){
|
|
||||||
writeSwitch.addStatement("boolean " + varName + " = " + writeBufferName + ".get() == 1");
|
|
||||||
}else{
|
|
||||||
writeSwitch.addStatement(typeName + " " + varName + " = " + writeBufferName + ".get" + capName + "()");
|
|
||||||
}
|
|
||||||
}else if(writeMap.get(simpleTypeName) != null){
|
|
||||||
String[] values = writeMap.get(simpleTypeName)[1];
|
|
||||||
for(String str : values){
|
|
||||||
writeSwitch.addStatement(str.replaceAll("rbuffer", writeBufferName)
|
|
||||||
.replaceAll("rvalue", varName)
|
|
||||||
.replaceAll("rtype", simpleTypeName));
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
messager.printMessage(Kind.ERROR, "No method for writing type: " + typeName, var);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
method.addStatement("packet.writeLength = TEMP_BUFFER.position()");
|
|
||||||
method.addStatement("io.anuke.mindustry.net.Net.send(packet, "+
|
|
||||||
(unreliable ? "io.anuke.mindustry.net.Net.SendMode.udp" : "io.anuke.mindustry.net.Net.SendMode.tcp")+")");
|
|
||||||
|
|
||||||
classBuilder.addMethod(method.build());
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
StringBuilder results = new StringBuilder();
|
|
||||||
|
|
||||||
for(VariableElement writevar : exec.getParameters()){
|
|
||||||
results.append(writevar.getSimpleName());
|
|
||||||
if(index != exec.getParameters().size() - 1) results.append(", ");
|
|
||||||
index ++;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSwitch.addStatement("com.badlogic.gdx.Gdx.app.postRunnable(() -> $N." + exec.getSimpleName() + "(" + results.toString() + "))",
|
|
||||||
((TypeElement)e.getEnclosingElement()).getQualifiedName().toString());
|
|
||||||
|
|
||||||
id ++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(started){
|
|
||||||
writeSwitch.endControlFlow();
|
|
||||||
}
|
|
||||||
|
|
||||||
readMethod.addCode(writeSwitch.build());
|
|
||||||
classBuilder.addMethod(readMethod.build());
|
|
||||||
|
|
||||||
TypeSpec spec = classBuilder.build();
|
|
||||||
|
|
||||||
JavaFile.builder(packageName, spec).build().writeTo(filer);
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
128
annotations/src/io/anuke/annotations/RemoteReadGenerator.java
Normal file
128
annotations/src/io/anuke/annotations/RemoteReadGenerator.java
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package io.anuke.annotations;
|
||||||
|
|
||||||
|
import com.squareup.javapoet.*;
|
||||||
|
import io.anuke.annotations.IOFinder.ClassSerializer;
|
||||||
|
|
||||||
|
import javax.lang.model.element.Modifier;
|
||||||
|
import javax.lang.model.element.TypeElement;
|
||||||
|
import javax.lang.model.element.VariableElement;
|
||||||
|
import javax.tools.Diagnostic.Kind;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**Generates code for reading remote invoke packets on the client and server.*/
|
||||||
|
public class RemoteReadGenerator {
|
||||||
|
private final HashMap<String, ClassSerializer> serializers;
|
||||||
|
|
||||||
|
/**Creates a read generator that uses the supplied serializer setup.*/
|
||||||
|
public RemoteReadGenerator(HashMap<String, ClassSerializer> serializers) {
|
||||||
|
this.serializers = serializers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Generates a class for reading remote invoke packets.
|
||||||
|
* @param entries List of methods to use/
|
||||||
|
* @param className Simple target class name.
|
||||||
|
* @param packageName Full target package name.
|
||||||
|
* @param needsPlayer Whether this read method requires a reference to the player sender.*/
|
||||||
|
public void generateFor(List<MethodEntry> entries, String className, String packageName, boolean needsPlayer)
|
||||||
|
throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException {
|
||||||
|
|
||||||
|
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC);
|
||||||
|
|
||||||
|
//create main method builder
|
||||||
|
MethodSpec.Builder readMethod = MethodSpec.methodBuilder("readPacket")
|
||||||
|
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||||
|
.addParameter(ByteBuffer.class, "buffer") //buffer to read form
|
||||||
|
.addParameter(int.class, "id") //ID of method type to read
|
||||||
|
.returns(void.class);
|
||||||
|
|
||||||
|
if(needsPlayer){
|
||||||
|
//since the player type isn't loaded yet, creating a type def is necessary
|
||||||
|
//this requires reflection since the TypeName constructor is private for some reason
|
||||||
|
Constructor<TypeName> cons = TypeName.class.getDeclaredConstructor(String.class);
|
||||||
|
cons.setAccessible(true);
|
||||||
|
|
||||||
|
TypeName playerType = cons.newInstance("io.anuke.mindustry.entities.Player");
|
||||||
|
//add player parameter
|
||||||
|
readMethod.addParameter(playerType, "player");
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeBlock.Builder readBlock = CodeBlock.builder(); //start building block of code inside read method
|
||||||
|
boolean started = false; //whether an if() statement has been written yet
|
||||||
|
|
||||||
|
for(MethodEntry entry : entries){
|
||||||
|
//write if check for this entry ID
|
||||||
|
if(!started){
|
||||||
|
started = true;
|
||||||
|
readBlock.beginControlFlow("if(id == " + entry.id + ")");
|
||||||
|
}else{
|
||||||
|
readBlock.nextControlFlow("else if(id == " + entry.id + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
//concatenated list of variable names for method invocation
|
||||||
|
StringBuilder varResult = new StringBuilder();
|
||||||
|
|
||||||
|
//go through each parameter
|
||||||
|
for(int i = 0; i < entry.element.getParameters().size(); i ++){
|
||||||
|
VariableElement var = entry.element.getParameters().get(i);
|
||||||
|
|
||||||
|
if(!(entry.client && i == 0)) { //if client, skip first parameter since it's always of type player and doesn't need to be read
|
||||||
|
//full type name of parameter
|
||||||
|
//TODO check if the result is correct
|
||||||
|
String typeName = var.asType().toString();
|
||||||
|
//name of parameter
|
||||||
|
String varName = var.getSimpleName().toString();
|
||||||
|
//captialized version of type name for reading primitives
|
||||||
|
String capName = typeName.equals("byte") ? "" : Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1);
|
||||||
|
|
||||||
|
//write primitives automatically
|
||||||
|
if (Utils.isPrimitive(typeName)) {
|
||||||
|
if (typeName.equals("boolean")) {
|
||||||
|
readBlock.addStatement("boolean " + varName + " = buffer.get() == 1");
|
||||||
|
} else {
|
||||||
|
readBlock.addStatement(typeName + " " + varName + " = buffer.get" + capName + "()");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//else, try and find a serializer
|
||||||
|
ClassSerializer ser = serializers.get(typeName);
|
||||||
|
|
||||||
|
if (ser == null) { //make sure a serializer exists!
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "No @ReadClass method to read class type: ", var);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//add statement for reading it
|
||||||
|
readBlock.addStatement(typeName + " " + varName + " = " + ser.readMethod + "(buffer)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//append variable name to string builder
|
||||||
|
varResult.append(var.getSimpleName());
|
||||||
|
if(i != entry.element.getParameters().size() - 1) varResult.append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
//now execute it
|
||||||
|
readBlock.addStatement("com.badlogic.gdx.Gdx.app.postRunnable(() -> $N." + entry.element.getSimpleName() + "(" + varResult.toString() + "))",
|
||||||
|
((TypeElement)entry.element.getEnclosingElement()).getQualifiedName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
//end control flow if necessary
|
||||||
|
if(started){
|
||||||
|
readBlock.nextControlFlow("else");
|
||||||
|
readBlock.addStatement("throw new $1N(\"Invalid read method ID: \" + id + \"\")"); //handle invalid method IDs
|
||||||
|
readBlock.endControlFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
//add block and method to class
|
||||||
|
readMethod.addCode(readBlock.build());
|
||||||
|
classBuilder.addMethod(readMethod.build());
|
||||||
|
|
||||||
|
//build and write resulting class
|
||||||
|
TypeSpec spec = classBuilder.build();
|
||||||
|
JavaFile.builder(packageName, spec).build().writeTo(Utils.filer);
|
||||||
|
}
|
||||||
|
}
|
||||||
161
annotations/src/io/anuke/annotations/RemoteWriteGenerator.java
Normal file
161
annotations/src/io/anuke/annotations/RemoteWriteGenerator.java
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package io.anuke.annotations;
|
||||||
|
|
||||||
|
import com.squareup.javapoet.*;
|
||||||
|
import io.anuke.annotations.IOFinder.ClassSerializer;
|
||||||
|
|
||||||
|
import javax.lang.model.element.ExecutableElement;
|
||||||
|
import javax.lang.model.element.Modifier;
|
||||||
|
import javax.lang.model.element.TypeElement;
|
||||||
|
import javax.lang.model.element.VariableElement;
|
||||||
|
import javax.tools.Diagnostic.Kind;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**Generates code for writing remote invoke packets on the client and server.*/
|
||||||
|
public class RemoteWriteGenerator {
|
||||||
|
private final HashMap<String, ClassSerializer> serializers;
|
||||||
|
|
||||||
|
/**Creates a write generator that uses the supplied serializer setup.*/
|
||||||
|
public RemoteWriteGenerator(HashMap<String, ClassSerializer> serializers) {
|
||||||
|
this.serializers = serializers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Generates all classes in this list.*/
|
||||||
|
public void generateFor(List<ClassEntry> entries, String packageName) throws IOException {
|
||||||
|
|
||||||
|
for(ClassEntry entry : entries){
|
||||||
|
//create builder
|
||||||
|
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(entry.name).addModifiers(Modifier.PUBLIC);
|
||||||
|
|
||||||
|
//add temporary write buffer
|
||||||
|
classBuilder.addField(FieldSpec.builder(ByteBuffer.class, "TEMP_BUFFER", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL)
|
||||||
|
.initializer("ByteBuffer.allocate($1L)", RemoteMethodAnnotationProcessor.maxPacketSize).build());
|
||||||
|
|
||||||
|
//go through each method entry in this class
|
||||||
|
for(MethodEntry methodEntry : entry.methods){
|
||||||
|
//write the 'send event to all players' variant: always happens for clients, but only happens if 'all' is enabled on the server method
|
||||||
|
if(!methodEntry.server || methodEntry.allVariant){
|
||||||
|
writeMethodVariant(classBuilder, methodEntry, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//write the 'send even to one player' variant, which is only applicable on the server
|
||||||
|
if(methodEntry.server && methodEntry.oneVariant){
|
||||||
|
writeMethodVariant(classBuilder, methodEntry, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//build and write resulting class
|
||||||
|
TypeSpec spec = classBuilder.build();
|
||||||
|
JavaFile.builder(packageName, spec).build().writeTo(Utils.filer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeMethodVariant(TypeSpec.Builder classBuilder, MethodEntry methodEntry, boolean toAll){
|
||||||
|
ExecutableElement elem = methodEntry.element;
|
||||||
|
|
||||||
|
//create builder
|
||||||
|
MethodSpec.Builder method = MethodSpec.methodBuilder(elem.getSimpleName().toString())
|
||||||
|
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||||
|
.returns(void.class);
|
||||||
|
|
||||||
|
//validate client methods to make sure
|
||||||
|
if(methodEntry.client){
|
||||||
|
if(elem.getParameters().isEmpty()){
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "Client invoke methods must have a first parameter of type Player.", elem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!elem.getParameters().get(0).asType().toString().equals("io.anuke.mindustry.entities.Player")){
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "Client invoke methods should have a first parameter of type Player.", elem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if toAll is false, it's a 'send to one player' variant, so add the player as a parameter
|
||||||
|
if(!toAll){
|
||||||
|
method.addParameter(int.class, "playerClientID");
|
||||||
|
}
|
||||||
|
|
||||||
|
//add all other parameters to method
|
||||||
|
for(VariableElement var : elem.getParameters()){
|
||||||
|
method.addParameter(TypeName.get(var.asType()), var.getSimpleName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
//call local method if applicable
|
||||||
|
if(methodEntry.local){
|
||||||
|
//concatenate parameters
|
||||||
|
int index = 0;
|
||||||
|
StringBuilder results = new StringBuilder();
|
||||||
|
for(VariableElement var : elem.getParameters()){
|
||||||
|
results.append(var.getSimpleName());
|
||||||
|
if(index != elem.getParameters().size() - 1) results.append(", ");
|
||||||
|
index ++;
|
||||||
|
}
|
||||||
|
|
||||||
|
//add the statement to call it
|
||||||
|
method.addStatement("$N." + elem.getSimpleName() + "(" + results.toString() + ")",
|
||||||
|
((TypeElement)elem.getEnclosingElement()).getQualifiedName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
//start control flow to check if it's actually client/server so no netcode is called
|
||||||
|
method.beginControlFlow("if(io.anuke.mindustry.net.Net." + (methodEntry.client ? "client" : "server")+"())");
|
||||||
|
|
||||||
|
//add statement to create packet from pool
|
||||||
|
method.addStatement("$1N packet = $2N.obtain($1N.class)", "io.anuke.mindustry.net.Packets.InvokePacket", "com.badlogic.gdx.utils.Pools");
|
||||||
|
//assign buffer
|
||||||
|
method.addStatement("packet.writeBuffer = TEMP_BUFFER");
|
||||||
|
//rewind buffer
|
||||||
|
method.addStatement("TEMP_BUFFER.position(0)");
|
||||||
|
|
||||||
|
for(VariableElement var : elem.getParameters()){
|
||||||
|
//name of parameter
|
||||||
|
String varName = var.getSimpleName().toString();
|
||||||
|
//name of parameter type
|
||||||
|
String typeName = var.asType().toString();
|
||||||
|
//captialized version of type name for writing primitives
|
||||||
|
String capName = typeName.equals("byte") ? "" : Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1);
|
||||||
|
|
||||||
|
if(Utils.isPrimitive(typeName)) { //check if it's a primitive, and if so write it
|
||||||
|
if(typeName.equals("boolean")){ //booleans are special
|
||||||
|
method.addStatement("TEMP_BUFFER.put(" + varName + " ? (byte)1 : 0)");
|
||||||
|
}else{
|
||||||
|
method.addStatement("TEMP_BUFFER.put" +
|
||||||
|
capName + "(" + varName + ")");
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
//else, try and find a serializer
|
||||||
|
ClassSerializer ser = serializers.get(typeName);
|
||||||
|
|
||||||
|
if(ser == null){ //make sure a serializer exists!
|
||||||
|
Utils.messager.printMessage(Kind.ERROR, "No @WriteClass method to write class type: ", var);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//add statement for writing it
|
||||||
|
method.addStatement(ser.writeMethod + "(buffer, " + varName +")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//assign packet length
|
||||||
|
method.addStatement("packet.writeLength = TEMP_BUFFER.position()");
|
||||||
|
|
||||||
|
//send the actual packet
|
||||||
|
if(toAll){
|
||||||
|
//send to all players / to server
|
||||||
|
method.addStatement("io.anuke.mindustry.net.Net.send(packet, "+
|
||||||
|
(methodEntry.unreliable ? "io.anuke.mindustry.net.Net.SendMode.udp" : "io.anuke.mindustry.net.Net.SendMode.tcp")+")");
|
||||||
|
}else{
|
||||||
|
//send to specific client from server
|
||||||
|
method.addStatement("io.anuke.mindustry.net.Net.sendTo(playerClientID, packet, "+
|
||||||
|
(methodEntry.unreliable ? "io.anuke.mindustry.net.Net.SendMode.udp" : "io.anuke.mindustry.net.Net.SendMode.tcp")+")");
|
||||||
|
}
|
||||||
|
|
||||||
|
//end check for server/client
|
||||||
|
method.endControlFlow();
|
||||||
|
|
||||||
|
//add method to class, finally
|
||||||
|
classBuilder.addMethod(method.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package io.anuke.annotations;
|
|||||||
|
|
||||||
import javax.annotation.processing.Filer;
|
import javax.annotation.processing.Filer;
|
||||||
import javax.annotation.processing.Messager;
|
import javax.annotation.processing.Messager;
|
||||||
|
import javax.lang.model.element.Element;
|
||||||
|
import javax.lang.model.element.TypeElement;
|
||||||
import javax.lang.model.util.Elements;
|
import javax.lang.model.util.Elements;
|
||||||
import javax.lang.model.util.Types;
|
import javax.lang.model.util.Types;
|
||||||
|
|
||||||
@@ -11,6 +13,10 @@ public class Utils {
|
|||||||
public static Filer filer;
|
public static Filer filer;
|
||||||
public static Messager messager;
|
public static Messager messager;
|
||||||
|
|
||||||
|
public static String getMethodName(Element element){
|
||||||
|
return ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString() + "." + element.getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isPrimitive(String type){
|
public static boolean isPrimitive(String type){
|
||||||
return type.equals("boolean") || type.equals("byte") || type.equals("short") || type.equals("int")
|
return type.equals("boolean") || type.equals("byte") || type.equals("short") || type.equals("int")
|
||||||
|| type.equals("long") || type.equals("float") || type.equals("double") || type.equals("char");
|
|| type.equals("long") || type.equals("float") || type.equals("double") || type.equals("char");
|
||||||
|
|||||||
Reference in New Issue
Block a user