Added entity revisions

This commit is contained in:
Anuken
2020-02-14 12:54:44 -05:00
parent c4d4dcb9db
commit abaa462645
7 changed files with 180 additions and 58 deletions

View File

@@ -68,6 +68,25 @@ public abstract class BaseProcessor extends AbstractProcessor{
}
}
//in bytes
public static int typeSize(String kind){
switch(kind){
case "boolean":
case "byte":
return 1;
case "short":
return 2;
case "float":
case "char":
case "int":
return 4;
case "long":
return 8;
default:
throw new IllegalArgumentException("Invalid primitive type: " + kind + "");
}
}
public static String simpleName(String str){
return str.contains(".") ? str.substring(str.lastIndexOf('.') + 1) : str;
}

View File

@@ -1,59 +1,185 @@
package mindustry.annotations.entity;
import arc.files.*;
import arc.struct.*;
import arc.util.serialization.*;
import com.squareup.javapoet.*;
import com.squareup.javapoet.MethodSpec.*;
import mindustry.annotations.Annotations.*;
import mindustry.annotations.*;
import mindustry.annotations.util.TypeIOResolver.*;
import javax.lang.model.element.*;
import static mindustry.annotations.BaseProcessor.instanceOf;
public class EntityIO{
final MethodSpec.Builder builder;
final boolean write;
final ClassSerializer serializer;
final static Json json = new Json();
EntityIO(Builder builder, boolean write, ClassSerializer serializer){
this.builder = builder;
final ClassSerializer serializer;
final String name;
final TypeSpec.Builder type;
final Fi directory;
final Array<Revision> revisions = new Array<>();
boolean write;
MethodSpec.Builder method;
ObjectSet<String> presentFields = new ObjectSet<>();
EntityIO(String name, TypeSpec.Builder type, ClassSerializer serializer, Fi directory){
this.directory = directory;
this.type = type;
this.serializer = serializer;
this.write = write;
this.name = name;
directory.mkdirs();
//load old revisions
for(Fi fi : directory.list()){
revisions.add(json.fromJson(Revision.class, fi));
}
//next revision to be used
int nextRevision = revisions.isEmpty() ? 0 : revisions.max(r -> r.version).version + 1;
//resolve preferred field order based on fields that fit
Array<FieldSpec> fields = Array.with(type.fieldSpecs).select(spec ->
!spec.hasModifier(Modifier.TRANSIENT) &&
!spec.hasModifier(Modifier.STATIC) &&
!spec.hasModifier(Modifier.FINAL) &&
(spec.type.isPrimitive() || serializer.has(spec.type.toString())));
//sort to keep order
fields.sortComparing(f -> f.name);
//keep track of fields present in the entity
presentFields.addAll(fields.map(f -> f.name));
//add new revision if it doesn't match or there are no revisions
if(revisions.isEmpty() || !revisions.peek().equal(fields)){
revisions.add(new Revision(nextRevision, fields.map(f -> new RevisionField(f.name, f.type.toString(), f.type.isPrimitive() ? BaseProcessor.typeSize(f.type.toString()) : -1))));
//write revision
directory.child(nextRevision + ".json").writeString(json.toJson(revisions.peek()));
}
}
void io(TypeName type, String field) throws Exception{
void write(MethodSpec.Builder method, boolean write) throws Exception{
this.method = method;
this.write = write;
if(type.isPrimitive()){
s(type == TypeName.BOOLEAN ? "bool" : type.toString().charAt(0) + "", field);
//}else if(type.toString().equals("java.lang.String")){
// s("str", field);
}else if(instanceOf(type.toString(), "mindustry.ctype.Content")){
//subclasses *have* to call this method
method.addAnnotation(CallSuper.class);
if(write){
//write short revision
st("write.s($L)", revisions.peek().version);
//write uses most recent revision
for(RevisionField field : revisions.peek().fields){
io(field.type, "this." + field.name);
}
}else{
//read revision
st("short REV = read.s()");
for(int i = 0; i < revisions.size; i++){
//check for the right revision
Revision rev = revisions.get(i);
if(i == 0){
cont("if(REV == $L)", rev.version);
}else{
ncont("else if(REV == $L)", rev.version);
}
//add code for reading revision
for(RevisionField field : rev.fields){
//if the field doesn't exist, the result will be an empty string, it won't get assigned
io(field.type, presentFields.contains(field.name) ? "this." + field.name + " = " : "");
}
}
//throw exception on illegal revisions
ncont("else");
st("throw new IllegalArgumentException(\"Unknown revision '\" + REV + \"' for entity type '" + name + "'\")");
econt();
}
}
private void io(String type, String field) throws Exception{
if(BaseProcessor.isPrimitive(type)){
s(type.equals("boolean") ? "bool" : type.charAt(0) + "", field);
}else if(instanceOf(type, "mindustry.ctype.Content")){
if(write){
s("s", field + ".id");
}else{
st(field + " = mindustry.Vars.content.getByID(mindustry.ctype.ContentType.$L, read.s())", BaseProcessor.simpleName(type.toString()).toLowerCase().replace("type", ""));
st(field + "mindustry.Vars.content.getByID(mindustry.ctype.ContentType.$L, read.s())", BaseProcessor.simpleName(type).toLowerCase().replace("type", ""));
}
}else if(serializer.writers.containsKey(type.toString()) && write){
st("$L(write, $L)", serializer.writers.get(type.toString()), field);
}else if(serializer.readers.containsKey(type.toString()) && !write){
st("$L = $L(read)", field, serializer.readers.get(type.toString()));
}else if(serializer.writers.containsKey(type) && write){
st("$L(write, $L)", serializer.writers.get(type), field);
}else if(serializer.readers.containsKey(type) && !write){
st("$L$L(read)", field, serializer.readers.get(type));
}
}
private void cont(String text, Object... fmt){
builder.beginControlFlow(text, fmt);
method.beginControlFlow(text, fmt);
}
private void cont(){
builder.endControlFlow();
private void econt(){
method.endControlFlow();
}
private void ncont(String text, Object... fmt){
method.nextControlFlow(text, fmt);
}
private void st(String text, Object... args){
builder.addStatement(text, args);
method.addStatement(text, args);
}
private void s(String type, String field){
if(write){
builder.addStatement("write.$L($L)", type, field);
method.addStatement("write.$L($L)", type, field);
}else{
builder.addStatement("$L = read.$L()", field, type);
method.addStatement("$Lread.$L()", field, type);
}
}
public static class Revision{
int version;
Array<RevisionField> fields;
Revision(int version, Array<RevisionField> fields){
this.version = version;
this.fields = fields;
}
Revision(){}
/** @return whether these two revisions are compatible */
boolean equal(Array<FieldSpec> specs){
if(fields.size != specs.size) return false;
for(int i = 0; i < fields.size; i++){
RevisionField field = fields.get(i);
FieldSpec spec = specs.get(i);
//TODO when making fields, their primitive size may be overwritten by an annotation; check for that
if(!(field.type.equals(spec.type.toString()) && (!spec.type.isPrimitive() || BaseProcessor.typeSize(spec.type.toString()) == field.size))){
return false;
}
}
return true;
}
}
public static class RevisionField{
String name, type;
int size; //in bytes
RevisionField(String name, String type, int size){
this.name = name;
this.size = size;
this.type = type;
}
RevisionField(){}
}
}

View File

@@ -275,6 +275,8 @@ public class EntityProcess extends BaseProcessor{
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S + $L", name + "#", "id").build());
EntityIO io = new EntityIO(type.name(), builder, serializer, rootDirectory.child("annotations/src/main/resources/revisions").child(name));
//add all methods from components
for(ObjectMap.Entry<String, Array<Smethod>> entry : methods){
if(entry.value.contains(m -> m.has(Replace.class))){
@@ -332,15 +334,7 @@ public class EntityProcess extends BaseProcessor{
//SPECIAL CASE: I/O code
//note that serialization is generated even for non-serializing entities for manual usage
if((first.name().equals("read") || first.name().equals("write")) && ann.genio()){
Array<FieldSpec> fields = Array.with(builder.fieldSpecs).select(spec -> !spec.hasModifier(Modifier.TRANSIENT) && !spec.hasModifier(Modifier.STATIC) && !spec.hasModifier(Modifier.FINAL));
fields.sortComparing(f -> f.name); //sort to keep order
EntityIO writer = new EntityIO(mbuilder, first.name().equals("write"), serializer);
//subclasses *have* to call this method
mbuilder.addAnnotation(CallSuper.class);
//write or read each non-transient field
for(FieldSpec spec : fields){
writer.io(spec.type, "this." + spec.name);
}
io.write(mbuilder, first.name().equals("write"));
}
for(Smethod elem : entry.value){

View File

@@ -45,5 +45,9 @@ public class TypeIOResolver{
this.writers = writers;
this.readers = readers;
}
public boolean has(String type){
return writers.containsKey(type) && readers.containsKey(type);
}
}
}