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:
parent
ccb97e34d5
commit
9e136bad94
@ -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>
|
||||
|
@ -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.*/
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
128
annotations/src/io/anuke/annotations/RemoteReadGenerator.java
Normal file
128
annotations/src/io/anuke/annotations/RemoteReadGenerator.java
Normal file
@ -0,0 +1,128 @@
|
||||
package io.anuke.annotations;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import io.anuke.annotations.IOFinder.ClassSerializer;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.tools.Diagnostic.Kind;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**Generates code for reading remote invoke packets on the client and server.*/
|
||||
public class RemoteReadGenerator {
|
||||
private final HashMap<String, ClassSerializer> serializers;
|
||||
|
||||
/**Creates a read generator that uses the supplied serializer setup.*/
|
||||
public RemoteReadGenerator(HashMap<String, ClassSerializer> serializers) {
|
||||
this.serializers = serializers;
|
||||
}
|
||||
|
||||
/**Generates a class for reading remote invoke packets.
|
||||
* @param entries List of methods to use/
|
||||
* @param className Simple target class name.
|
||||
* @param packageName Full target package name.
|
||||
* @param needsPlayer Whether this read method requires a reference to the player sender.*/
|
||||
public void generateFor(List<MethodEntry> entries, String className, String packageName, boolean needsPlayer)
|
||||
throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException {
|
||||
|
||||
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC);
|
||||
|
||||
//create main method builder
|
||||
MethodSpec.Builder readMethod = MethodSpec.methodBuilder("readPacket")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addParameter(ByteBuffer.class, "buffer") //buffer to read form
|
||||
.addParameter(int.class, "id") //ID of method type to read
|
||||
.returns(void.class);
|
||||
|
||||
if(needsPlayer){
|
||||
//since the player type isn't loaded yet, creating a type def is necessary
|
||||
//this requires reflection since the TypeName constructor is private for some reason
|
||||
Constructor<TypeName> cons = TypeName.class.getDeclaredConstructor(String.class);
|
||||
cons.setAccessible(true);
|
||||
|
||||
TypeName playerType = cons.newInstance("io.anuke.mindustry.entities.Player");
|
||||
//add player parameter
|
||||
readMethod.addParameter(playerType, "player");
|
||||
}
|
||||
|
||||
CodeBlock.Builder readBlock = CodeBlock.builder(); //start building block of code inside read method
|
||||
boolean started = false; //whether an if() statement has been written yet
|
||||
|
||||
for(MethodEntry entry : entries){
|
||||
//write if check for this entry ID
|
||||
if(!started){
|
||||
started = true;
|
||||
readBlock.beginControlFlow("if(id == " + entry.id + ")");
|
||||
}else{
|
||||
readBlock.nextControlFlow("else if(id == " + entry.id + ")");
|
||||
}
|
||||
|
||||
//concatenated list of variable names for method invocation
|
||||
StringBuilder varResult = new StringBuilder();
|
||||
|
||||
//go through each parameter
|
||||
for(int i = 0; i < entry.element.getParameters().size(); i ++){
|
||||
VariableElement var = entry.element.getParameters().get(i);
|
||||
|
||||
if(!(entry.client && i == 0)) { //if client, skip first parameter since it's always of type player and doesn't need to be read
|
||||
//full type name of parameter
|
||||
//TODO check if the result is correct
|
||||
String typeName = var.asType().toString();
|
||||
//name of parameter
|
||||
String varName = var.getSimpleName().toString();
|
||||
//captialized version of type name for reading primitives
|
||||
String capName = typeName.equals("byte") ? "" : Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1);
|
||||
|
||||
//write primitives automatically
|
||||
if (Utils.isPrimitive(typeName)) {
|
||||
if (typeName.equals("boolean")) {
|
||||
readBlock.addStatement("boolean " + varName + " = buffer.get() == 1");
|
||||
} else {
|
||||
readBlock.addStatement(typeName + " " + varName + " = buffer.get" + capName + "()");
|
||||
}
|
||||
} else {
|
||||
//else, try and find a serializer
|
||||
ClassSerializer ser = serializers.get(typeName);
|
||||
|
||||
if (ser == null) { //make sure a serializer exists!
|
||||
Utils.messager.printMessage(Kind.ERROR, "No @ReadClass method to read class type: ", var);
|
||||
return;
|
||||
}
|
||||
|
||||
//add statement for reading it
|
||||
readBlock.addStatement(typeName + " " + varName + " = " + ser.readMethod + "(buffer)");
|
||||
}
|
||||
}
|
||||
|
||||
//append variable name to string builder
|
||||
varResult.append(var.getSimpleName());
|
||||
if(i != entry.element.getParameters().size() - 1) varResult.append(", ");
|
||||
}
|
||||
|
||||
//now execute it
|
||||
readBlock.addStatement("com.badlogic.gdx.Gdx.app.postRunnable(() -> $N." + entry.element.getSimpleName() + "(" + varResult.toString() + "))",
|
||||
((TypeElement)entry.element.getEnclosingElement()).getQualifiedName().toString());
|
||||
}
|
||||
|
||||
//end control flow if necessary
|
||||
if(started){
|
||||
readBlock.nextControlFlow("else");
|
||||
readBlock.addStatement("throw new $1N(\"Invalid read method ID: \" + id + \"\")"); //handle invalid method IDs
|
||||
readBlock.endControlFlow();
|
||||
}
|
||||
|
||||
//add block and method to class
|
||||
readMethod.addCode(readBlock.build());
|
||||
classBuilder.addMethod(readMethod.build());
|
||||
|
||||
//build and write resulting class
|
||||
TypeSpec spec = classBuilder.build();
|
||||
JavaFile.builder(packageName, spec).build().writeTo(Utils.filer);
|
||||
}
|
||||
}
|
161
annotations/src/io/anuke/annotations/RemoteWriteGenerator.java
Normal file
161
annotations/src/io/anuke/annotations/RemoteWriteGenerator.java
Normal file
@ -0,0 +1,161 @@
|
||||
package io.anuke.annotations;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import io.anuke.annotations.IOFinder.ClassSerializer;
|
||||
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.tools.Diagnostic.Kind;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**Generates code for writing remote invoke packets on the client and server.*/
|
||||
public class RemoteWriteGenerator {
|
||||
private final HashMap<String, ClassSerializer> serializers;
|
||||
|
||||
/**Creates a write generator that uses the supplied serializer setup.*/
|
||||
public RemoteWriteGenerator(HashMap<String, ClassSerializer> serializers) {
|
||||
this.serializers = serializers;
|
||||
}
|
||||
|
||||
/**Generates all classes in this list.*/
|
||||
public void generateFor(List<ClassEntry> entries, String packageName) throws IOException {
|
||||
|
||||
for(ClassEntry entry : entries){
|
||||
//create builder
|
||||
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(entry.name).addModifiers(Modifier.PUBLIC);
|
||||
|
||||
//add temporary write buffer
|
||||
classBuilder.addField(FieldSpec.builder(ByteBuffer.class, "TEMP_BUFFER", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL)
|
||||
.initializer("ByteBuffer.allocate($1L)", RemoteMethodAnnotationProcessor.maxPacketSize).build());
|
||||
|
||||
//go through each method entry in this class
|
||||
for(MethodEntry methodEntry : entry.methods){
|
||||
//write the 'send event to all players' variant: always happens for clients, but only happens if 'all' is enabled on the server method
|
||||
if(!methodEntry.server || methodEntry.allVariant){
|
||||
writeMethodVariant(classBuilder, methodEntry, true);
|
||||
}
|
||||
|
||||
//write the 'send even to one player' variant, which is only applicable on the server
|
||||
if(methodEntry.server && methodEntry.oneVariant){
|
||||
writeMethodVariant(classBuilder, methodEntry, true);
|
||||
}
|
||||
}
|
||||
|
||||
//build and write resulting class
|
||||
TypeSpec spec = classBuilder.build();
|
||||
JavaFile.builder(packageName, spec).build().writeTo(Utils.filer);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMethodVariant(TypeSpec.Builder classBuilder, MethodEntry methodEntry, boolean toAll){
|
||||
ExecutableElement elem = methodEntry.element;
|
||||
|
||||
//create builder
|
||||
MethodSpec.Builder method = MethodSpec.methodBuilder(elem.getSimpleName().toString())
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.returns(void.class);
|
||||
|
||||
//validate client methods to make sure
|
||||
if(methodEntry.client){
|
||||
if(elem.getParameters().isEmpty()){
|
||||
Utils.messager.printMessage(Kind.ERROR, "Client invoke methods must have a first parameter of type Player.", elem);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!elem.getParameters().get(0).asType().toString().equals("io.anuke.mindustry.entities.Player")){
|
||||
Utils.messager.printMessage(Kind.ERROR, "Client invoke methods should have a first parameter of type Player.", elem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//if toAll is false, it's a 'send to one player' variant, so add the player as a parameter
|
||||
if(!toAll){
|
||||
method.addParameter(int.class, "playerClientID");
|
||||
}
|
||||
|
||||
//add all other parameters to method
|
||||
for(VariableElement var : elem.getParameters()){
|
||||
method.addParameter(TypeName.get(var.asType()), var.getSimpleName().toString());
|
||||
}
|
||||
|
||||
//call local method if applicable
|
||||
if(methodEntry.local){
|
||||
//concatenate parameters
|
||||
int index = 0;
|
||||
StringBuilder results = new StringBuilder();
|
||||
for(VariableElement var : elem.getParameters()){
|
||||
results.append(var.getSimpleName());
|
||||
if(index != elem.getParameters().size() - 1) results.append(", ");
|
||||
index ++;
|
||||
}
|
||||
|
||||
//add the statement to call it
|
||||
method.addStatement("$N." + elem.getSimpleName() + "(" + results.toString() + ")",
|
||||
((TypeElement)elem.getEnclosingElement()).getQualifiedName().toString());
|
||||
}
|
||||
|
||||
//start control flow to check if it's actually client/server so no netcode is called
|
||||
method.beginControlFlow("if(io.anuke.mindustry.net.Net." + (methodEntry.client ? "client" : "server")+"())");
|
||||
|
||||
//add statement to create packet from pool
|
||||
method.addStatement("$1N packet = $2N.obtain($1N.class)", "io.anuke.mindustry.net.Packets.InvokePacket", "com.badlogic.gdx.utils.Pools");
|
||||
//assign buffer
|
||||
method.addStatement("packet.writeBuffer = TEMP_BUFFER");
|
||||
//rewind buffer
|
||||
method.addStatement("TEMP_BUFFER.position(0)");
|
||||
|
||||
for(VariableElement var : elem.getParameters()){
|
||||
//name of parameter
|
||||
String varName = var.getSimpleName().toString();
|
||||
//name of parameter type
|
||||
String typeName = var.asType().toString();
|
||||
//captialized version of type name for writing primitives
|
||||
String capName = typeName.equals("byte") ? "" : Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1);
|
||||
|
||||
if(Utils.isPrimitive(typeName)) { //check if it's a primitive, and if so write it
|
||||
if(typeName.equals("boolean")){ //booleans are special
|
||||
method.addStatement("TEMP_BUFFER.put(" + varName + " ? (byte)1 : 0)");
|
||||
}else{
|
||||
method.addStatement("TEMP_BUFFER.put" +
|
||||
capName + "(" + varName + ")");
|
||||
}
|
||||
}else{
|
||||
//else, try and find a serializer
|
||||
ClassSerializer ser = serializers.get(typeName);
|
||||
|
||||
if(ser == null){ //make sure a serializer exists!
|
||||
Utils.messager.printMessage(Kind.ERROR, "No @WriteClass method to write class type: ", var);
|
||||
return;
|
||||
}
|
||||
|
||||
//add statement for writing it
|
||||
method.addStatement(ser.writeMethod + "(buffer, " + varName +")");
|
||||
}
|
||||
}
|
||||
|
||||
//assign packet length
|
||||
method.addStatement("packet.writeLength = TEMP_BUFFER.position()");
|
||||
|
||||
//send the actual packet
|
||||
if(toAll){
|
||||
//send to all players / to server
|
||||
method.addStatement("io.anuke.mindustry.net.Net.send(packet, "+
|
||||
(methodEntry.unreliable ? "io.anuke.mindustry.net.Net.SendMode.udp" : "io.anuke.mindustry.net.Net.SendMode.tcp")+")");
|
||||
}else{
|
||||
//send to specific client from server
|
||||
method.addStatement("io.anuke.mindustry.net.Net.sendTo(playerClientID, packet, "+
|
||||
(methodEntry.unreliable ? "io.anuke.mindustry.net.Net.SendMode.udp" : "io.anuke.mindustry.net.Net.SendMode.tcp")+")");
|
||||
}
|
||||
|
||||
//end check for server/client
|
||||
method.endControlFlow();
|
||||
|
||||
//add method to class, finally
|
||||
classBuilder.addMethod(method.build());
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ package io.anuke.annotations;
|
||||
|
||||
import javax.annotation.processing.Filer;
|
||||
import javax.annotation.processing.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");
|
||||
|
Loading…
Reference in New Issue
Block a user