diff --git a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java index c341349971..37cbff235f 100644 --- a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java @@ -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; } diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java b/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java index 1d116cbb97..93b07ace1a 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityIO.java @@ -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 revisions = new Array<>(); + + boolean write; + MethodSpec.Builder method; + ObjectSet 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 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 fields; + + Revision(int version, Array fields){ + this.version = version; + this.fields = fields; + } + + Revision(){} + + /** @return whether these two revisions are compatible */ + boolean equal(Array 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(){} + } } diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java index 8a39f97804..4affd92b7e 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java @@ -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> 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 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){ diff --git a/annotations/src/main/java/mindustry/annotations/util/TypeIOResolver.java b/annotations/src/main/java/mindustry/annotations/util/TypeIOResolver.java index 450409f689..d0359ba51c 100644 --- a/annotations/src/main/java/mindustry/annotations/util/TypeIOResolver.java +++ b/annotations/src/main/java/mindustry/annotations/util/TypeIOResolver.java @@ -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); + } } } diff --git a/annotations/src/main/resources/classids.properties b/annotations/src/main/resources/classids.properties deleted file mode 100644 index 3f4db7471a..0000000000 --- a/annotations/src/main/resources/classids.properties +++ /dev/null @@ -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 \ No newline at end of file diff --git a/core/src/mindustry/entities/def/HealthComp.java b/core/src/mindustry/entities/def/HealthComp.java index 18001d55df..c7bd269666 100644 --- a/core/src/mindustry/entities/def/HealthComp.java +++ b/core/src/mindustry/entities/def/HealthComp.java @@ -9,9 +9,10 @@ import mindustry.gen.*; abstract class HealthComp implements Entityc{ static final float hitDuration = 9f; + float health; transient float hitTime; - float health, maxHealth = 1f; - boolean dead; + transient float maxHealth = 1f; + transient boolean dead; boolean isValid(){ return !dead && isAdded(); diff --git a/server/src/mindustry/server/ServerControl.java b/server/src/mindustry/server/ServerControl.java index 30196e9257..8dac088d17 100644 --- a/server/src/mindustry/server/ServerControl.java +++ b/server/src/mindustry/server/ServerControl.java @@ -732,7 +732,7 @@ public class ServerControl implements ApplicationListener{ Core.app.post(() -> { try{ SaveIO.load(file); - state.rules.zone = null; + state.rules.sector = null; info("Save loaded."); state.set(State.playing); netServer.openServer();