Save version chunks changed to 4-byte lengths

This commit is contained in:
Anuken
2025-09-12 14:42:22 -04:00
parent 5f58adb764
commit 729f18726f
19 changed files with 311 additions and 150 deletions

View File

@@ -41,7 +41,7 @@ public class MapIO{
SaveVersion ver = SaveIO.getSaveWriter(version);
if(ver == null) throw new IOException("Unknown save version: " + version + ". Are you trying to load a save from a newer version?");
StringMap tags = new StringMap();
ver.region("meta", stream, counter, in -> tags.putAll(ver.readStringMap(in)));
ver.readRegion("meta", stream, counter, in -> tags.putAll(ver.readStringMap(in)));
return new Map(file, tags.getInt("width"), tags.getInt("height"), tags, custom, version, Version.build);
}
}
@@ -71,7 +71,7 @@ public class MapIO{
int version = stream.readInt();
SaveVersion ver = SaveIO.getSaveWriter(version);
if(ver == null) throw new IOException("Unknown save version: " + version + ". Are you trying to load a save from a newer version?");
ver.region("meta", stream, counter, ver::readStringMap);
ver.readRegion("meta", stream, counter, ver::readStringMap);
Pixmap floors = new Pixmap(map.width, map.height);
Pixmap walls = new Pixmap(map.width, map.height);
@@ -96,8 +96,8 @@ public class MapIO{
}
};
ver.region("content", stream, counter, ver::readContentHeader);
ver.region("preview_map", stream, counter, in -> ver.readMap(in, new WorldContext(){
ver.readRegion("content", stream, counter, ver::readContentHeader);
ver.readRegion("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(){

View File

@@ -2,7 +2,6 @@ package mindustry.io;
import arc.struct.*;
import arc.struct.ObjectMap.*;
import arc.util.*;
import arc.util.io.*;
import mindustry.world.*;
@@ -75,25 +74,22 @@ public abstract class SaveFileReader{
"slag", "molten-slag"
);
protected final ReusableByteOutStream byteOutput = new ReusableByteOutStream(), byteOutput2 = new ReusableByteOutStream();
protected final DataOutputStream dataBytes = new DataOutputStream(byteOutput), dataBytes2 = new DataOutputStream(byteOutput2);
protected final ReusableByteOutStream byteOutputSmall = new ReusableByteOutStream();
protected final DataOutputStream dataBytesSmall = new DataOutputStream(byteOutputSmall);
protected boolean chunkNested = false;
protected int lastRegionLength;
protected @Nullable CounterInputStream currCounter;
protected static final ReusableByteOutStream byteOutput = new ReusableByteOutStream(), byteOutput2 = new ReusableByteOutStream();
protected static final DataOutputStream dataBytes = new DataOutputStream(byteOutput), dataBytes2 = new DataOutputStream(byteOutput2);
protected static final Writes writes1 = new Writes(dataBytes), writes2 = new Writes(dataBytes2);
protected static final Reads chunkReads = new Reads(null);
protected static boolean chunkNested = false;
public static String mapFallback(String name){
return fallback.get(name, name);
}
public void region(String name, DataInput stream, CounterInputStream counter, IORunner<DataInput> cons) throws IOException{
//TODO: unify readRegion with readChunk, they do the same thing and both should have good error messages
public void readRegion(String name, DataInput stream, CounterInputStream counter, IORunner<DataInput> cons) throws IOException{
counter.resetCount();
this.currCounter = counter;
int length;
try{
length = readChunk(stream, cons);
length = readChunk(stream, (chunkStream, len) -> cons.accept(chunkStream));
}catch(Throwable e){
throw new IOException("Error reading region \"" + name + "\".", e);
}
@@ -103,75 +99,74 @@ public abstract class SaveFileReader{
}
}
public void region(String name, DataOutput stream, IORunner<DataOutput> cons) throws IOException{
public void writeRegion(String name, DataOutput stream, IORunner<DataOutput> cons) throws IOException{
try{
writeChunk(stream, cons);
writeChunk(stream, writes -> cons.accept(writes.output));
}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 isShort, IORunner<DataOutput> runner) throws IOException{
//TODO awful
public void writeChunk(DataOutput output, IORunner<Writes> runner) throws IOException{
boolean wasNested = chunkNested;
if(!isShort){
chunkNested = true;
}
ReusableByteOutStream dout =
isShort ? byteOutputSmall :
wasNested ? byteOutput2 :
byteOutput;
chunkNested = true;
//regions can be nested once, so use a different output if it's already nested
ReusableByteOutStream dout = wasNested ? byteOutput2 : byteOutput;
try{
//reset output position
dout.reset();
//write the needed info
runner.accept(
isShort ? dataBytesSmall :
wasNested ? dataBytes2 :
dataBytes
);
runner.accept(wasNested ? writes2 : writes1);
//write the length of the stream contents as an int, then write the contents
int length = dout.size();
//write length (either int or byte) followed by the output bytes
if(!isShort){
output.writeInt(length);
}else{
if(length > 65535){
throw new IOException("Byte write length exceeded: " + length + " > 65535");
}
output.writeShort(length);
}
output.writeInt(length);
output.write(dout.getBytes(), 0, length);
}finally{
chunkNested = wasNested;
}
}
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 isShort, IORunner<DataInput> runner) throws IOException{
int length = isShort ? input.readUnsignedShort() : input.readInt();
lastRegionLength = length;
runner.accept(input);
public int readChunk(DataInput input, IORunnerLength<DataInput> runner) throws IOException{
//TODO: it would be really nice to support counting here to detect serialization errors
int length = input.readInt();
runner.accept(input, length);
return length;
}
public void skipChunk(DataInput input) throws IOException{
skipChunk(input, false);
/** Reads a chunk of some length. Use the runner for reading to catch more descriptive errors. */
public int readChunkReads(DataInput input, IORunnerLength<Reads> runner) throws IOException{
return readChunk(input, (in, length) -> {
chunkReads.input = in;
runner.accept(chunkReads, length);
});
}
/** Skip a chunk completely, discarding the bytes. */
public void skipChunk(DataInput input, boolean isShort) throws IOException{
int length = readChunk(input, isShort, t -> {});
public void skipChunk(DataInput input) throws IOException{
int length = readChunk(input, (t, len) -> {});
int skipped = input.skipBytes(length);
if(length != skipped){
throw new IOException("Could not skip bytes. Expected length: " + length + "; Actual length: " + skipped);
}
}
/** Reads a legacy chunk where the length is only 2 bytes. */
public int readLegacyShortChunk(DataInput input, IORunnerLength<Reads> runner) throws IOException{
int length = input.readUnsignedShort();
chunkReads.input = input;
runner.accept(chunkReads, length);
return length;
}
/** Skip a legacy chunk completely, discarding the bytes. */
public void skipLegacyShortChunk(DataInput input) throws IOException{
int length = readLegacyShortChunk(input, (t, len) -> {});
int skipped = input.skipBytes(length);
if(length != skipped){
throw new IOException("Could not skip bytes. Expected length: " + length + "; Actual length: " + skipped);
@@ -203,10 +198,18 @@ public abstract class SaveFileReader{
void accept(T stream) throws IOException;
}
public interface IORunnerLength<T>{
void accept(T stream, int length) throws IOException;
}
public interface CustomChunk{
void write(DataOutput stream) throws IOException;
void read(DataInput stream) throws IOException;
default void read(DataInput stream, int length) throws IOException{
read(stream);
}
/** @return whether this chunk is enabled at all */
default boolean shouldWrite(){
return true;

View File

@@ -20,7 +20,7 @@ public class SaveIO{
/** Save format header. */
public static final byte[] header = {'M', 'S', 'A', 'V'};
public static final IntMap<SaveVersion> versions = new IntMap<>();
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9());
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9(), new Save10());
static{
for(SaveVersion version : versionArray){

View File

@@ -29,12 +29,6 @@ public abstract class SaveVersion extends SaveFileReader{
public final int version;
//HACK stores the last read build of the save file, valid after read meta call
protected int lastReadBuild;
//stores entity mappings for use after readEntityMapping
//if null, fall back to EntityMapping's values
protected @Nullable Prov[] entityMapping;
/**
* Registers a custom save chunk reader/writer by name. This is mostly used for mods that need to save extra data.
* @param name a mod-specific, unique name for identifying this chunk. Prefixing is recommended.
@@ -69,26 +63,26 @@ public abstract class SaveVersion extends SaveFileReader{
@Override
public void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException{
region("meta", stream, counter, in -> readMeta(in, context));
region("content", stream, counter, this::readContentHeader);
readRegion("meta", stream, counter, in -> readMeta(in, context));
readRegion("content", stream, counter, this::readContentHeader);
try{
region("map", stream, counter, in -> readMap(in, context));
region("entities", stream, counter, this::readEntities);
if(version >= 8) region("markers", stream, counter, this::readMarkers);
region("custom", stream, counter, this::readCustomChunks);
readRegion("map", stream, counter, in -> readMap(in, context));
readRegion("entities", stream, counter, this::readEntities);
if(version >= 8) readRegion("markers", stream, counter, this::readMarkers);
readRegion("custom", stream, counter, this::readCustomChunks);
}finally{
content.setTemporaryMapper(null);
}
}
public 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);
region("markers", stream, this::writeMarkers);
region("custom", stream, s -> writeCustomChunks(s, false));
writeRegion("meta", stream, out -> writeMeta(out, extraTags));
writeRegion("content", stream, this::writeContentHeader);
writeRegion("map", stream, this::writeMap);
writeRegion("entities", stream, this::writeEntities);
writeRegion("markers", stream, this::writeMarkers);
writeRegion("custom", stream, s -> writeCustomChunks(s, false));
}
public void writeCustomChunks(DataOutput stream, boolean net) throws IOException{
@@ -98,7 +92,7 @@ public abstract class SaveVersion extends SaveFileReader{
var chunk = customChunks.get(chunkName);
stream.writeUTF(chunkName);
writeChunk(stream, false, chunk::write);
writeChunk(stream, writes -> chunk.write(writes.output));
}
}
@@ -108,7 +102,7 @@ public abstract class SaveVersion extends SaveFileReader{
String name = stream.readUTF();
var chunk = customChunks.get(name);
if(chunk != null){
readChunk(stream, false, chunk::read);
readChunk(stream, chunk::read);
}else{
skipChunk(stream);
}
@@ -163,7 +157,6 @@ public abstract class SaveVersion extends SaveFileReader{
state.rules = JsonIO.read(Rules.class, map.get("rules", "{}"));
state.mapLocales = JsonIO.read(MapLocales.class, map.get("locales", "{}"));
if(state.rules.spawns.isEmpty()) state.rules.spawns = waves.get();
lastReadBuild = map.getInt("build", -1);
if(context.getSector() != null){
state.rules.sector = context.getSector();
@@ -254,9 +247,9 @@ public abstract class SaveVersion extends SaveFileReader{
if(tile.build != null){
if(tile.isCenter()){
stream.writeBoolean(true);
writeChunk(stream, true, out -> {
out.writeByte(tile.build.version());
tile.build.writeAll(Writes.get(out));
writeChunk(stream, out -> {
out.b(tile.build.version());
tile.build.writeAll(out);
});
}else{
stream.writeBoolean(false);
@@ -354,16 +347,16 @@ public abstract class SaveVersion extends SaveFileReader{
if(isCenter){ //only read entity for center blocks
if(block.hasBuilding()){
try{
readChunk(stream, true, in -> {
byte revision = in.readByte();
tile.build.readAll(Reads.get(in), revision);
readChunkReads(stream, (in, len) -> {
byte revision = in.b();
tile.build.readAll(in, revision);
});
}catch(Throwable e){
throw new IOException("Failed to read tile entity of block: " + block, e);
}
}else{
//skip the entity region, as the entity and its IO code are now gone
skipChunk(stream, true);
skipChunk(stream);
}
context.onReadBuilding();
@@ -393,6 +386,9 @@ public abstract class SaveVersion extends SaveFileReader{
//write team data with entities.
Seq<TeamData> data = state.teams.getActive().copy();
if(!data.contains(Team.sharded.data())) data.add(Team.sharded.data());
Writes writes = new Writes(stream);
stream.writeInt(data.size);
for(TeamData team : data){
stream.writeInt(team.team.id);
@@ -402,7 +398,7 @@ public abstract class SaveVersion extends SaveFileReader{
stream.writeShort(block.y);
stream.writeShort(block.rotation);
stream.writeShort(block.block.id);
TypeIO.writeObject(Writes.get(stream), block.config);
TypeIO.writeObject(writes, block.config);
}
}
}
@@ -412,11 +408,11 @@ public abstract class SaveVersion extends SaveFileReader{
for(Entityc entity : Groups.all){
if(!entity.serialize()) continue;
writeChunk(stream, true, out -> {
out.writeByte(entity.classId());
out.writeInt(entity.id());
writeChunk(stream, out -> {
out.b(entity.classId());
out.i(entity.id());
entity.beforeWrite();
entity.write(Writes.get(out));
entity.write(out);
});
}
}
@@ -446,13 +442,14 @@ public abstract class SaveVersion extends SaveFileReader{
public void readTeamBlocks(DataInput stream) throws IOException{
int teamc = stream.readInt();
var reads = new Reads(stream);
for(int i = 0; i < teamc; i++){
Team team = Team.get(stream.readInt());
TeamData data = team.data();
int blocks = stream.readInt();
data.plans.clear();
data.plans.ensureCapacity(Math.min(blocks, 1000));
var reads = Reads.get(stream);
var set = new IntSet();
for(int j = 0; j < blocks; j++){
@@ -466,25 +463,23 @@ public abstract class SaveVersion extends SaveFileReader{
}
}
public void readWorldEntities(DataInput stream) throws IOException{
//entityMapping is null in older save versions, so use the default
var mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping;
public void readWorldEntities(DataInput stream, Prov[] mapping) throws IOException{
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
readChunk(stream, true, in -> {
int typeid = in.readUnsignedByte();
readChunkReads(stream, (in, len) -> {
int typeid = in.ub();
if(mapping[typeid] == null){
in.skipBytes(lastRegionLength - 1);
in.skip(len - 1);
return;
}
int id = in.readInt();
int id = in.i();
Entityc entity = (Entityc)mapping[typeid].get();
EntityGroup.checkNextId(id);
entity.id(id);
entity.read(Reads.get(in));
entity.read(in);
entity.add();
});
}
@@ -492,9 +487,9 @@ public abstract class SaveVersion extends SaveFileReader{
Groups.all.each(Entityc::afterReadAll);
}
public void readEntityMapping(DataInput stream) throws IOException{
public Prov[] readEntityMapping(DataInput stream) throws IOException{
//copy entityMapping for further mutation; will be used in readWorldEntities
entityMapping = Arrays.copyOf(EntityMapping.idMap, EntityMapping.idMap.length);
Prov[] entityMapping = Arrays.copyOf(EntityMapping.idMap, EntityMapping.idMap.length);
short amount = stream.readShort();
for(int i = 0; i < amount; i++){
@@ -504,12 +499,14 @@ public abstract class SaveVersion extends SaveFileReader{
String name = stream.readUTF();
entityMapping[id] = EntityMapping.map(name);
}
return entityMapping;
}
public void readEntities(DataInput stream) throws IOException{
readEntityMapping(stream);
var mapping = readEntityMapping(stream);
readTeamBlocks(stream);
readWorldEntities(stream);
readWorldEntities(stream, mapping);
}
public void readContentHeader(DataInput stream) throws IOException{

View File

@@ -1,7 +1,6 @@
package mindustry.io.versions;
import arc.util.io.*;
import mindustry.io.*;
import mindustry.world.*;
import java.io.*;
@@ -9,7 +8,7 @@ import java.io.*;
import static mindustry.Vars.*;
/** This version does not read custom chunk data (<= 6). */
public class LegacyRegionSaveVersion extends SaveVersion{
public class LegacyRegionSaveVersion extends ShortChunkSaveVersion{
public LegacyRegionSaveVersion(int version){
super(version);
@@ -17,12 +16,12 @@ public class LegacyRegionSaveVersion extends SaveVersion{
@Override
public void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException{
region("meta", stream, counter, in -> readMeta(in, context));
region("content", stream, counter, this::readContentHeader);
readRegion("meta", stream, counter, in -> readMeta(in, context));
readRegion("content", stream, counter, this::readContentHeader);
try{
region("map", stream, counter, in -> readMap(in, context));
region("entities", stream, counter, this::readEntities);
readRegion("map", stream, counter, in -> readMap(in, context));
readRegion("entities", stream, counter, this::readEntities);
}finally{
content.setTemporaryMapper(null);

View File

@@ -1,7 +1,6 @@
package mindustry.io.versions;
import arc.util.*;
import arc.util.io.*;
import mindustry.content.*;
import mindustry.game.*;
import mindustry.world.*;
@@ -61,8 +60,8 @@ public abstract class LegacySaveVersion extends LegacyRegionSaveVersion{
if(block.hasBuilding()){
try{
readChunk(stream, true, in -> {
byte version = in.readByte();
readLegacyShortChunk(stream, (in, len) -> {
byte version = in.b();
//legacy impl of Building#read()
tile.build.health = stream.readUnsignedShort();
byte packedrot = stream.readByte();
@@ -72,14 +71,14 @@ public abstract class LegacySaveVersion extends LegacyRegionSaveVersion{
tile.setTeam(Team.get(team));
tile.build.rotation = rotation;
if(tile.build.items != null) tile.build.items.read(Reads.get(stream), true);
if(tile.build.power != null) tile.build.power.read(Reads.get(stream), true);
if(tile.build.liquids != null) tile.build.liquids.read(Reads.get(stream), true);
if(tile.build.items != null) tile.build.items.read(in, true);
if(tile.build.power != null) tile.build.power.read(in, true);
if(tile.build.liquids != null) tile.build.liquids.read(in, true);
//skip cons.valid boolean, it's not very important here
stream.readByte();
//read only from subclasses!
tile.build.read(Reads.get(in), version);
tile.build.read(in, version);
});
}catch(Throwable e){
throw new IOException("Failed to read tile entity of block: " + block, e);
@@ -111,7 +110,7 @@ public abstract class LegacySaveVersion extends LegacyRegionSaveVersion{
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
//simply skip all the entities
skipChunk(stream, true);
skipLegacyShortChunk(stream);
}
}
}

View File

@@ -1,7 +1,6 @@
package mindustry.io.versions;
import arc.func.*;
import arc.util.io.*;
import mindustry.gen.*;
import java.io.*;
@@ -14,21 +13,19 @@ public class LegacySaveVersion2 extends LegacyRegionSaveVersion{
}
@Override
public void readWorldEntities(DataInput stream) throws IOException{
//entityMapping is null in older save versions, so use the default
Prov[] mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping;
public void readWorldEntities(DataInput stream, Prov[] mapping) throws IOException{
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
readChunk(stream, true, in -> {
int typeid = in.readUnsignedByte();
readLegacyShortChunk(stream, (in, len) -> {
int typeid = in.ub();
if(mapping[typeid] == null){
in.skipBytes(lastRegionLength - 1);
in.skip(len - 1);
return;
}
Entityc entity = (Entityc)mapping[typeid].get();
entity.read(Reads.get(in));
entity.read(in);
entity.add();
});
}

View File

@@ -0,0 +1,11 @@
package mindustry.io.versions;
import mindustry.io.*;
/** Removes short entity chunks, switching to 4 byte lengths for all chunks. */
public class Save10 extends SaveVersion{
public Save10(){
super(10);
}
}

View File

@@ -1,5 +1,7 @@
package mindustry.io.versions;
import mindustry.gen.*;
import java.io.*;
/** This version only reads entities, no entity ID mappings. */
@@ -12,7 +14,7 @@ public class Save4 extends LegacySaveVersion2{
@Override
public void readEntities(DataInput stream) throws IOException{
readTeamBlocks(stream);
readWorldEntities(stream);
readWorldEntities(stream, EntityMapping.idMap);
}
}

View File

@@ -1,8 +1,6 @@
package mindustry.io.versions;
import mindustry.io.*;
public class Save7 extends SaveVersion{
public class Save7 extends ShortChunkSaveVersion{
public Save7(){
super(7);

View File

@@ -1,9 +1,7 @@
package mindustry.io.versions;
import mindustry.io.*;
/** Adds support for the marker binary data region. The code is unchanged here, because it was easier to add a >= 8 check in the SaveVersion class itself. */
public class Save8 extends SaveVersion{
public class Save8 extends ShortChunkSaveVersion{
public Save8(){
super(8);

View File

@@ -1,10 +1,7 @@
package mindustry.io.versions;
import mindustry.io.*;
/** Adds support for the new 7-byte custom tile data. This can read Save8 data, but Save8 doesn't know how to handle this version's output, thus the version change. */
public class Save9 extends SaveVersion{
/** Adds support for the new 7-byte custom tile data. This can read Save8 data, but Save8 doesn't know how to handle this version's output, thus the version change. This version is the last one that uses short chunks. */
public class Save9 extends ShortChunkSaveVersion{
public Save9(){
super(9);
}

View File

@@ -0,0 +1,153 @@
package mindustry.io.versions;
import arc.func.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
import mindustry.io.*;
import mindustry.world.*;
import java.io.*;
import static mindustry.Vars.*;
public class ShortChunkSaveVersion extends SaveVersion{
public ShortChunkSaveVersion(int version){
super(version);
}
@Override
public void readWorldEntities(DataInput stream, Prov[] mapping) throws IOException{
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
readLegacyShortChunk(stream, (in, len) -> {
int typeid = in.ub();
if(mapping[typeid] == null){
in.skip(len - 1);
return;
}
int id = in.i();
Entityc entity = (Entityc)mapping[typeid].get();
EntityGroup.checkNextId(id);
entity.id(id);
entity.read(in);
entity.add();
});
}
Groups.all.each(Entityc::afterReadAll);
}
@Override
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++){
Block block = content.block(stream.readShort());
Tile tile = context.tile(i);
if(block == null) block = Blocks.air;
boolean isCenter = true;
byte packedCheck = stream.readByte();
boolean hadEntity = (packedCheck & 1) != 0;
//old data format (bit 2): 1 byte only if no building is present
//new data format (bit 3): 7 bytes (3x block-specific bytes + 1x 4-byte extra data int)
boolean hadDataOld = (packedCheck & 2) != 0, hadDataNew = (packedCheck & 4) != 0;
byte data = 0, floorData = 0, overlayData = 0;
int extraData = 0;
if(hadDataNew){
data = stream.readByte();
floorData = stream.readByte();
overlayData = stream.readByte();
extraData = stream.readInt();
}
if(hadEntity){
isCenter = stream.readBoolean();
}
//set block only if this is the center; otherwise, it's handled elsewhere
if(isCenter){
tile.setBlock(block);
}
//must be assigned after setBlock, because that can reset data
if(hadDataNew){
tile.data = data;
tile.floorData = floorData;
tile.overlayData = overlayData;
tile.extraData = extraData;
context.onReadTileData();
}
if(hadEntity){
if(isCenter){ //only read entity for center blocks
if(block.hasBuilding()){
try{
readLegacyShortChunk(stream, (in, len) -> {
byte revision = in.b();
tile.build.readAll(in, revision);
});
}catch(Throwable e){
throw new IOException("Failed to read tile entity of block: " + block, e);
}
}else{
//skip the entity region, as the entity and its IO code are now gone
skipLegacyShortChunk(stream);
}
context.onReadBuilding();
}
}else if(hadDataOld || hadDataNew){ //never read consecutive blocks if there's any kind of data
if(hadDataOld){
tile.setBlock(block);
//the old data format was only read in the case where there is no building, and only contained a single byte
tile.data = stream.readByte();
}
}else{
int consecutives = stream.readUnsignedByte();
for(int j = i + 1; j < i + 1 + consecutives; j++){
context.tile(j).setBlock(block);
}
i += consecutives;
}
}
}finally{
if(!generating) context.end();
}
}
}