it is done

This commit is contained in:
Anuken
2019-12-25 01:39:38 -05:00
parent 5b21873f3c
commit 514d4817c8
488 changed files with 4572 additions and 4574 deletions

View File

@@ -0,0 +1,151 @@
package mindustry.io;
import arc.util.serialization.*;
import arc.util.serialization.Json.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.ctype.ContentType;
import mindustry.game.*;
import mindustry.type.*;
import mindustry.world.*;
import java.io.*;
@SuppressWarnings("unchecked")
public class JsonIO{
private static CustomJson jsonBase = new CustomJson();
private static Json json = new Json(){
{ apply(this); }
@Override
public void writeValue(Object value, Class knownType, Class elementType){
if(value instanceof mindustry.ctype.MappableContent){
try{
getWriter().value(((MappableContent)value).name);
}catch(IOException e){
throw new RuntimeException(e);
}
}else{
super.writeValue(value, knownType, elementType);
}
}
@Override
protected String convertToString(Object object){
if(object instanceof MappableContent){
return ((MappableContent)object).name;
}
return super.convertToString(object);
}
};
public static Json json(){
return json;
}
public static String write(Object object){
return json.toJson(object, object.getClass());
}
public static <T> T copy(T object){
return read((Class<T>)object.getClass(), write(object));
}
public static <T> T read(Class<T> type, String string){
return json.fromJson(type, string);
}
public static <T> T read(Class<T> type, T base, String string){
return jsonBase.fromBaseJson(type, base, string);
}
public static String print(String in){
return json.prettyPrint(in);
}
private static void apply(Json json){
json.setIgnoreUnknownFields(true);
json.setElementType(Rules.class, "spawns", SpawnGroup.class);
json.setElementType(Rules.class, "loadout", ItemStack.class);
json.setSerializer(Zone.class, new Serializer<Zone>(){
@Override
public void write(Json json, Zone object, Class knownType){
json.writeValue(object.name);
}
@Override
public Zone read(Json json, JsonValue jsonData, Class type){
return Vars.content.getByName(ContentType.zone, jsonData.asString());
}
});
json.setSerializer(Item.class, new Serializer<Item>(){
@Override
public void write(Json json, Item object, Class knownType){
json.writeValue(object.name);
}
@Override
public Item read(Json json, JsonValue jsonData, Class type){
if(jsonData.asString() == null) return Items.copper;
Item i = Vars.content.getByName(ContentType.item, jsonData.asString());
return i == null ? Items.copper : i;
}
});
json.setSerializer(Block.class, new Serializer<Block>(){
@Override
public void write(Json json, Block object, Class knownType){
json.writeValue(object.name);
}
@Override
public Block read(Json json, JsonValue jsonData, Class type){
return Vars.content.getByName(ContentType.block, jsonData.asString());
}
});
json.setSerializer(ItemStack.class, new Serializer<ItemStack>(){
@Override
public void write(Json json, ItemStack object, Class knownType){
json.writeObjectStart();
json.writeValue("item", object.item);
json.writeValue("amount", object.amount);
json.writeObjectEnd();
}
@Override
public ItemStack read(Json json, JsonValue jsonData, Class type){
return new ItemStack(json.getSerializer(Item.class).read(json, jsonData.get("item"), Item.class), jsonData.getInt("amount"));
}
});
}
static class CustomJson extends Json{
private Object baseObject;
{
apply(this);
}
@Override
public <T> T fromJson(Class<T> type, String json){
return fromBaseJson(type, null, json);
}
public <T> T fromBaseJson(Class<T> type, T base, String json){
this.baseObject = base;
return readValue(type, null, new JsonReader().parse(json));
}
@Override
protected Object newInstance(Class type){
if(baseObject == null || baseObject.getClass() != type){
return super.newInstance(type);
}
return baseObject;
}
}
}

View File

@@ -0,0 +1,214 @@
package mindustry.io;
import arc.struct.*;
import arc.files.*;
import arc.graphics.*;
import arc.util.*;
import arc.util.serialization.*;
import mindustry.content.*;
import mindustry.ctype.ContentType;
import mindustry.game.*;
import mindustry.io.MapIO.*;
import mindustry.maps.*;
import mindustry.world.*;
import mindustry.world.LegacyColorMapper.*;
import mindustry.world.blocks.*;
import java.io.*;
import java.util.zip.*;
import static mindustry.Vars.*;
/** Map IO for the "old" .mmap format.
* Differentiate between legacy maps and new maps by checking the extension (or the header).*/
public class LegacyMapIO{
private static final ObjectMap<String, String> 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(Fi in, Fi out) throws IOException{
Map map = readMap(in, true);
String waves = map.tags.get("waves", "[]");
Array<SpawnGroup> 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 CachedTile();
tiles[x][y].x = (short)x;
tiles[x][y].y = (short)y;
}
}
state.rules.spawns = groups;
readTiles(map, tiles);
MapIO.writeMap(out, map);
}
public static Map readMap(Fi file, boolean custom) throws IOException{
try(DataInputStream stream = new DataInputStream(file.read(1024))){
StringMap tags = new StringMap();
//meta is uncompressed
int version = stream.readInt();
if(version != 1){
throw new IOException("Outdated legacy map format");
}
int build = stream.readInt();
short width = stream.readShort(), height = stream.readShort();
byte tagAmount = stream.readByte();
for(int i = 0; i < tagAmount; i++){
String name = stream.readUTF();
String value = stream.readUTF();
tags.put(name, value);
}
return new Map(file, width, height, tags, custom, version, build);
}
}
public static void readTiles(Map map, Tile[][] tiles) throws IOException{
readTiles(map, (x, y) -> tiles[x][y]);
}
public static void readTiles(Map map, TileProvider tiles) throws IOException{
readTiles(map.file, map.width, map.height, tiles);
}
private static void readTiles(Fi file, int width, int height, Tile[][] tiles) throws IOException{
readTiles(file, width, height, (x, y) -> tiles[x][y]);
}
private static void readTiles(Fi file, int width, int height, TileProvider tiles) throws IOException{
try(BufferedInputStream input = file.read(bufferSize)){
//read map
{
DataInputStream stream = new DataInputStream(input);
stream.readInt(); //version
stream.readInt(); //build
stream.readInt(); //width + height
byte tagAmount = stream.readByte();
for(int i = 0; i < tagAmount; i++){
stream.readUTF(); //key
stream.readUTF(); //val
}
}
try(DataInputStream stream = new DataInputStream(new InflaterInputStream(input))){
try{
byte mapped = stream.readByte();
IntMap<Block> idmap = new IntMap<>();
IntMap<String> namemap = new IntMap<>();
for(int i = 0; i < mapped; i++){
byte type = stream.readByte();
short total = stream.readShort();
for(int j = 0; j < total; j++){
String name = stream.readUTF();
if(type == 1){
Block res = content.getByName(ContentType.block, fallback.get(name, name));
idmap.put(j, res == null ? Blocks.air : res);
namemap.put(j, fallback.get(name, name));
}
}
}
//read floor and create tiles first
for(int i = 0; i < width * height; i++){
int x = i % width, y = i / width;
int floorid = stream.readUnsignedByte();
int oreid = stream.readUnsignedByte();
int consecutives = stream.readUnsignedByte();
Tile tile = tiles.get(x, y);
tile.setFloor((Floor)idmap.get(floorid));
tile.setOverlay(idmap.get(oreid));
for(int j = i + 1; j < i + 1 + consecutives; j++){
int newx = j % width, newy = j / width;
Tile newTile = tiles.get(newx, newy);
newTile.setFloor((Floor)idmap.get(floorid));
newTile.setOverlay(idmap.get(oreid));
}
i += consecutives;
}
//read blocks
for(int i = 0; i < width * height; i++){
int x = i % width, y = i / width;
int id = stream.readUnsignedByte();
Block block = idmap.get(id);
if(block == null) block = Blocks.air;
Tile tile = tiles.get(x, y);
//the spawn block is saved in the block tile layer in older maps, shift it to the overlay
if(block != Blocks.spawn){
tile.setBlock(block);
}else{
tile.setOverlay(block);
}
if(namemap.get(id, "").equals("part")){
stream.readByte(); //link
}else if(tile.entity != null){
byte tr = stream.readByte();
stream.readShort(); //read health (which is actually irrelevant)
byte team = Pack.leftByte(tr);
byte rotation = Pack.rightByte(tr);
tile.setTeam(Team.all[team]);
tile.entity.health = tile.block().health;
tile.rotation(rotation);
if(tile.block() == Blocks.liquidSource || tile.block() == Blocks.unloader || tile.block() == Blocks.sorter){
stream.readByte(); //these blocks have an extra config byte, read it
}
}else{ //no entity/part, read consecutives
int consecutives = stream.readUnsignedByte();
for(int j = i + 1; j < i + 1 + consecutives; j++){
int newx = j % width, newy = j / width;
tiles.get(newx, newy).setBlock(block);
}
i += consecutives;
}
}
}finally{
content.setTemporaryMapper(null);
}
}
}
}
/** Reads a pixmap in the 3.5 pixmap format. */
public static void readPixmap(Pixmap pixmap, Tile[][] tiles){
for(int x = 0; x < pixmap.getWidth(); x++){
for(int y = 0; y < pixmap.getHeight(); y++){
int color = pixmap.getPixel(x, pixmap.getHeight() - 1 - y);
LegacyBlock block = LegacyColorMapper.get(color);
Tile tile = tiles[x][y];
tile.setFloor(block.floor);
tile.setBlock(block.wall);
if(block.ore != null) tile.setOverlay(block.ore);
//place core
if(color == Color.rgba8888(Color.green)){
//actual core parts
tile.setBlock(Blocks.coreShard);
tile.setTeam(Team.sharded);
}
}
}
}
}

View File

@@ -0,0 +1,156 @@
package mindustry.io;
import arc.struct.*;
import arc.files.*;
import arc.graphics.*;
import arc.graphics.Pixmap.*;
import arc.util.io.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.game.*;
import mindustry.maps.*;
import mindustry.world.*;
import mindustry.world.blocks.storage.*;
import java.io.*;
import java.util.zip.*;
import static mindustry.Vars.*;
/** Reads and writes map files. */
//TODO does this class even need to exist??? move to Maps?
public class MapIO{
private static final int[] pngHeader = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
public static boolean isImage(Fi file){
try(InputStream stream = file.read(32)){
for(int i1 : pngHeader){
if(stream.read() != i1){
return false;
}
}
return true;
}catch(IOException e){
return false;
}
}
public static Map createMap(Fi file, boolean custom) throws IOException{
try(InputStream is = new InflaterInputStream(file.read(bufferSize)); CounterInputStream counter = new CounterInputStream(is); DataInputStream stream = new DataInputStream(counter)){
SaveIO.readHeader(stream);
int version = stream.readInt();
SaveVersion ver = SaveIO.getSaveWriter(version);
StringMap tags = new StringMap();
ver.region("meta", stream, counter, in -> tags.putAll(ver.readStringMap(in)));
return new Map(file, tags.getInt("width"), tags.getInt("height"), tags, custom, version, Version.build);
}
}
public static void writeMap(Fi file, Map map) throws IOException{
try{
SaveIO.write(file, map.tags);
}catch(Exception e){
throw new IOException(e);
}
}
public static void loadMap(Map map){
SaveIO.load(map.file);
}
public static void loadMap(Map map, WorldContext cons){
SaveIO.load(map.file, cons);
}
public static Pixmap generatePreview(Map map) throws IOException{
map.spawns = 0;
map.teams.clear();
try(InputStream is = new InflaterInputStream(map.file.read(bufferSize)); CounterInputStream counter = new CounterInputStream(is); DataInputStream stream = new DataInputStream(counter)){
SaveIO.readHeader(stream);
int version = stream.readInt();
SaveVersion ver = SaveIO.getSaveWriter(version);
ver.region("meta", stream, counter, ver::readStringMap);
Pixmap floors = new Pixmap(map.width, map.height, Format.RGBA8888);
Pixmap walls = new Pixmap(map.width, map.height, Format.RGBA8888);
int black = Color.rgba8888(Color.black);
int shade = Color.rgba8888(0f, 0f, 0f, 0.5f);
CachedTile tile = new CachedTile(){
@Override
public void setBlock(Block type){
super.setBlock(type);
int c = colorFor(Blocks.air, block(), Blocks.air, getTeam());
if(c != black){
walls.draw(x, floors.getHeight() - 1 - y, c);
floors.draw(x, floors.getHeight() - 1 - y + 1, shade);
}
}
@Override
public void setTeam(Team team){
super.setTeam(team);
if(block instanceof CoreBlock){
map.teams.add(team.ordinal());
}
}
};
ver.region("content", stream, counter, ver::readContentHeader);
ver.region("preview_map", stream, counter, in -> ver.readMap(in, new WorldContext(){
@Override public void resize(int width, int height){}
@Override public boolean isGenerating(){return false;}
@Override public void begin(){}
@Override public void end(){}
@Override
public Tile tile(int x, int y){
tile.x = (short)x;
tile.y = (short)y;
return tile;
}
@Override
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
if(overlayID != 0){
floors.draw(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict));
}else{
floors.draw(x, floors.getHeight() - 1 - y, colorFor(content.block(floorID), Blocks.air, Blocks.air, Team.derelict));
}
if(content.block(overlayID) == Blocks.spawn){
map.spawns ++;
}
return tile;
}
}));
floors.drawPixmap(walls, 0, 0);
walls.dispose();
return floors;
}finally{
content.setTemporaryMapper(null);
}
}
public static Pixmap generatePreview(Tile[][] tiles){
Pixmap pixmap = new Pixmap(tiles.length, tiles[0].length, Format.RGBA8888);
for(int x = 0; x < pixmap.getWidth(); x++){
for(int y = 0; y < pixmap.getHeight(); y++){
Tile tile = tiles[x][y];
pixmap.draw(x, pixmap.getHeight() - 1 - y, colorFor(tile.floor(), tile.block(), tile.overlay(), tile.getTeam()));
}
}
return pixmap;
}
public static int colorFor(Block floor, Block wall, Block ore, Team team){
if(wall.synthetic()){
return team.intColor;
}
return Color.rgba8888(wall.solid ? wall.color : ore == Blocks.air ? floor.color : ore.color);
}
interface TileProvider{
Tile get(int x, int y);
}
}

View File

@@ -0,0 +1,113 @@
package mindustry.io;
import arc.struct.ObjectMap;
import arc.struct.ObjectMap.Entry;
import arc.struct.StringMap;
import arc.util.io.CounterInputStream;
import arc.util.io.ReusableByteOutStream;
import mindustry.world.WorldContext;
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<String, String> fallback = ObjectMap.of();
protected void region(String name, DataInput stream, CounterInputStream counter, IORunner<DataInput> cons) throws IOException{
counter.resetCount();
int length;
try{
length = readChunk(stream, cons);
}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));
}
}
protected void region(String name, DataOutput stream, IORunner<DataOutput> cons) throws IOException{
try{
writeChunk(stream, cons);
}catch(Throwable e){
throw new IOException("Error writing region \"" + name + "\".", e);
}
}
public void writeChunk(DataOutput output, IORunner<DataOutput> runner) throws IOException{
writeChunk(output, false, runner);
}
/** 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<DataOutput> runner) throws IOException{
ReusableByteOutStream dout = isByte ? byteOutputSmall : byteOutput;
//reset output position
dout.reset();
//write the needed info
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);
}else{
if(length > Short.MAX_VALUE){
throw new IOException("Byte write length exceeded: " + length + " > " + Short.MAX_VALUE);
}
output.writeShort(length);
}
output.write(dout.getBytes(), 0, length);
}
public int readChunk(DataInput input, IORunner<DataInput> runner) throws IOException{
return readChunk(input, false, runner);
}
/** Reads a chunk of some length. Use the runner for reading to catch more descriptive errors. */
public int readChunk(DataInput input, boolean isByte, IORunner<DataInput> runner) throws IOException{
int length = isByte ? input.readUnsignedShort() : input.readInt();
runner.accept(input);
return length;
}
public void skipRegion(DataInput input) throws IOException{
skipRegion(input, false);
}
/** Skip a region completely. */
public void skipRegion(DataInput input, boolean isByte) throws IOException{
int length = readChunk(input, isByte, t -> {});
int skipped = input.skipBytes(length);
if(length != skipped){
throw new IOException("Could not skip bytes. Expected length: " + length + "; Actual length: " + skipped);
}
}
public void writeStringMap(DataOutput stream, ObjectMap<String, String> map) throws IOException{
stream.writeShort(map.size);
for(Entry<String, String> entry : map.entries()){
stream.writeUTF(entry.key);
stream.writeUTF(entry.value);
}
}
public StringMap readStringMap(DataInput stream) throws IOException{
StringMap map = new StringMap();
short size = stream.readShort();
for(int i = 0; i < size; i++){
map.put(stream.readUTF(), stream.readUTF());
}
return map;
}
public abstract void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException;
public abstract void write(DataOutputStream stream) throws IOException;
protected interface IORunner<T>{
void accept(T stream) throws IOException;
}
}

View File

@@ -0,0 +1,178 @@
package mindustry.io;
import arc.struct.*;
import arc.files.Fi;
import arc.util.io.CounterInputStream;
import arc.util.io.FastDeflaterOutputStream;
import mindustry.Vars;
import mindustry.io.versions.*;
import mindustry.world.WorldContext;
import java.io.*;
import java.util.Arrays;
import java.util.zip.InflaterInputStream;
import static mindustry.Vars.*;
public class SaveIO{
/** Format header. This is the string 'MSAV' in ASCII. */
public static final byte[] header = {77, 83, 65, 86};
public static final IntMap<SaveVersion> versions = new IntMap<>();
public static final Array<SaveVersion> versionArray = Array.with(new Save1(), new Save2(), new Save3());
static{
for(SaveVersion version : versionArray){
versions.put(version.version, version);
}
}
public static SaveVersion getSaveWriter(){
return versionArray.peek();
}
public static SaveVersion getSaveWriter(int version){
return versions.get(version);
}
public static void save(Fi file){
boolean exists = file.exists();
if(exists) file.moveTo(backupFileFor(file));
try{
write(file);
}catch(Exception e){
if(exists) backupFileFor(file).moveTo(file);
throw new RuntimeException(e);
}
}
public static DataInputStream getStream(Fi file){
return new DataInputStream(new InflaterInputStream(file.read(bufferSize)));
}
public static DataInputStream getBackupStream(Fi file){
return new DataInputStream(new InflaterInputStream(backupFileFor(file).read(bufferSize)));
}
public static boolean isSaveValid(Fi file){
try{
return isSaveValid(new DataInputStream(new InflaterInputStream(file.read(bufferSize))));
}catch(Exception e){
return false;
}
}
public static boolean isSaveValid(DataInputStream stream){
try{
getMeta(stream);
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}
}
public static SaveMeta getMeta(Fi file){
try{
return getMeta(getStream(file));
}catch(Exception e){
return getMeta(getBackupStream(file));
}
}
public static SaveMeta getMeta(DataInputStream stream){
try{
readHeader(stream);
int version = stream.readInt();
SaveMeta meta = versions.get(version).getMeta(stream);
stream.close();
return meta;
}catch(IOException e){
throw new RuntimeException(e);
}
}
public static Fi fileFor(int slot){
return saveDirectory.child(slot + "." + Vars.saveExtension);
}
public static Fi backupFileFor(Fi file){
return file.sibling(file.name() + "-backup." + file.extension());
}
public static void write(Fi file, StringMap tags){
write(new FastDeflaterOutputStream(file.write(false, bufferSize)), tags);
}
public static void write(Fi file){
write(file, null);
}
public static void write(OutputStream os, StringMap tags){
try(DataOutputStream stream = new DataOutputStream(os)){
stream.write(header);
stream.writeInt(getVersion().version);
if(tags == null){
getVersion().write(stream);
}else{
getVersion().write(stream, tags);
}
}catch(Exception e){
throw new RuntimeException(e);
}
}
public static void load(Fi file) throws SaveException{
load(file, world.context);
}
public static void load(Fi file, WorldContext context) throws SaveException{
try{
//try and load; if any exception at all occurs
load(new InflaterInputStream(file.read(bufferSize)), context);
}catch(SaveException e){
e.printStackTrace();
Fi backup = file.sibling(file.name() + "-backup." + file.extension());
if(backup.exists()){
load(new InflaterInputStream(backup.read(bufferSize)), context);
}else{
throw new SaveException(e.getCause());
}
}
}
/** Loads from a deflated (!) input stream.*/
public static void load(InputStream is, WorldContext context) throws SaveException{
try(CounterInputStream counter = new CounterInputStream(is); DataInputStream stream = new DataInputStream(counter)){
logic.reset();
readHeader(stream);
int version = stream.readInt();
SaveVersion ver = versions.get(version);
ver.read(stream, counter, context);
}catch(Exception e){
throw new SaveException(e);
}finally{
world.setGenerating(false);
content.setTemporaryMapper(null);
}
}
public static SaveVersion getVersion(){
return versionArray.peek();
}
public static void readHeader(DataInput input) throws IOException{
byte[] bytes = new byte[header.length];
input.readFully(bytes);
if(!Arrays.equals(bytes, header)){
throw new IOException("Incorrect header! Expecting: " + Arrays.toString(header) + "; Actual: " + Arrays.toString(bytes));
}
}
public static class SaveException extends RuntimeException{
public SaveException(Throwable throwable){
super(throwable);
}
}
}

View File

@@ -0,0 +1,31 @@
package mindustry.io;
import arc.struct.*;
import mindustry.game.*;
import mindustry.maps.*;
import static mindustry.Vars.maps;
public class SaveMeta{
public int version;
public int build;
public long timestamp;
public long timePlayed;
public Map map;
public int wave;
public Rules rules;
public StringMap tags;
public String[] mods;
public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, StringMap tags){
this.version = version;
this.build = build;
this.timestamp = timestamp;
this.timePlayed = timePlayed;
this.map = maps.all().find(m -> m.name().equals(map));
this.wave = wave;
this.rules = rules;
this.tags = tags;
this.mods = JsonIO.read(String[].class, tags.get("mods", "[]"));
}
}

View File

@@ -0,0 +1,23 @@
package mindustry.io;
import arc.assets.*;
import arc.assets.loaders.*;
import arc.assets.loaders.resolvers.*;
import arc.files.*;
public class SavePreviewLoader extends TextureLoader{
public SavePreviewLoader(){
super(new AbsoluteFileHandleResolver());
}
@Override
public void loadAsync(AssetManager manager, String fileName, Fi file, TextureParameter parameter){
try{
super.loadAsync(manager, fileName, file.sibling(file.nameWithoutExtension()), parameter);
}catch(Exception e){
e.printStackTrace();
file.sibling(file.nameWithoutExtension()).delete();
}
}
}

View File

@@ -0,0 +1,325 @@
package mindustry.io;
import arc.struct.*;
import arc.util.*;
import arc.util.io.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.ctype.ContentType;
import mindustry.entities.*;
import mindustry.entities.traits.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.maps.*;
import mindustry.type.*;
import mindustry.world.*;
import java.io.*;
import static mindustry.Vars.*;
public abstract class SaveVersion extends SaveFileReader{
public int version;
//HACK stores the last read build of the save file, valid after read meta call
protected int lastReadBuild;
public SaveVersion(int version){
this.version = version;
}
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"), JsonIO.read(Rules.class, map.get("rules", "{}")), map);
}
@Override
public final void write(DataOutputStream stream) throws IOException{
write(stream, new StringMap());
}
@Override
public final void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException{
region("meta", stream, counter, this::readMeta);
region("content", stream, counter, this::readContentHeader);
try{
region("map", stream, counter, in -> readMap(in, context));
region("entities", stream, counter, this::readEntities);
}finally{
content.setTemporaryMapper(null);
}
}
public final void write(DataOutputStream stream, StringMap extraTags) throws IOException{
region("meta", stream, out -> writeMeta(out, extraTags));
region("content", stream, this::writeContentHeader);
region("map", stream, this::writeMap);
region("entities", stream, this::writeEntities);
}
public void writeMeta(DataOutput stream, StringMap tags) throws IOException{
writeStringMap(stream, StringMap.of(
"saved", Time.millis(),
"playtime", headless ? 0 : control.saves.getTotalPlaytime(),
"build", Version.build,
"mapname", world.getMap() == null ? "unknown" : world.getMap().name(),
"wave", state.wave,
"wavetime", state.wavetime,
"stats", JsonIO.write(state.stats),
"rules", JsonIO.write(state.rules),
"mods", JsonIO.write(mods.getModStrings().toArray(String.class)),
"width", world.width(),
"height", world.height()
).merge(tags));
}
public void readMeta(DataInput stream) throws IOException{
StringMap map = readStringMap(stream);
state.wave = map.getInt("wave");
state.wavetime = map.getFloat("wavetime", state.rules.waveSpacing);
state.stats = JsonIO.read(Stats.class, map.get("stats", "{}"));
state.rules = JsonIO.read(Rules.class, map.get("rules", "{}"));
if(state.rules.spawns.isEmpty()) state.rules.spawns = defaultWaves.get();
lastReadBuild = map.getInt("build", -1);
Map worldmap = maps.byName(map.get("mapname", "\\\\\\"));
world.setMap(worldmap == null ? new Map(StringMap.of(
"name", map.get("mapname", "Unknown"),
"width", 1,
"height", 1
)) : worldmap);
}
public void writeMap(DataOutput stream) throws IOException{
//write world size
stream.writeShort(world.width());
stream.writeShort(world.height());
//floor + overlay
for(int i = 0; i < world.width() * world.height(); i++){
Tile tile = world.rawTile(i % world.width(), i / world.width());
stream.writeShort(tile.floorID());
stream.writeShort(tile.overlayID());
int consecutives = 0;
for(int j = i + 1; j < world.width() * world.height() && consecutives < 255; j++){
Tile nextTile = world.rawTile(j % world.width(), j / world.width());
if(nextTile.floorID() != tile.floorID() || nextTile.overlayID() != tile.overlayID()){
break;
}
consecutives++;
}
stream.writeByte(consecutives);
i += consecutives;
}
//blocks
for(int i = 0; i < world.width() * world.height(); i++){
Tile tile = world.rawTile(i % world.width(), i / world.width());
stream.writeShort(tile.blockID());
if(tile.entity != null){
writeChunk(stream, true, out -> {
out.writeByte(tile.entity.version());
tile.entity.write(out);
});
}else{
//write consecutive non-entity blocks
int consecutives = 0;
for(int j = i + 1; j < world.width() * world.height() && consecutives < 255; j++){
Tile nextTile = world.rawTile(j % world.width(), j / world.width());
if(nextTile.blockID() != tile.blockID()){
break;
}
consecutives++;
}
stream.writeByte(consecutives);
i += consecutives;
}
}
}
public void readMap(DataInput stream, WorldContext context) throws IOException{
int width = stream.readUnsignedShort();
int height = stream.readUnsignedShort();
boolean generating = context.isGenerating();
if(!generating) context.begin();
try{
context.resize(width, height);
//read floor and create tiles first
for(int i = 0; i < width * height; i++){
int x = i % width, y = i / width;
short floorid = stream.readShort();
short oreid = stream.readShort();
int consecutives = stream.readUnsignedByte();
if(content.block(floorid) == Blocks.air) floorid = Blocks.stone.id;
context.create(x, y, floorid, oreid, (short)0);
for(int j = i + 1; j < i + 1 + consecutives; j++){
int newx = j % width, newy = j / width;
context.create(newx, newy, floorid, oreid, (short)0);
}
i += consecutives;
}
//read blocks
for(int i = 0; i < width * height; i++){
int x = i % width, y = i / width;
Block block = content.block(stream.readShort());
Tile tile = context.tile(x, y);
if(block == null) block = Blocks.air;
tile.setBlock(block);
if(tile.entity != null){
try{
readChunk(stream, true, in -> {
byte version = in.readByte();
tile.entity.read(in, version);
});
}catch(Exception e){
throw new IOException("Failed to read tile entity of block: " + block, e);
}
}else{
int consecutives = stream.readUnsignedByte();
for(int j = i + 1; j < i + 1 + consecutives; j++){
int newx = j % width, newy = j / width;
context.tile(newx, newy).setBlock(block);
}
i += consecutives;
}
}
}finally{
if(!generating) context.end();
}
}
public void writeEntities(DataOutput stream) throws IOException{
//write team data with entities.
Array<TeamData> data = state.teams.getActive();
stream.writeInt(data.size);
for(TeamData team : data){
stream.writeInt(team.team.ordinal());
stream.writeInt(team.brokenBlocks.size);
for(BrokenBlock block : team.brokenBlocks){
stream.writeShort(block.x);
stream.writeShort(block.y);
stream.writeShort(block.rotation);
stream.writeShort(block.block);
stream.writeInt(block.config);
}
}
//write entity chunk
int groups = 0;
for(EntityGroup<?> group : entities.all()){
if(!group.isEmpty() && group.all().get(0) instanceof SaveTrait){
groups++;
}
}
stream.writeByte(groups);
for(EntityGroup<?> group : entities.all()){
if(!group.isEmpty() && group.all().get(0) instanceof SaveTrait){
stream.writeInt(group.size());
for(Entity entity : group.all()){
SaveTrait save = (SaveTrait)entity;
//each entity is a separate chunk.
writeChunk(stream, true, out -> {
out.writeByte(save.getTypeID().id);
out.writeByte(save.version());
save.writeSave(out);
});
}
}
}
}
public void readEntities(DataInput stream) throws IOException{
int teamc = stream.readInt();
for(int i = 0; i < teamc; i++){
Team team = Team.all[stream.readInt()];
TeamData data = state.teams.get(team);
int blocks = stream.readInt();
for(int j = 0; j < blocks; j++){
data.brokenBlocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, stream.readInt()));
}
}
byte groups = stream.readByte();
for(int i = 0; i < groups; i++){
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
//TODO throw exception on read fail
readChunk(stream, true, in -> {
byte typeid = in.readByte();
byte version = in.readByte();
SaveTrait trait = (SaveTrait)content.<TypeID>getByID(ContentType.typeid, typeid).constructor.get();
trait.readSave(in, version);
});
}
}
}
public void readContentHeader(DataInput stream) throws IOException{
byte mapped = stream.readByte();
MappableContent[][] map = new MappableContent[ContentType.values().length][0];
for(int i = 0; i < mapped; i++){
ContentType type = ContentType.values()[stream.readByte()];
short total = stream.readShort();
map[type.ordinal()] = new MappableContent[total];
for(int j = 0; j < total; j++){
String name = stream.readUTF();
map[type.ordinal()][j] = content.getByName(type, fallback.get(name, name));
}
}
content.setTemporaryMapper(map);
}
public void writeContentHeader(DataOutput stream) throws IOException{
Array<Content>[] map = content.getContentMap();
int mappable = 0;
for(Array<Content> arr : map){
if(arr.size > 0 && arr.first() instanceof MappableContent){
mappable++;
}
}
stream.writeByte(mappable);
for(Array<Content> arr : map){
if(arr.size > 0 && arr.first() instanceof MappableContent){
stream.writeByte(arr.first().getContentType().ordinal());
stream.writeShort(arr.size);
for(Content c : arr){
stream.writeUTF(((MappableContent)c).name);
}
}
}
}
}

View File

@@ -0,0 +1,367 @@
package mindustry.io;
import mindustry.annotations.Annotations.ReadClass;
import mindustry.annotations.Annotations.WriteClass;
import arc.graphics.Color;
import mindustry.ctype.ContentType;
import mindustry.entities.Effects;
import mindustry.entities.Effects.Effect;
import mindustry.entities.type.Bullet;
import mindustry.entities.bullet.BulletType;
import mindustry.entities.traits.BuilderTrait.BuildRequest;
import mindustry.entities.traits.ShooterTrait;
import mindustry.entities.type.*;
import mindustry.entities.units.*;
import mindustry.game.*;
import mindustry.net.Administration.TraceInfo;
import mindustry.net.Packets.AdminAction;
import mindustry.net.Packets.KickReason;
import mindustry.type.*;
import mindustry.world.*;
import java.io.*;
import java.nio.ByteBuffer;
import static mindustry.Vars.*;
/** Class for specifying read/write methods for code generation. */
@SuppressWarnings("unused")
public class TypeIO{
@WriteClass(Player.class)
public static void writePlayer(ByteBuffer buffer, Player player){
if(player == null){
buffer.putInt(-1);
}else{
buffer.putInt(player.id);
}
}
@ReadClass(Player.class)
public static Player readPlayer(ByteBuffer buffer){
int id = buffer.getInt();
return id == -1 ? null : playerGroup.getByID(id);
}
@WriteClass(Unit.class)
public static void writeUnit(ByteBuffer buffer, Unit unit){
if(unit.getGroup() == null){
buffer.put((byte)-1);
return;
}
buffer.put((byte)unit.getGroup().getID());
buffer.putInt(unit.getID());
}
@ReadClass(Unit.class)
public static Unit readUnit(ByteBuffer buffer){
byte gid = buffer.get();
if(gid == -1) return null;
int id = buffer.getInt();
return (Unit)entities.get(gid).getByID(id);
}
@WriteClass(ShooterTrait.class)
public static void writeShooter(ByteBuffer buffer, ShooterTrait trait){
buffer.put((byte)trait.getGroup().getID());
buffer.putInt(trait.getID());
}
@ReadClass(ShooterTrait.class)
public static ShooterTrait readShooter(ByteBuffer buffer){
byte gid = buffer.get();
int id = buffer.getInt();
return (ShooterTrait)entities.get(gid).getByID(id);
}
@WriteClass(Bullet.class)
public static void writeBullet(ByteBuffer buffer, Bullet bullet){
buffer.putInt(bullet.getID());
}
@ReadClass(Bullet.class)
public static Bullet readBullet(ByteBuffer buffer){
int id = buffer.getInt();
return bulletGroup.getByID(id);
}
@WriteClass(BaseUnit.class)
public static void writeBaseUnit(ByteBuffer buffer, BaseUnit unit){
buffer.put((byte)unit.getTeam().ordinal());
buffer.putInt(unit.getID());
}
@ReadClass(BaseUnit.class)
public static BaseUnit readBaseUnit(ByteBuffer buffer){
byte tid = buffer.get();
int id = buffer.getInt();
return unitGroups[tid].getByID(id);
}
@WriteClass(Tile.class)
public static void writeTile(ByteBuffer buffer, Tile tile){
buffer.putInt(tile == null ? Pos.get(-1, -1) : tile.pos());
}
@ReadClass(Tile.class)
public static Tile readTile(ByteBuffer buffer){
return world.tile(buffer.getInt());
}
@WriteClass(Block.class)
public static void writeBlock(ByteBuffer buffer, Block block){
buffer.putShort(block.id);
}
@ReadClass(Block.class)
public static Block readBlock(ByteBuffer buffer){
return content.block(buffer.getShort());
}
@WriteClass(BuildRequest[].class)
public static void writeRequests(ByteBuffer buffer, BuildRequest[] requests){
buffer.putShort((short)requests.length);
for(BuildRequest request : requests){
buffer.put(request.breaking ? (byte)1 : 0);
buffer.putInt(Pos.get(request.x, request.y));
if(!request.breaking){
buffer.putShort(request.block.id);
buffer.put((byte)request.rotation);
buffer.put(request.hasConfig ? (byte)1 : 0);
buffer.putInt(request.config);
}
}
}
@ReadClass(BuildRequest[].class)
public static BuildRequest[] readRequests(ByteBuffer buffer){
short reqamount = buffer.getShort();
BuildRequest[] reqs = new BuildRequest[reqamount];
for(int i = 0; i < reqamount; i++){
byte type = buffer.get();
int position = buffer.getInt();
BuildRequest currentRequest;
if(world.tile(position) == null){
continue;
}
if(type == 1){ //remove
currentRequest = new BuildRequest(Pos.x(position), Pos.y(position));
}else{ //place
short block = buffer.getShort();
byte rotation = buffer.get();
boolean hasConfig = buffer.get() == 1;
int config = buffer.getInt();
currentRequest = new BuildRequest(Pos.x(position), Pos.y(position), rotation, content.block(block));
if(hasConfig){
currentRequest.configure(config);
}
}
reqs[i] = (currentRequest);
}
return reqs;
}
@WriteClass(KickReason.class)
public static void writeKick(ByteBuffer buffer, KickReason reason){
buffer.put((byte)reason.ordinal());
}
@ReadClass(KickReason.class)
public static KickReason readKick(ByteBuffer buffer){
return KickReason.values()[buffer.get()];
}
@WriteClass(Rules.class)
public static void writeRules(ByteBuffer buffer, Rules rules){
String string = JsonIO.write(rules);
byte[] bytes = string.getBytes(charset);
buffer.putInt(bytes.length);
buffer.put(bytes);
}
@ReadClass(Rules.class)
public static Rules readRules(ByteBuffer buffer){
int length = buffer.getInt();
byte[] bytes = new byte[length];
buffer.get(bytes);
String string = new String(bytes, charset);
return JsonIO.read(Rules.class, string);
}
@WriteClass(Team.class)
public static void writeTeam(ByteBuffer buffer, Team reason){
buffer.put((byte)reason.ordinal());
}
@ReadClass(Team.class)
public static Team readTeam(ByteBuffer buffer){
return Team.all[buffer.get()];
}
@WriteClass(UnitCommand.class)
public static void writeUnitCommand(ByteBuffer buffer, UnitCommand reason){
buffer.put((byte)reason.ordinal());
}
@ReadClass(UnitCommand.class)
public static UnitCommand readUnitCommand(ByteBuffer buffer){
return UnitCommand.all[buffer.get()];
}
@WriteClass(AdminAction.class)
public static void writeAction(ByteBuffer buffer, AdminAction reason){
buffer.put((byte)reason.ordinal());
}
@ReadClass(AdminAction.class)
public static AdminAction readAction(ByteBuffer buffer){
return AdminAction.values()[buffer.get()];
}
@WriteClass(Effect.class)
public static void writeEffect(ByteBuffer buffer, Effect effect){
buffer.putShort((short)effect.id);
}
@ReadClass(Effect.class)
public static Effect readEffect(ByteBuffer buffer){
return Effects.getEffect(buffer.getShort());
}
@WriteClass(UnitType.class)
public static void writeUnitType(ByteBuffer buffer, UnitType effect){
buffer.putShort(effect.id);
}
@ReadClass(UnitType.class)
public static UnitType readUnitType(ByteBuffer buffer){
return content.getByID(ContentType.unit, buffer.getShort());
}
@WriteClass(Color.class)
public static void writeColor(ByteBuffer buffer, Color color){
buffer.putInt(Color.rgba8888(color));
}
@ReadClass(Color.class)
public static Color readColor(ByteBuffer buffer){
return new Color(buffer.getInt());
}
@WriteClass(Mech.class)
public static void writeMech(ByteBuffer buffer, Mech mech){
buffer.put((byte)mech.id);
}
@ReadClass(Mech.class)
public static Mech readMech(ByteBuffer buffer){
return content.getByID(ContentType.mech, buffer.get());
}
@WriteClass(Liquid.class)
public static void writeLiquid(ByteBuffer buffer, Liquid liquid){
buffer.putShort(liquid == null ? -1 : liquid.id);
}
@ReadClass(Liquid.class)
public static Liquid readLiquid(ByteBuffer buffer){
short id = buffer.getShort();
return id == -1 ? null : content.liquid(id);
}
@WriteClass(BulletType.class)
public static void writeBulletType(ByteBuffer buffer, BulletType type){
buffer.putShort(type.id);
}
@ReadClass(BulletType.class)
public static BulletType readBulletType(ByteBuffer buffer){
return content.getByID(ContentType.bullet, buffer.getShort());
}
@WriteClass(Item.class)
public static void writeItem(ByteBuffer buffer, Item item){
buffer.putShort(item == null ? -1 : item.id);
}
@ReadClass(Item.class)
public static Item readItem(ByteBuffer buffer){
short id = buffer.getShort();
return id == -1 ? null : content.item(id);
}
@WriteClass(String.class)
public static void writeString(ByteBuffer buffer, String string){
if(string != null){
byte[] bytes = string.getBytes(charset);
buffer.putShort((short)bytes.length);
buffer.put(bytes);
}else{
buffer.putShort((short)-1);
}
}
@ReadClass(String.class)
public static String readString(ByteBuffer buffer){
short slength = buffer.getShort();
if(slength != -1){
byte[] bytes = new byte[slength];
buffer.get(bytes);
return new String(bytes, charset);
}else{
return null;
}
}
@WriteClass(byte[].class)
public static void writeBytes(ByteBuffer buffer, byte[] bytes){
buffer.putShort((short)bytes.length);
buffer.put(bytes);
}
@ReadClass(byte[].class)
public static byte[] readBytes(ByteBuffer buffer){
short length = buffer.getShort();
byte[] bytes = new byte[length];
buffer.get(bytes);
return bytes;
}
@WriteClass(TraceInfo.class)
public static void writeTraceInfo(ByteBuffer buffer, TraceInfo trace){
writeString(buffer, trace.ip);
writeString(buffer, trace.uuid);
buffer.put(trace.modded ? (byte)1 : 0);
buffer.put(trace.mobile ? (byte)1 : 0);
}
@ReadClass(TraceInfo.class)
public static TraceInfo readTraceInfo(ByteBuffer buffer){
return new TraceInfo(readString(buffer), readString(buffer), buffer.get() == 1, buffer.get() == 1);
}
public static void writeStringData(DataOutput buffer, String string) throws IOException{
if(string != null){
byte[] bytes = string.getBytes(charset);
buffer.writeShort((short)bytes.length);
buffer.write(bytes);
}else{
buffer.writeShort((short)-1);
}
}
public static String readStringData(DataInput buffer) throws IOException{
short slength = buffer.readShort();
if(slength != -1){
byte[] bytes = new byte[slength];
buffer.readFully(bytes);
return new String(bytes, charset);
}else{
return null;
}
}
}

View File

@@ -0,0 +1,145 @@
package mindustry.io.versions;
import arc.func.Prov;
import mindustry.entities.type.Bullet;
import mindustry.entities.effect.*;
import mindustry.entities.type.Player;
import mindustry.entities.type.base.*;
/*
Latest data: [build 81]
0 = Player
1 = Fire
2 = Puddle
3 = MinerDrone
4 = RepairDrone
5 = BuilderDrone
6 = GroundUnit
7 = GroundUnit
8 = GroundUnit
9 = GroundUnit
10 = GroundUnit
11 = FlyingUnit
12 = FlyingUnit
13 = Revenant
Before removal of lightining/bullet: [build 80]
0 = Player
1 = Fire
2 = Puddle
3 = Bullet
4 = Lightning
5 = MinerDrone
6 = RepairDrone
7 = BuilderDrone
8 = GroundUnit
9 = GroundUnit
10 = GroundUnit
11 = GroundUnit
12 = GroundUnit
13 = FlyingUnit
14 = FlyingUnit
15 = Revenant
Before addition of new units: [build 79 and below]
0 = Player
1 = Fire
2 = Puddle
3 = Bullet
4 = Lightning
5 = RepairDrone
6 = GroundUnit
7 = GroundUnit
8 = GroundUnit
9 = GroundUnit
10 = GroundUnit
11 = FlyingUnit
12 = FlyingUnit
13 = BuilderDrone
14 = Revenant
*/
public class LegacyTypeTable{
/*
0 = Player
1 = Fire
2 = Puddle
3 = Draug
4 = Spirit
5 = Phantom
6 = Dagger
7 = Crawler
8 = Titan
9 = Fortress
10 = Eruptor
11 = Wraith
12 = Ghoul
13 = Revenant
*/
private static final Prov[] build81Table = {
Player::new,
Fire::new,
Puddle::new,
MinerDrone::new,
RepairDrone::new,
BuilderDrone::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
FlyingUnit::new,
FlyingUnit::new,
HoverUnit::new
};
private static final Prov[] build80Table = {
Player::new,
Fire::new,
Puddle::new,
Bullet::new,
Lightning::new,
MinerDrone::new,
RepairDrone::new,
BuilderDrone::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
FlyingUnit::new,
FlyingUnit::new,
HoverUnit::new
};
private static final Prov[] build79Table = {
Player::new,
Fire::new,
Puddle::new,
Bullet::new,
Lightning::new,
RepairDrone::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
GroundUnit::new,
FlyingUnit::new,
FlyingUnit::new,
BuilderDrone::new,
HoverUnit::new
};
public static Prov[] getTable(int build){
if(build == -1 || build == 81){
//return most recent one since that's probably it; not guaranteed
return build81Table;
}else if(build == 80){
return build80Table;
}else{
return build79Table;
}
}
}

View File

@@ -0,0 +1,32 @@
package mindustry.io.versions;
import arc.func.*;
import mindustry.entities.traits.*;
import java.io.*;
public class Save1 extends Save2{
public Save1(){
version = 1;
}
@Override
public void readEntities(DataInput stream) throws IOException{
Prov[] table = LegacyTypeTable.getTable(lastReadBuild);
byte groups = stream.readByte();
for(int i = 0; i < groups; i++){
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
readChunk(stream, true, in -> {
byte typeid = in.readByte();
byte version = in.readByte();
SaveTrait trait = (SaveTrait)table[typeid].get();
trait.readSave(in, version);
});
}
}
}
}

View File

@@ -0,0 +1,35 @@
package mindustry.io.versions;
import mindustry.ctype.ContentType;
import mindustry.entities.traits.*;
import mindustry.io.*;
import mindustry.type.TypeID;
import java.io.*;
import static mindustry.Vars.content;
public class Save2 extends SaveVersion{
public Save2(){
super(2);
}
@Override
public void readEntities(DataInput stream) throws IOException{
byte groups = stream.readByte();
for(int i = 0; i < groups; i++){
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
//TODO throw exception on read fail
readChunk(stream, true, in -> {
byte typeid = in.readByte();
byte version = in.readByte();
SaveTrait trait = (SaveTrait)content.<TypeID>getByID(ContentType.typeid, typeid).constructor.get();
trait.readSave(in, version);
});
}
}
}
}

View File

@@ -0,0 +1,9 @@
package mindustry.io.versions;
import mindustry.io.*;
public class Save3 extends SaveVersion{
public Save3(){
super(3);
}
}