diff --git a/annotations/src/main/java/io/anuke/annotations/SerializeAnnotationProcessor.java b/annotations/src/main/java/io/anuke/annotations/SerializeAnnotationProcessor.java index 43f366c3f9..3b38b842ee 100644 --- a/annotations/src/main/java/io/anuke/annotations/SerializeAnnotationProcessor.java +++ b/annotations/src/main/java/io/anuke/annotations/SerializeAnnotationProcessor.java @@ -13,9 +13,7 @@ import java.util.List; import java.util.Set; @SupportedSourceVersion(SourceVersion.RELEASE_8) -@SupportedAnnotationTypes({ -"io.anuke.annotations.Annotations.Serialize" -}) +@SupportedAnnotationTypes("io.anuke.annotations.Annotations.Serialize") public class SerializeAnnotationProcessor extends AbstractProcessor{ /** Target class name. */ private static final String className = "Serialization"; @@ -44,6 +42,28 @@ public class SerializeAnnotationProcessor extends AbstractProcessor{ TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC); classBuilder.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "\"unchecked\"").build()); classBuilder.addJavadoc(RemoteMethodAnnotationProcessor.autogenWarning); + + + classBuilder.addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.bestGuess("io.anuke.arc.collection.ObjectMap"), + ClassName.get(Class.class), + ClassName.bestGuess("io.anuke.mindustry.io.JsonTypeWriter")), + "writers", Modifier.PRIVATE, Modifier.STATIC).initializer("new io.anuke.arc.collection.ObjectMap<>()").build()); + + classBuilder.addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.bestGuess("io.anuke.arc.collection.ObjectMap"), + ClassName.get(Class.class), + ClassName.bestGuess("io.anuke.mindustry.io.JsonTypeReader")), + "readers", Modifier.PRIVATE, Modifier.STATIC).initializer("new io.anuke.arc.collection.ObjectMap<>()").build()); + + classBuilder.addMethod(MethodSpec.methodBuilder("setSerializer") + .addModifiers(Modifier.STATIC, Modifier.PUBLIC) + .addTypeVariable(TypeVariableName.get("T")) + .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), TypeVariableName.get("T")), "type") + .addParameter(ParameterizedTypeName.get(ClassName.bestGuess("io.anuke.mindustry.io.JsonTypeWriter"), TypeVariableName.get("T")), "writer") + .addParameter(ParameterizedTypeName.get(ClassName.bestGuess("io.anuke.mindustry.io.JsonTypeReader"), TypeVariableName.get("T")), "reader") + .addStatement("writers.put(type, writer)") + .addStatement("readers.put(type, reader)").build()); + + MethodSpec.Builder method = MethodSpec.methodBuilder("init").addModifiers(Modifier.PUBLIC, Modifier.STATIC); TypeName jsonType = ClassName.bestGuess("io.anuke.arc.util.serialization.Json"); @@ -107,6 +127,32 @@ public class SerializeAnnotationProcessor extends AbstractProcessor{ }else{ writeMethod.addStatement("io.anuke.arc.Core.settings.getSerializer(" + typeName + ".class).write(stream, object." + name + ")"); readMethod.addStatement("object." + name + " = (" + typeName + ")io.anuke.arc.Core.settings.getSerializer(" + typeName + ".class).read(stream)"); + + if(field.asType().toString().equalsIgnoreCase("java.lang.String")){ + jsonWriteMethod.addStatement("json.writeValue(\"" + name + "\", object." + name + ")"); + jsonReadMethod.addStatement("if(value.has(\"" + name + "\")) object." + name + "= value.getString(\"" + name + "\")"); + }else if(field.asType().toString().startsWith("io.anuke.arc.collection.Array")){ //oh boy here it begins + String genericType = field.asType().toString().substring(field.asType().toString().indexOf('<') + 1, field.asType().toString().indexOf('>')); + { + jsonWriteMethod.addStatement("json.writeArrayStart($S)", name) + .beginControlFlow("for(" + genericType + " item : object." + name + ")") + .addStatement("json.writeValue(item)") + .endControlFlow() + .addStatement("json.writeArrayEnd()"); + } + + { + //jsonWriteMethod.beginControlFlow("if(value.has($S))", name); + //jsonWriteMethod.addStatement("io.anuke.arc.util.serialization.JsonValue list = value.get($S)", name); + //jsonWriteMethod.endControlFlow(); + } + //jsonWriteMethod.addStatement("for( ") + }else{ + jsonWriteMethod.addStatement("if(object."+name+" != null) writers.getThrow("+typeName+".class, () -> new IllegalArgumentException(\"Class '" + + typeName + "' does not have a serializer!\")).write(bjson, object."+name+", \"" + name+"\")"); + jsonReadMethod.addStatement("if(value.has(\"" + name + "\")) object." + name + " = ("+typeName+")readers.getThrow("+typeName+".class, () -> new IllegalArgumentException(\"Class '" + + typeName + "' does not have a serializer!\")).read(value, \""+name+"\")"); + } } } diff --git a/core/src/io/anuke/mindustry/core/Logic.java b/core/src/io/anuke/mindustry/core/Logic.java index a81d3c2702..76f57f9ad9 100644 --- a/core/src/io/anuke/mindustry/core/Logic.java +++ b/core/src/io/anuke/mindustry/core/Logic.java @@ -60,9 +60,10 @@ public class Logic implements ApplicationListener{ state.wavetime = state.rules.waveSpacing * 2; //grace period of 2x wave time before game starts //sometimes a map has no waves defined, they're defined in the zone rules - if(world.getMap().getWaves() != DefaultWaves.get() || !world.isZone()){ - state.rules.spawns = world.getMap().getWaves(); - } + //TODO ???? how does this even work now + //if(world.getMap().getWaves() != DefaultWaves.get() || !world.isZone()){ + // state.rules.spawns = world.getMap().getWaves(); + //} Events.fire(new PlayEvent()); } diff --git a/core/src/io/anuke/mindustry/core/World.java b/core/src/io/anuke/mindustry/core/World.java index c303fd4c75..30c1a1c1ef 100644 --- a/core/src/io/anuke/mindustry/core/World.java +++ b/core/src/io/anuke/mindustry/core/World.java @@ -35,7 +35,9 @@ public class World implements ApplicationListener{ private boolean generating, invalidMap; public World(){ + //TODO swap Core.app.post(maps::load); + //maps.load(); } @Override diff --git a/core/src/io/anuke/mindustry/game/Stats.java b/core/src/io/anuke/mindustry/game/Stats.java index 7e00d05d44..ebaa62dfc6 100644 --- a/core/src/io/anuke/mindustry/game/Stats.java +++ b/core/src/io/anuke/mindustry/game/Stats.java @@ -9,7 +9,7 @@ import io.anuke.mindustry.type.*; @Serialize public class Stats{ /** Items delivered to global resoure counter. Zones only. */ - public ObjectIntMap itemsDelivered = new ObjectIntMap<>(); + public transient ObjectIntMap itemsDelivered = new ObjectIntMap<>(); /** Enemy (red team) units destroyed. */ public int enemyUnitsDestroyed; /** Total waves lasted. */ diff --git a/core/src/io/anuke/mindustry/io/JsonIO.java b/core/src/io/anuke/mindustry/io/JsonIO.java new file mode 100644 index 0000000000..ff5c736a13 --- /dev/null +++ b/core/src/io/anuke/mindustry/io/JsonIO.java @@ -0,0 +1,24 @@ +package io.anuke.mindustry.io; + +import io.anuke.arc.util.serialization.Json; +import io.anuke.mindustry.game.Rules; +import io.anuke.mindustry.game.SpawnGroup; + +public class JsonIO{ + private static Json json = new Json(){{ + setIgnoreUnknownFields(true); + setElementType(Rules.class, "spawns", SpawnGroup.class); + }}; + + public static String write(Object object){ + return json.toJson(object); + } + + public static T read(Class type, String string){ + return json.fromJson(type, string); + } + + public static String print(String in){ + return json.prettyPrint(in); + } +} diff --git a/core/src/io/anuke/mindustry/io/JsonTypeReader.java b/core/src/io/anuke/mindustry/io/JsonTypeReader.java new file mode 100644 index 0000000000..5d91e1a896 --- /dev/null +++ b/core/src/io/anuke/mindustry/io/JsonTypeReader.java @@ -0,0 +1,7 @@ +package io.anuke.mindustry.io; + +import io.anuke.arc.util.serialization.JsonValue; + +public interface JsonTypeReader{ + T read(JsonValue json, String name); +} diff --git a/core/src/io/anuke/mindustry/io/JsonTypeWriter.java b/core/src/io/anuke/mindustry/io/JsonTypeWriter.java new file mode 100644 index 0000000000..116a9a7857 --- /dev/null +++ b/core/src/io/anuke/mindustry/io/JsonTypeWriter.java @@ -0,0 +1,7 @@ +package io.anuke.mindustry.io; + +import io.anuke.arc.util.serialization.Json; + +public interface JsonTypeWriter{ + void write(Json json, T object, String name); +} diff --git a/core/src/io/anuke/mindustry/io/LegacyMapIO.java b/core/src/io/anuke/mindustry/io/LegacyMapIO.java index d002a4d0f8..ec86df1773 100644 --- a/core/src/io/anuke/mindustry/io/LegacyMapIO.java +++ b/core/src/io/anuke/mindustry/io/LegacyMapIO.java @@ -6,7 +6,9 @@ import io.anuke.arc.graphics.Color; import io.anuke.arc.graphics.Pixmap; import io.anuke.arc.util.Pack; import io.anuke.arc.util.Structs; +import io.anuke.arc.util.serialization.Json; import io.anuke.mindustry.content.Blocks; +import io.anuke.mindustry.game.SpawnGroup; import io.anuke.mindustry.game.Team; import io.anuke.mindustry.io.MapIO.TileProvider; import io.anuke.mindustry.maps.Map; @@ -25,16 +27,22 @@ import static io.anuke.mindustry.Vars.*; * Differentiate between legacy maps and new maps by checking the extension (or the header).*/ public class LegacyMapIO{ private static final ObjectMap fallback = ObjectMap.of("alpha-dart-mech-pad", "dart-mech-pad"); + private static final Json json = new Json(); /* Convert a map from the old format to the new format. */ public static void convertMap(FileHandle in, FileHandle out) throws IOException{ Map map = readMap(in, true); + + String waves = map.tags.get("waves", "[]"); + Array groups = new Array<>(json.fromJson(SpawnGroup[].class, waves)); + Tile[][] tiles = world.createTiles(map.width, map.height); for(int x = 0; x < map.width; x++){ for(int y = 0; y < map.height; y++){ - tiles[x][y] = new Tile(x, y); + tiles[x][y] = new CachedTile(); } } + state.rules.spawns = groups; readTiles(map, tiles); MapIO.writeMap(out, map); } diff --git a/core/src/io/anuke/mindustry/io/SaveFileReader.java b/core/src/io/anuke/mindustry/io/SaveFileReader.java index e761a0d00e..a559430afd 100644 --- a/core/src/io/anuke/mindustry/io/SaveFileReader.java +++ b/core/src/io/anuke/mindustry/io/SaveFileReader.java @@ -11,6 +11,8 @@ import java.io.*; public abstract class SaveFileReader{ protected final ReusableByteOutStream byteOutput = new ReusableByteOutStream(); protected final DataOutputStream dataBytes = new DataOutputStream(byteOutput); + protected final ReusableByteOutStream byteOutputSmall = new ReusableByteOutStream(); + protected final DataOutputStream dataBytesSmall = new DataOutputStream(byteOutputSmall); protected final ObjectMap fallback = ObjectMap.of(); protected void region(String name, DataInput stream, CounterInputStream counter, IORunner cons) throws IOException{ @@ -21,6 +23,7 @@ public abstract class SaveFileReader{ }catch(Throwable e){ throw new IOException("Error reading region \"" + name + "\".", e); } + if(length != counter.count() - 4){ throw new IOException("Error reading region \"" + name + "\": read length mismatch. Expected: " + length + "; Actual: " + (counter.count() - 4)); } @@ -40,11 +43,12 @@ public abstract class SaveFileReader{ /** Write a chunk of input to the stream. An integer of some length is written first, followed by the data. */ public void writeChunk(DataOutput output, boolean isByte, IORunner runner) throws IOException{ + ReusableByteOutStream dout = isByte ? byteOutputSmall : byteOutput; //reset output position - byteOutput.reset(); + dout.reset(); //write the needed info - runner.accept(dataBytes); - int length = byteOutput.size(); + runner.accept(isByte ? dataBytesSmall : dataBytes); + int length = dout.size(); //write length (either int or byte) followed by the output bytes if(!isByte){ output.writeInt(length); @@ -54,7 +58,7 @@ public abstract class SaveFileReader{ } output.writeShort(length); } - output.write(byteOutput.getBytes(), 0, length); + output.write(dout.getBytes(), 0, length); } public int readChunk(DataInput input, IORunner runner) throws IOException{ diff --git a/core/src/io/anuke/mindustry/io/SaveIO.java b/core/src/io/anuke/mindustry/io/SaveIO.java index 628217cc98..05914d4299 100644 --- a/core/src/io/anuke/mindustry/io/SaveIO.java +++ b/core/src/io/anuke/mindustry/io/SaveIO.java @@ -90,6 +90,7 @@ public class SaveIO{ public static SaveMeta getMeta(DataInputStream stream){ try{ + readHeader(stream); int version = stream.readInt(); SaveMeta meta = versions.get(version).getMeta(stream); stream.close(); @@ -116,10 +117,7 @@ public class SaveIO{ } public static void write(OutputStream os, StringMap tags){ - DataOutputStream stream; - - try{ - stream = new DataOutputStream(os); + try(DataOutputStream stream = new DataOutputStream(os)){ stream.write(header); stream.writeInt(getVersion().version); if(tags == null){ @@ -127,7 +125,6 @@ public class SaveIO{ }else{ getVersion().write(stream, tags); } - stream.close(); }catch(Exception e){ throw new RuntimeException(e); } diff --git a/core/src/io/anuke/mindustry/io/SaveVersion.java b/core/src/io/anuke/mindustry/io/SaveVersion.java index 5d513971a0..7026bdd549 100644 --- a/core/src/io/anuke/mindustry/io/SaveVersion.java +++ b/core/src/io/anuke/mindustry/io/SaveVersion.java @@ -8,7 +8,6 @@ import io.anuke.mindustry.entities.Entities; import io.anuke.mindustry.entities.EntityGroup; import io.anuke.mindustry.entities.traits.*; import io.anuke.mindustry.game.*; -import io.anuke.mindustry.gen.Serialization; import io.anuke.mindustry.type.ContentType; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; @@ -27,7 +26,7 @@ public abstract class SaveVersion extends SaveFileReader{ public SaveMeta getMeta(DataInput stream) throws IOException{ stream.readInt(); //length of data, doesn't matter here StringMap map = readStringMap(stream); - return new SaveMeta(map.getInt("version"), map.getLong("saved"), map.getLong("playtime"), map.getInt("build"), map.get("mapname"), map.getInt("wave"), Serialization.readRulesStringJson(map.get("rules", "{}"))); + return new SaveMeta(map.getInt("version"), map.getLong("saved"), map.getLong("playtime"), map.getInt("build"), map.get("mapname"), map.getInt("wave"), JsonIO.read(Rules.class, map.get("rules", "{}"))); } public final void write(DataOutputStream stream) throws IOException{ @@ -56,8 +55,8 @@ public abstract class SaveVersion extends SaveFileReader{ "mapname", world.getMap() == null ? "unknown" : world.getMap().name(), "wave", state.wave, "wavetime", state.wavetime, - "stats", Serialization.writeStatsJson(state.stats), - "rules", Serialization.writeRulesJson(state.rules), + "stats", JsonIO.write(state.stats), + "rules", JsonIO.write(state.rules), "width", world.width(), "height", world.height() ).merge(tags)); @@ -68,11 +67,12 @@ public abstract class SaveVersion extends SaveFileReader{ state.wave = map.getInt("wave"); state.wavetime = map.getFloat("wavetime", state.rules.waveSpacing); - state.stats = Serialization.readStatsStringJson(map.get("stats", "{}")); - state.rules = Serialization.readRulesStringJson(map.get("rules", "{}")); + state.stats = JsonIO.read(Stats.class, map.get("stats", "{}")); + state.rules = JsonIO.read(Rules.class, map.get("rules", "{}")); } public void writeMap(DataOutput stream) throws IOException{ + //TODO something here messes up everything //write world size stream.writeShort(world.width()); stream.writeShort(world.height()); @@ -106,7 +106,7 @@ public abstract class SaveVersion extends SaveFileReader{ if(tile.entity != null){ writeChunk(stream, true, out -> { out.writeByte(tile.entity.version()); - tile.entity.write(stream); + tile.entity.write(out); }); }else{ //write consecutive non-entity blocks @@ -129,10 +129,12 @@ public abstract class SaveVersion extends SaveFileReader{ } public void readMap(DataInput stream) throws IOException{ - short width = stream.readShort(); - short height = stream.readShort(); + int width = stream.readUnsignedShort(); + int height = stream.readUnsignedShort(); - world.beginMapLoad(); + boolean generating = world.isGenerating(); + + if(!generating) world.beginMapLoad(); Tile[][] tiles = world.createTiles(width, height); @@ -163,7 +165,7 @@ public abstract class SaveVersion extends SaveFileReader{ if(tile.entity != null){ readChunk(stream, true, in -> { byte version = in.readByte(); - tile.entity.read(stream, version); + tile.entity.read(in, version); }); }else{ int consecutives = stream.readUnsignedByte(); @@ -178,7 +180,7 @@ public abstract class SaveVersion extends SaveFileReader{ } content.setTemporaryMapper(null); - world.endMapLoad(); + if(!generating) world.endMapLoad(); } public void writeEntities(DataOutput stream) throws IOException{ @@ -200,8 +202,8 @@ public abstract class SaveVersion extends SaveFileReader{ SaveTrait save = (SaveTrait)entity; //each entity is a separate chunk. writeChunk(stream, true, out -> { - stream.writeByte(save.getTypeID()); - stream.writeByte(save.version()); + out.writeByte(save.getTypeID()); + out.writeByte(save.version()); save.writeSave(out); }); } @@ -217,10 +219,10 @@ public abstract class SaveVersion extends SaveFileReader{ for(int j = 0; j < amount; j++){ //TODO throw exception on read fail readChunk(stream, true, in -> { - byte typeid = stream.readByte(); - byte version = stream.readByte(); + byte typeid = in.readByte(); + byte version = in.readByte(); SaveTrait trait = (SaveTrait)TypeTrait.getTypeByID(typeid).get(); - trait.readSave(stream, version); + trait.readSave(in, version); }); } } diff --git a/core/src/io/anuke/mindustry/maps/Map.java b/core/src/io/anuke/mindustry/maps/Map.java index 6765528ec0..920adc89ca 100644 --- a/core/src/io/anuke/mindustry/maps/Map.java +++ b/core/src/io/anuke/mindustry/maps/Map.java @@ -1,15 +1,10 @@ package io.anuke.mindustry.maps; import io.anuke.arc.Core; -import io.anuke.arc.collection.*; +import io.anuke.arc.collection.StringMap; import io.anuke.arc.files.FileHandle; import io.anuke.arc.graphics.Texture; -import io.anuke.arc.util.Log; import io.anuke.mindustry.Vars; -import io.anuke.mindustry.game.DefaultWaves; -import io.anuke.mindustry.game.SpawnGroup; - -import static io.anuke.mindustry.Vars.world; public class Map implements Comparable{ /** Whether this is a custom map. */ @@ -49,20 +44,6 @@ public class Map implements Comparable{ this(Vars.customMapDirectory.child(tags.get("name", "unknown")), 0, 0, tags, true); } - public Array getWaves(){ - if(tags.containsKey("waves")){ - try{ - return world.maps.readWaves(tags.get("waves")); - }catch(Exception e){ - Log.err("Malformed waves: {0}", tags.get("waves")); - e.printStackTrace(); - return DefaultWaves.get(); - } - }else{ - return DefaultWaves.get(); - } - } - public int getHightScore(){ return Core.settings.getInt("hiscore" + file.nameWithoutExtension(), 0); } diff --git a/core/src/io/anuke/mindustry/maps/Maps.java b/core/src/io/anuke/mindustry/maps/Maps.java index c5b03e6d2c..587182c871 100644 --- a/core/src/io/anuke/mindustry/maps/Maps.java +++ b/core/src/io/anuke/mindustry/maps/Maps.java @@ -8,8 +8,7 @@ import io.anuke.arc.util.Disposable; import io.anuke.arc.util.Log; import io.anuke.arc.util.serialization.Json; import io.anuke.mindustry.game.SpawnGroup; -import io.anuke.mindustry.io.LegacyMapIO; -import io.anuke.mindustry.io.MapIO; +import io.anuke.mindustry.io.*; import java.io.IOException; import java.io.StringWriter; @@ -61,6 +60,7 @@ public class Maps implements Disposable{ public void load(){ try{ //TODO remove, this is only for testing + for(FileHandle in : Core.files.absolute("/home/anuke/Projects/Mindustry/core/assets/maps").list()){ if(in.extension().equalsIgnoreCase(oldMapExtension)){ Log.info("Converting {0}...", in); diff --git a/core/src/io/anuke/mindustry/maps/generators/MapGenerator.java b/core/src/io/anuke/mindustry/maps/generators/MapGenerator.java index f0fab14dea..aa318bf072 100644 --- a/core/src/io/anuke/mindustry/maps/generators/MapGenerator.java +++ b/core/src/io/anuke/mindustry/maps/generators/MapGenerator.java @@ -60,10 +60,9 @@ public class MapGenerator extends Generator{ @Override public void init(Loadout loadout){ this.loadout = loadout; - //TODO uncomment once conversion works - //map = world.maps.loadInternalMap(mapName); - //width = map.width; - //height = map.height; + map = world.maps.loadInternalMap(mapName); + width = map.width; + height = map.height; } @Override diff --git a/core/src/io/anuke/mindustry/type/Zone.java b/core/src/io/anuke/mindustry/type/Zone.java index 74ec3d9183..9951c1939b 100644 --- a/core/src/io/anuke/mindustry/type/Zone.java +++ b/core/src/io/anuke/mindustry/type/Zone.java @@ -13,6 +13,7 @@ import io.anuke.mindustry.game.EventType.ZoneConfigureCompleteEvent; import io.anuke.mindustry.game.EventType.ZoneRequireCompleteEvent; import io.anuke.mindustry.game.Rules; import io.anuke.mindustry.game.UnlockableContent; +import io.anuke.mindustry.gen.Serialization; import io.anuke.mindustry.maps.generators.Generator; import io.anuke.mindustry.world.Block; @@ -38,6 +39,12 @@ public class Zone extends UnlockableContent{ private Array defaultStartingItems = new Array<>(); + static{ + Serialization.setSerializer(Zone.class, + (json, zone, name) -> json.writeValue(name, zone.name), + (json, name) -> content.getByName(ContentType.zone, json.getString(name))); + } + public Zone(String name, Generator generator){ super(name); this.generator = generator; diff --git a/core/src/io/anuke/mindustry/world/CachedTile.java b/core/src/io/anuke/mindustry/world/CachedTile.java index 75dded63db..05f3b16144 100644 --- a/core/src/io/anuke/mindustry/world/CachedTile.java +++ b/core/src/io/anuke/mindustry/world/CachedTile.java @@ -37,6 +37,7 @@ public class CachedTile extends Tile{ if(!entities.containsKey(block.id)){ TileEntity n = block.newEntity(); n.cons = new ConsumeModule(entity); + n.tile = this; if(block.hasItems) n.items = new ItemModule(); if(block.hasLiquids) n.liquids = new LiquidModule(); if(block.hasPower) n.power = new PowerModule();