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 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 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");