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){ public static String simpleName(String str){
return str.contains(".") ? str.substring(str.lastIndexOf('.') + 1) : str; return str.contains(".") ? str.substring(str.lastIndexOf('.') + 1) : str;
} }

View File

@@ -1,59 +1,185 @@
package mindustry.annotations.entity; package mindustry.annotations.entity;
import arc.files.*;
import arc.struct.*;
import arc.util.serialization.*;
import com.squareup.javapoet.*; import com.squareup.javapoet.*;
import com.squareup.javapoet.MethodSpec.*; import mindustry.annotations.Annotations.*;
import mindustry.annotations.*; import mindustry.annotations.*;
import mindustry.annotations.util.TypeIOResolver.*; import mindustry.annotations.util.TypeIOResolver.*;
import javax.lang.model.element.*;
import static mindustry.annotations.BaseProcessor.instanceOf; import static mindustry.annotations.BaseProcessor.instanceOf;
public class EntityIO{ public class EntityIO{
final MethodSpec.Builder builder; final static Json json = new Json();
final boolean write;
final ClassSerializer serializer;
EntityIO(Builder builder, boolean write, ClassSerializer serializer){ final ClassSerializer serializer;
this.builder = builder; 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.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()){ //subclasses *have* to call this method
s(type == TypeName.BOOLEAN ? "bool" : type.toString().charAt(0) + "", field); method.addAnnotation(CallSuper.class);
//}else if(type.toString().equals("java.lang.String")){
// s("str", field); if(write){
}else if(instanceOf(type.toString(), "mindustry.ctype.Content")){ //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){ if(write){
s("s", field + ".id"); s("s", field + ".id");
}else{ }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){ }else if(serializer.writers.containsKey(type) && write){
st("$L(write, $L)", serializer.writers.get(type.toString()), field); st("$L(write, $L)", serializer.writers.get(type), field);
}else if(serializer.readers.containsKey(type.toString()) && !write){ }else if(serializer.readers.containsKey(type) && !write){
st("$L = $L(read)", field, serializer.readers.get(type.toString())); st("$L$L(read)", field, serializer.readers.get(type));
} }
} }
private void cont(String text, Object... fmt){ private void cont(String text, Object... fmt){
builder.beginControlFlow(text, fmt); method.beginControlFlow(text, fmt);
} }
private void cont(){ private void econt(){
builder.endControlFlow(); method.endControlFlow();
}
private void ncont(String text, Object... fmt){
method.nextControlFlow(text, fmt);
} }
private void st(String text, Object... args){ private void st(String text, Object... args){
builder.addStatement(text, args); method.addStatement(text, args);
} }
private void s(String type, String field){ private void s(String type, String field){
if(write){ if(write){
builder.addStatement("write.$L($L)", type, field); method.addStatement("write.$L($L)", type, field);
}else{ }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) .addModifiers(Modifier.PUBLIC)
.addStatement("return $S + $L", name + "#", "id").build()); .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 //add all methods from components
for(ObjectMap.Entry<String, Array<Smethod>> entry : methods){ for(ObjectMap.Entry<String, Array<Smethod>> entry : methods){
if(entry.value.contains(m -> m.has(Replace.class))){ if(entry.value.contains(m -> m.has(Replace.class))){
@@ -332,15 +334,7 @@ public class EntityProcess extends BaseProcessor{
//SPECIAL CASE: I/O code //SPECIAL CASE: I/O code
//note that serialization is generated even for non-serializing entities for manual usage //note that serialization is generated even for non-serializing entities for manual usage
if((first.name().equals("read") || first.name().equals("write")) && ann.genio()){ 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)); io.write(mbuilder, first.name().equals("write"));
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);
}
} }
for(Smethod elem : entry.value){ for(Smethod elem : entry.value){

View File

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

View File

@@ -1,22 +0,0 @@
#Maps entity names to IDs. Autogenerated.
mindustry.entities.def.PuddleComp=12
mindustry.entities.def.BulletComp=13
dagger=7
mindustry.entities.AllEntities.GenericBuilderDef=6
mindustry.entities.AllEntities.BulletDef=0
mindustry.entities.AllEntities.PlayerDef=4
mindustry.entities.AllEntities.GroundEffectDef=9
mindustry.entities.AllEntities.FireDef=11
mindustry.entities.AllEntities.EffectDef=2
mindustry.entities.AllEntities.GenericUnitDef=5
mindustry.entities.AllEntities.TileDef=3
vanguard=10
mindustry.entities.def.PlayerComp=17
dagger2=8
mindustry.entities.def.DecalComp=14
mindustry.entities.AllEntities.DecalDef=1
mindustry.entities.def.StandardEffectComp=18
mindustry.entities.def.TileComp=19
mindustry.entities.def.FireComp=15
mindustry.entities.def.GroundEffectComp=16

View File

@@ -9,9 +9,10 @@ import mindustry.gen.*;
abstract class HealthComp implements Entityc{ abstract class HealthComp implements Entityc{
static final float hitDuration = 9f; static final float hitDuration = 9f;
float health;
transient float hitTime; transient float hitTime;
float health, maxHealth = 1f; transient float maxHealth = 1f;
boolean dead; transient boolean dead;
boolean isValid(){ boolean isValid(){
return !dead && isAdded(); return !dead && isAdded();

View File

@@ -732,7 +732,7 @@ public class ServerControl implements ApplicationListener{
Core.app.post(() -> { Core.app.post(() -> {
try{ try{
SaveIO.load(file); SaveIO.load(file);
state.rules.zone = null; state.rules.sector = null;
info("Save loaded."); info("Save loaded.");
state.set(State.playing); state.set(State.playing);
netServer.openServer(); netServer.openServer();