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

@@ -61,6 +61,7 @@ public class NetClient implements ApplicationListener{
/** Byte stream for reading in snapshots. */ /** Byte stream for reading in snapshots. */
private ReusableByteInStream byteStream = new ReusableByteInStream(); private ReusableByteInStream byteStream = new ReusableByteInStream();
private DataInputStream dataStream = new DataInputStream(byteStream); private DataInputStream dataStream = new DataInputStream(byteStream);
private Reads dataStreamReads = new Reads(dataStream);
/** Packet handlers for custom types of messages. */ /** Packet handlers for custom types of messages. */
private ObjectMap<String, Seq<Cons<String>>> customPacketHandlers = new ObjectMap<>(); private ObjectMap<String, Seq<Cons<String>>> customPacketHandlers = new ObjectMap<>();
/** Packet handlers for custom types of messages, in binary. */ /** Packet handlers for custom types of messages, in binary. */
@@ -485,9 +486,10 @@ public class NetClient implements ApplicationListener{
netClient.lastSnapshotTimestamp = Time.millis(); netClient.lastSnapshotTimestamp = Time.millis();
netClient.byteStream.setBytes(data); netClient.byteStream.setBytes(data);
DataInputStream input = netClient.dataStream; DataInputStream input = netClient.dataStream;
Reads reads = netClient.dataStreamReads;
for(int j = 0; j < amount; j++){ for(int j = 0; j < amount; j++){
readSyncEntity(input, Reads.get(input)); readSyncEntity(input, reads);
} }
}catch(Exception e){ }catch(Exception e){
//don't disconnect, just log it //don't disconnect, just log it
@@ -511,6 +513,7 @@ public class NetClient implements ApplicationListener{
try{ try{
netClient.byteStream.setBytes(data); netClient.byteStream.setBytes(data);
DataInputStream input = netClient.dataStream; DataInputStream input = netClient.dataStream;
Reads reads = netClient.dataStreamReads;
for(int i = 0; i < amount; i++){ for(int i = 0; i < amount; i++){
int pos = input.readInt(); int pos = input.readInt();
@@ -524,7 +527,7 @@ public class NetClient implements ApplicationListener{
Log.warn("Block ID mismatch at @: @ != @. Skipping block snapshot.", tile, tile.build.block.id, block); Log.warn("Block ID mismatch at @: @ != @. Skipping block snapshot.", tile, tile.build.block.id, block);
break; break;
} }
tile.build.readSync(Reads.get(input), tile.build.version()); tile.build.readSync(reads, tile.build.version());
} }
}catch(Exception e){ }catch(Exception e){
Log.err(e); Log.err(e);

View File

@@ -116,6 +116,7 @@ public class NetServer implements ApplicationListener{
private ReusableByteOutStream syncStream = new ReusableByteOutStream(); private ReusableByteOutStream syncStream = new ReusableByteOutStream();
/** Data stream for writing player sync data to. */ /** Data stream for writing player sync data to. */
private DataOutputStream dataStream = new DataOutputStream(syncStream); private DataOutputStream dataStream = new DataOutputStream(syncStream);
private Writes dataStreamWrites = new Writes(dataStream);
/** Packet handlers for custom types of messages. */ /** Packet handlers for custom types of messages. */
private ObjectMap<String, Seq<Cons2<Player, String>>> customPacketHandlers = new ObjectMap<>(); private ObjectMap<String, Seq<Cons2<Player, String>>> customPacketHandlers = new ObjectMap<>();
/** Packet handlers for custom types of messages - binary version. */ /** Packet handlers for custom types of messages - binary version. */
@@ -938,7 +939,7 @@ public class NetServer implements ApplicationListener{
dataStream.writeInt(build.pos()); dataStream.writeInt(build.pos());
dataStream.writeShort(build.block.id); dataStream.writeShort(build.block.id);
build.writeSync(Writes.get(dataStream)); build.writeSync(dataStreamWrites);
if(syncStream.size() > maxSnapshotSize){ if(syncStream.size() > maxSnapshotSize){
dataStream.close(); dataStream.close();
@@ -993,7 +994,7 @@ public class NetServer implements ApplicationListener{
dataStream.writeInt(entity.id()); //write id dataStream.writeInt(entity.id()); //write id
dataStream.writeByte(entity.classId() & 0xFF); //write type ID dataStream.writeByte(entity.classId() & 0xFF); //write type ID
entity.beforeWrite(); entity.beforeWrite();
entity.writeSync(Writes.get(dataStream)); //write entity entity.writeSync(dataStreamWrites); //write entity itself
sent++; sent++;

View File

@@ -593,11 +593,13 @@ public class Schematics implements Loadable{
if(total > 128 * 128) throw new IOException("Invalid schematic: Too many blocks."); if(total > 128 * 128) throw new IOException("Invalid schematic: Too many blocks.");
Reads read = new Reads(stream);
Seq<Stile> tiles = new Seq<>(total); Seq<Stile> tiles = new Seq<>(total);
for(int i = 0; i < total; i++){ for(int i = 0; i < total; i++){
Block block = blocks.get(stream.readByte()); Block block = blocks.get(stream.readByte());
int position = stream.readInt(); int position = stream.readInt();
Object config = ver == 0 ? mapConfig(block, stream.readInt(), position) : TypeIO.readObject(Reads.get(stream), false, mapper); Object config = ver == 0 ? mapConfig(block, stream.readInt(), position) : TypeIO.readObject(read, false, mapper);
byte rotation = stream.readByte(); byte rotation = stream.readByte();
if(block != Blocks.air){ if(block != Blocks.air){
tiles.add(new Stile(block, Point2.x(position), Point2.y(position), config, rotation)); tiles.add(new Stile(block, Point2.x(position), Point2.y(position), config, rotation));
@@ -619,6 +621,7 @@ public class Schematics implements Loadable{
output.write(version); output.write(version);
try(DataOutputStream stream = new DataOutputStream(new DeflaterOutputStream(output))){ try(DataOutputStream stream = new DataOutputStream(new DeflaterOutputStream(output))){
Writes write = new Writes(stream);
stream.writeShort(schematic.width); stream.writeShort(schematic.width);
stream.writeShort(schematic.height); stream.writeShort(schematic.height);
@@ -655,7 +658,7 @@ public class Schematics implements Loadable{
for(Stile tile : schematic.tiles){ for(Stile tile : schematic.tiles){
stream.writeByte(blocks.orderedItems().indexOf(tile.block)); stream.writeByte(blocks.orderedItems().indexOf(tile.block));
stream.writeInt(Point2.pack(tile.x, tile.y)); stream.writeInt(Point2.pack(tile.x, tile.y));
TypeIO.writeObject(Writes.get(stream), tile.config); TypeIO.writeObject(write, tile.config);
stream.writeByte(tile.rotation); stream.writeByte(tile.rotation);
} }
} }

View File

@@ -41,7 +41,7 @@ public class MapIO{
SaveVersion ver = SaveIO.getSaveWriter(version); 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?"); 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(); 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); 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(); int version = stream.readInt();
SaveVersion ver = SaveIO.getSaveWriter(version); 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?"); 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 floors = new Pixmap(map.width, map.height);
Pixmap walls = 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.readRegion("content", stream, counter, ver::readContentHeader);
ver.region("preview_map", stream, counter, in -> ver.readMap(in, new WorldContext(){ ver.readRegion("preview_map", stream, counter, in -> ver.readMap(in, new WorldContext(){
@Override public void resize(int width, int height){} @Override public void resize(int width, int height){}
@Override public boolean isGenerating(){return false;} @Override public boolean isGenerating(){return false;}
@Override public void begin(){ @Override public void begin(){

View File

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

View File

@@ -20,7 +20,7 @@ public class SaveIO{
/** Save format header. */ /** Save format header. */
public static final byte[] header = {'M', 'S', 'A', 'V'}; public static final byte[] header = {'M', 'S', 'A', 'V'};
public static final IntMap<SaveVersion> versions = new IntMap<>(); 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{ static{
for(SaveVersion version : versionArray){ for(SaveVersion version : versionArray){

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
package mindustry.io.versions; 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. */ /** 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(){ public Save8(){
super(8); super(8);

View File

@@ -1,10 +1,7 @@
package mindustry.io.versions; 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. This version is the last one that uses short chunks. */
public class Save9 extends ShortChunkSaveVersion{
/** 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{
public Save9(){ public Save9(){
super(9); 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();
}
}
}

View File

@@ -377,7 +377,7 @@ public class LExecutor{
if(unit instanceof Payloadc pay){ if(unit instanceof Payloadc pay){
//units //units
if(p1.bool()){ if(p1.bool()){
Unit result = Units.closest(unit.team, unit.x, unit.y, unit.type.hitSize * 2f, u -> u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize * 1.2f)); Unit result = Units.closest(unit.team, unit.x, unit.y, unit.type.hitSize * 2f, u -> u != unit && u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize * 1.2f));
if(result != null){ if(result != null){
Call.pickedUnitPayload(unit, result); Call.pickedUnitPayload(unit, result);

View File

@@ -42,7 +42,7 @@ public class SwitchBlock extends Block{
if(privileged) return; if(privileged) return;
super.damage(damage); super.damage(damage);
} }
@Override @Override
public boolean canPickup(){ public boolean canPickup(){
return !privileged; return !privileged;
@@ -80,8 +80,8 @@ public class SwitchBlock extends Block{
} }
@Override @Override
public void readAll(Reads read, byte revision){ public void read(Reads read, byte revision){
super.readAll(read, revision); super.read(read, revision);
if(revision == 1){ if(revision == 1){
enabled = read.bool(); enabled = read.bool();

View File

@@ -26,4 +26,4 @@ org.gradle.caching=true
org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.socketTimeout=100000
org.gradle.internal.http.connectionTimeout=100000 org.gradle.internal.http.connectionTimeout=100000
android.enableR8.fullMode=false android.enableR8.fullMode=false
archash=390d5e8665 archash=2c95712087