diff --git a/annotations/src/io/anuke/annotations/Annotations.java b/annotations/src/io/anuke/annotations/Annotations.java
index 5b08e70d37..60904a29d2 100644
--- a/annotations/src/io/anuke/annotations/Annotations.java
+++ b/annotations/src/io/anuke/annotations/Annotations.java
@@ -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.
* These events may optionally also trigger on the caller client/server as well.
- *
- * Three annotations are used for this purpose.
- * {@link RemoteClient}: Marks a method as able to be invoked remotely on a client from a server.
- * {@link RemoteServer}: Marks a method as able to be invoked remotely on a server from a client.
- * {@link Local}: Makes this method get invoked locally as well as remotely.
- *
- * All RemoteClient methods are put in the class io.anuke.mindustry.gen.CallClient.
- * All RemoteServer methods are put in the class io.anuke.mindustry.gen.CallServer.
*/
public class Annotations {
/**Marks a method as invokable remotely from a server on a client.*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
- public @interface RemoteClient {
- /**Whether a client-specific method is generated that accepts a connecton ID and sends to only one player. Default is false.*/
+ public @interface Remote {
+ /**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;
- /**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;
- }
-
- /**Marks a method as invokable remotely from a client on a server.
- * All RemoteServer methods must have their first formal parameter be of type Player.
- * This player is the invoker of the method.*/
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.CLASS)
- 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();
+ /**Whether this method is invoked locally as well as remotely.*/
+ boolean local() default true;
+ /**Whether the packet for this method is sent with UDP instead of TCP.
+ * UDP is faster, but is prone to packet loss and duplication.*/
+ boolean unreliable() default false;
+ /**The simple class name where this method is placed.*/
+ String target() default "Call";
}
/**Specifies that this method will be used to write classes of the type returned by {@link #value()}.
diff --git a/annotations/src/io/anuke/annotations/IOFinder.java b/annotations/src/io/anuke/annotations/IOFinder.java
index f536dd9a3e..aec7208efc 100644
--- a/annotations/src/io/anuke/annotations/IOFinder.java
+++ b/annotations/src/io/anuke/annotations/IOFinder.java
@@ -15,7 +15,7 @@ import java.util.stream.Stream;
public class IOFinder {
/**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 findSerializers(RoundEnvironment env){
HashMap result = new HashMap<>();
@@ -44,16 +44,12 @@ public class IOFinder {
Element reader = stream.findFirst().get();
//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;
}
- private String getFullMethod(Element element){
- return element.getEnclosingElement().asType().toString() + "." + element.getSimpleName();
- }
-
/**Information about read/write methods for a specific class type.*/
public static class ClassSerializer{
/**Fully qualified method name of the reader.*/
diff --git a/annotations/src/io/anuke/annotations/MethodEntry.java b/annotations/src/io/anuke/annotations/MethodEntry.java
index 3036afde33..d0a60d031e 100644
--- a/annotations/src/io/anuke/annotations/MethodEntry.java
+++ b/annotations/src/io/anuke/annotations/MethodEntry.java
@@ -1,5 +1,7 @@
package io.anuke.annotations;
+import javax.lang.model.element.ExecutableElement;
+
/**Class that repesents a remote method to be constructed and put into a class.*/
public class MethodEntry {
/**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.
* Only applicable to client (server-invoked) methods.*/
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.targetMethod = targetMethod;
this.client = client;
this.server = server;
this.allVariant = allVariant;
this.oneVariant = oneVariant;
+ this.local = local;
+ this.id = id;
+ this.element = element;
+ this.unreliable = unreliable;
+ }
+
+ @Override
+ public int hashCode() {
+ return targetMethod.hashCode();
}
}
diff --git a/annotations/src/io/anuke/annotations/RemoteMethodAnnotationProcessor.java b/annotations/src/io/anuke/annotations/RemoteMethodAnnotationProcessor.java
index ff8ca50e0d..638e801f53 100644
--- a/annotations/src/io/anuke/annotations/RemoteMethodAnnotationProcessor.java
+++ b/annotations/src/io/anuke/annotations/RemoteMethodAnnotationProcessor.java
@@ -1,24 +1,23 @@
package io.anuke.annotations;
-import com.squareup.javapoet.*;
-import io.anuke.annotations.Annotations.Local;
-import io.anuke.annotations.Annotations.RemoteClient;
-import io.anuke.annotations.Annotations.RemoteServer;
-import io.anuke.annotations.Annotations.Unreliable;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.TypeSpec;
+import io.anuke.annotations.Annotations.Remote;
import io.anuke.annotations.IOFinder.ClassSerializer;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
-import javax.lang.model.element.*;
-import javax.lang.model.util.Elements;
-import javax.lang.model.util.Types;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
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.HashMap;
+import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
//TODO document
//TODO split up into more classes
@@ -42,12 +41,15 @@ import java.util.Set;
})
public class RemoteMethodAnnotationProcessor extends AbstractProcessor {
/**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.*/
- 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 serializers;
/**Whether the initial round is done.*/
private boolean done;
@@ -63,203 +65,87 @@ public class RemoteMethodAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
- if(done) return false;
+ if(done) return false; //only process 1 round
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 {
- boolean client = annotation == RemoteServer.class;
- String className = fullClassName.substring(1 + fullClassName.lastIndexOf('.'));
- String packageName = fullClassName.substring(0, fullClassName.lastIndexOf('.'));
- Constructor cons = TypeName.class.getDeclaredConstructor(String.class);
- cons.setAccessible(true);
+ //get serializers
+ HashMap 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 classMap = new HashMap<>();
+ //list of all method entries
+ ArrayList methods = new ArrayList<>();
+ //list of all method entries
+ ArrayList 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)
- .initializer("ByteBuffer.allocate($1L)", maxPacketSize).build());
+ //get and create class entry if needed
+ 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")
- .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
- .addParameter(ByteBuffer.class, "buffer")
- .addParameter(int.class, "id")
- .returns(void.class);
+ ClassEntry entry = classMap.get(annotation.target());
- if(client){
- readMethod.addParameter(playerType, "player");
+ //make sure that each server annotation has at least one method to generate, otherwise throw an error
+ 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();
- boolean started = false;
+ //create read/write generators
+ 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)) {
- if(!e.getModifiers().contains(Modifier.STATIC)) {
- 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);
- }
+ //generate the methods to invoke (write)
+ writegen.generateFor(classes, packageName);
- if(e.getAnnotation(annotation) == null) continue;
- boolean local = e.getAnnotation(Local.class) != null;
- boolean unreliable = e.getAnnotation(Unreliable.class) != null;
+ //create class for storing unique method hash
+ TypeSpec.Builder hashBuilder = TypeSpec.classBuilder("MethodHash").addModifiers(Modifier.PUBLIC);
+ hashBuilder.addField(FieldSpec.builder(int.class, "HASH", Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL)
+ .initializer("$1L)", Objects.hash(methods)).build());
- ExecutableElement exec = (ExecutableElement)e;
-
- MethodSpec.Builder method = MethodSpec.methodBuilder(e.getSimpleName().toString())
- .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 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);
+ //build and write resulting hash class
+ TypeSpec spec = hashBuilder.build();
+ JavaFile.builder(packageName, spec).build().writeTo(Utils.filer);
+ return true;
}catch (Exception e){
- e.printStackTrace();
throw new RuntimeException(e);
}
}
-
-
-
}
diff --git a/annotations/src/io/anuke/annotations/RemoteReadGenerator.java b/annotations/src/io/anuke/annotations/RemoteReadGenerator.java
new file mode 100644
index 0000000000..6068fb175b
--- /dev/null
+++ b/annotations/src/io/anuke/annotations/RemoteReadGenerator.java
@@ -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 serializers;
+
+ /**Creates a read generator that uses the supplied serializer setup.*/
+ public RemoteReadGenerator(HashMap 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 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 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);
+ }
+}
diff --git a/annotations/src/io/anuke/annotations/RemoteWriteGenerator.java b/annotations/src/io/anuke/annotations/RemoteWriteGenerator.java
new file mode 100644
index 0000000000..1838ea152d
--- /dev/null
+++ b/annotations/src/io/anuke/annotations/RemoteWriteGenerator.java
@@ -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 serializers;
+
+ /**Creates a write generator that uses the supplied serializer setup.*/
+ public RemoteWriteGenerator(HashMap serializers) {
+ this.serializers = serializers;
+ }
+
+ /**Generates all classes in this list.*/
+ public void generateFor(List 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());
+ }
+}
diff --git a/annotations/src/io/anuke/annotations/Utils.java b/annotations/src/io/anuke/annotations/Utils.java
index 63d21b539e..d379a2d257 100644
--- a/annotations/src/io/anuke/annotations/Utils.java
+++ b/annotations/src/io/anuke/annotations/Utils.java
@@ -2,6 +2,8 @@ package io.anuke.annotations;
import javax.annotation.processing.Filer;
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.Types;
@@ -11,6 +13,10 @@ public class Utils {
public static Filer filer;
public static Messager messager;
+ public static String getMethodName(Element element){
+ return ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString() + "." + element.getSimpleName();
+ }
+
public static boolean isPrimitive(String type){
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");