From 729f18726f888efd6c55f565e4e8cd9c57b13842 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 12 Sep 2025 14:42:22 -0400 Subject: [PATCH] Save version chunks changed to 4-byte lengths --- core/src/mindustry/core/NetClient.java | 7 +- core/src/mindustry/core/NetServer.java | 5 +- core/src/mindustry/game/Schematics.java | 7 +- core/src/mindustry/io/MapIO.java | 8 +- core/src/mindustry/io/SaveFileReader.java | 111 ++++++------- core/src/mindustry/io/SaveIO.java | 2 +- core/src/mindustry/io/SaveVersion.java | 89 +++++----- .../io/versions/LegacyRegionSaveVersion.java | 11 +- .../io/versions/LegacySaveVersion.java | 15 +- .../io/versions/LegacySaveVersion2.java | 13 +- core/src/mindustry/io/versions/Save10.java | 11 ++ core/src/mindustry/io/versions/Save4.java | 4 +- core/src/mindustry/io/versions/Save7.java | 4 +- core/src/mindustry/io/versions/Save8.java | 4 +- core/src/mindustry/io/versions/Save9.java | 7 +- .../io/versions/ShortChunkSaveVersion.java | 153 ++++++++++++++++++ core/src/mindustry/logic/LExecutor.java | 2 +- .../world/blocks/logic/SwitchBlock.java | 6 +- gradle.properties | 2 +- 19 files changed, 311 insertions(+), 150 deletions(-) create mode 100644 core/src/mindustry/io/versions/Save10.java create mode 100644 core/src/mindustry/io/versions/ShortChunkSaveVersion.java diff --git a/core/src/mindustry/core/NetClient.java b/core/src/mindustry/core/NetClient.java index 9e85367193..3d8202e4fd 100644 --- a/core/src/mindustry/core/NetClient.java +++ b/core/src/mindustry/core/NetClient.java @@ -61,6 +61,7 @@ public class NetClient implements ApplicationListener{ /** Byte stream for reading in snapshots. */ private ReusableByteInStream byteStream = new ReusableByteInStream(); private DataInputStream dataStream = new DataInputStream(byteStream); + private Reads dataStreamReads = new Reads(dataStream); /** Packet handlers for custom types of messages. */ private ObjectMap>> customPacketHandlers = new ObjectMap<>(); /** Packet handlers for custom types of messages, in binary. */ @@ -485,9 +486,10 @@ public class NetClient implements ApplicationListener{ netClient.lastSnapshotTimestamp = Time.millis(); netClient.byteStream.setBytes(data); DataInputStream input = netClient.dataStream; + Reads reads = netClient.dataStreamReads; for(int j = 0; j < amount; j++){ - readSyncEntity(input, Reads.get(input)); + readSyncEntity(input, reads); } }catch(Exception e){ //don't disconnect, just log it @@ -511,6 +513,7 @@ public class NetClient implements ApplicationListener{ try{ netClient.byteStream.setBytes(data); DataInputStream input = netClient.dataStream; + Reads reads = netClient.dataStreamReads; for(int i = 0; i < amount; i++){ 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); break; } - tile.build.readSync(Reads.get(input), tile.build.version()); + tile.build.readSync(reads, tile.build.version()); } }catch(Exception e){ Log.err(e); diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index ebd5136139..1e91e6a6b0 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -116,6 +116,7 @@ public class NetServer implements ApplicationListener{ private ReusableByteOutStream syncStream = new ReusableByteOutStream(); /** Data stream for writing player sync data to. */ private DataOutputStream dataStream = new DataOutputStream(syncStream); + private Writes dataStreamWrites = new Writes(dataStream); /** Packet handlers for custom types of messages. */ private ObjectMap>> customPacketHandlers = new ObjectMap<>(); /** Packet handlers for custom types of messages - binary version. */ @@ -938,7 +939,7 @@ public class NetServer implements ApplicationListener{ dataStream.writeInt(build.pos()); dataStream.writeShort(build.block.id); - build.writeSync(Writes.get(dataStream)); + build.writeSync(dataStreamWrites); if(syncStream.size() > maxSnapshotSize){ dataStream.close(); @@ -993,7 +994,7 @@ public class NetServer implements ApplicationListener{ dataStream.writeInt(entity.id()); //write id dataStream.writeByte(entity.classId() & 0xFF); //write type ID entity.beforeWrite(); - entity.writeSync(Writes.get(dataStream)); //write entity + entity.writeSync(dataStreamWrites); //write entity itself sent++; diff --git a/core/src/mindustry/game/Schematics.java b/core/src/mindustry/game/Schematics.java index a67ce3fa39..2333f83d45 100644 --- a/core/src/mindustry/game/Schematics.java +++ b/core/src/mindustry/game/Schematics.java @@ -593,11 +593,13 @@ public class Schematics implements Loadable{ if(total > 128 * 128) throw new IOException("Invalid schematic: Too many blocks."); + Reads read = new Reads(stream); + Seq tiles = new Seq<>(total); for(int i = 0; i < total; i++){ Block block = blocks.get(stream.readByte()); 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(); if(block != Blocks.air){ 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); try(DataOutputStream stream = new DataOutputStream(new DeflaterOutputStream(output))){ + Writes write = new Writes(stream); stream.writeShort(schematic.width); stream.writeShort(schematic.height); @@ -655,7 +658,7 @@ public class Schematics implements Loadable{ for(Stile tile : schematic.tiles){ stream.writeByte(blocks.orderedItems().indexOf(tile.block)); stream.writeInt(Point2.pack(tile.x, tile.y)); - TypeIO.writeObject(Writes.get(stream), tile.config); + TypeIO.writeObject(write, tile.config); stream.writeByte(tile.rotation); } } diff --git a/core/src/mindustry/io/MapIO.java b/core/src/mindustry/io/MapIO.java index 9213d573b3..d37cf8f43a 100644 --- a/core/src/mindustry/io/MapIO.java +++ b/core/src/mindustry/io/MapIO.java @@ -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(){ diff --git a/core/src/mindustry/io/SaveFileReader.java b/core/src/mindustry/io/SaveFileReader.java index 7cdc8f6d87..b09bd4004e 100644 --- a/core/src/mindustry/io/SaveFileReader.java +++ b/core/src/mindustry/io/SaveFileReader.java @@ -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 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 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 cons) throws IOException{ + public void writeRegion(String name, DataOutput stream, IORunner 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 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 runner) throws IOException{ - - //TODO awful + public void writeChunk(DataOutput output, IORunner 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 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 runner) throws IOException{ - int length = isShort ? input.readUnsignedShort() : input.readInt(); - lastRegionLength = length; - runner.accept(input); + public int readChunk(DataInput input, IORunnerLength 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 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 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{ + 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; diff --git a/core/src/mindustry/io/SaveIO.java b/core/src/mindustry/io/SaveIO.java index a575c10056..f383f4daf1 100644 --- a/core/src/mindustry/io/SaveIO.java +++ b/core/src/mindustry/io/SaveIO.java @@ -20,7 +20,7 @@ public class SaveIO{ /** Save format header. */ public static final byte[] header = {'M', 'S', 'A', 'V'}; public static final IntMap versions = new IntMap<>(); - public static final Seq 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 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){ diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 9ddb6519a2..0fa5bf6634 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -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 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{ diff --git a/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java b/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java index a616d985f0..4623352df2 100644 --- a/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java +++ b/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java @@ -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); diff --git a/core/src/mindustry/io/versions/LegacySaveVersion.java b/core/src/mindustry/io/versions/LegacySaveVersion.java index 6dcf6c2b9b..ef01f750a9 100644 --- a/core/src/mindustry/io/versions/LegacySaveVersion.java +++ b/core/src/mindustry/io/versions/LegacySaveVersion.java @@ -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); } } } diff --git a/core/src/mindustry/io/versions/LegacySaveVersion2.java b/core/src/mindustry/io/versions/LegacySaveVersion2.java index 02a986cb85..73d39488b7 100644 --- a/core/src/mindustry/io/versions/LegacySaveVersion2.java +++ b/core/src/mindustry/io/versions/LegacySaveVersion2.java @@ -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(); }); } diff --git a/core/src/mindustry/io/versions/Save10.java b/core/src/mindustry/io/versions/Save10.java new file mode 100644 index 0000000000..a7acf80422 --- /dev/null +++ b/core/src/mindustry/io/versions/Save10.java @@ -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); + } +} diff --git a/core/src/mindustry/io/versions/Save4.java b/core/src/mindustry/io/versions/Save4.java index dac92ee29d..793272e67b 100644 --- a/core/src/mindustry/io/versions/Save4.java +++ b/core/src/mindustry/io/versions/Save4.java @@ -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); } } diff --git a/core/src/mindustry/io/versions/Save7.java b/core/src/mindustry/io/versions/Save7.java index dde5dc8b8c..173e411800 100644 --- a/core/src/mindustry/io/versions/Save7.java +++ b/core/src/mindustry/io/versions/Save7.java @@ -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); diff --git a/core/src/mindustry/io/versions/Save8.java b/core/src/mindustry/io/versions/Save8.java index b65984b044..c463d435f3 100644 --- a/core/src/mindustry/io/versions/Save8.java +++ b/core/src/mindustry/io/versions/Save8.java @@ -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); diff --git a/core/src/mindustry/io/versions/Save9.java b/core/src/mindustry/io/versions/Save9.java index 08d1786e18..8a9a7dd64c 100644 --- a/core/src/mindustry/io/versions/Save9.java +++ b/core/src/mindustry/io/versions/Save9.java @@ -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); } diff --git a/core/src/mindustry/io/versions/ShortChunkSaveVersion.java b/core/src/mindustry/io/versions/ShortChunkSaveVersion.java new file mode 100644 index 0000000000..ce1300a622 --- /dev/null +++ b/core/src/mindustry/io/versions/ShortChunkSaveVersion.java @@ -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(); + } + } +} diff --git a/core/src/mindustry/logic/LExecutor.java b/core/src/mindustry/logic/LExecutor.java index ab2bbe6fcc..15223f2061 100644 --- a/core/src/mindustry/logic/LExecutor.java +++ b/core/src/mindustry/logic/LExecutor.java @@ -377,7 +377,7 @@ public class LExecutor{ if(unit instanceof Payloadc pay){ //units 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){ Call.pickedUnitPayload(unit, result); diff --git a/core/src/mindustry/world/blocks/logic/SwitchBlock.java b/core/src/mindustry/world/blocks/logic/SwitchBlock.java index db49b70f6c..7a470f27d4 100644 --- a/core/src/mindustry/world/blocks/logic/SwitchBlock.java +++ b/core/src/mindustry/world/blocks/logic/SwitchBlock.java @@ -42,7 +42,7 @@ public class SwitchBlock extends Block{ if(privileged) return; super.damage(damage); } - + @Override public boolean canPickup(){ return !privileged; @@ -80,8 +80,8 @@ public class SwitchBlock extends Block{ } @Override - public void readAll(Reads read, byte revision){ - super.readAll(read, revision); + public void read(Reads read, byte revision){ + super.read(read, revision); if(revision == 1){ enabled = read.bool(); diff --git a/gradle.properties b/gradle.properties index 5aabc69edc..4cd1a84579 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=390d5e8665 +archash=2c95712087