1
0
mirror of https://github.com/Anuken/Mindustry.git synced 2024-11-13 07:15:28 +03:00

Core annotation system finished

This commit is contained in:
Anuken 2018-06-06 20:23:19 -04:00
parent ccb97e34d5
commit 9e136bad94
7 changed files with 412 additions and 237 deletions

View File

@ -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>
* 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 {
/**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()}.<br>

View File

@ -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<String, ClassSerializer> findSerializers(RoundEnvironment env){
HashMap<String, ClassSerializer> 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.*/

View File

@ -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();
}
}

View File

@ -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<String, ClassSerializer> 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<TypeName> cons = TypeName.class.getDeclaredConstructor(String.class);
cons.setAccessible(true);
//get serializers
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)
.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<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);
//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);
}
}
}

View 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);
}
}

View 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());
}
}

View File

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