Anuken/Mindustry · error · IOException

Could not skip bytes. Expected length: {}; Actual length: {}

Error message

Could not skip bytes. Expected length: {}; Actual length: {}

What it means

Thrown by SaveFileReader.skipChunk after reading a 4-byte chunk length and calling DataInput.skipBytes(length). The Java DataInput contract allows skipBytes to skip fewer bytes than requested (it is best-effort and returns the number actually skipped); this guard fires when the returned count does not match the declared chunk length. The mismatch almost always means the underlying stream is truncated or the chunk header is corrupt rather than a transient skip failure.

Source

Thrown at core/src/mindustry/io/SaveFileReader.java:154

        int length = input.readInt();
        runner.accept(input, length);
        return length;
    }

    /** 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) 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);
        }

View on GitHub (pinned to f695ad7e60)

Solutions

  1. Treat the save as corrupt: call SaveIO.isSaveValid(file) first, and on failure fall back to the auto-generated backup via SaveIO.getBackupStream / SaveIO.load(file) which already retries the -backup file.
  2. If loading a custom DataInput, verify the source stream has at least length bytes remaining before invoking skipChunk (use a CounterInputStream or available()/mark).
  3. Re-save the game in the current version so the chunk layout matches, instead of loading an older/cross-version file.
  4. If you control the writer, ensure writeChunk always writes a length prefix that exactly equals the buffered output size (it does in SaveFileReader.writeChunk).

Example fix

// before
try {
    SaveIO.load(file);
} catch (SaveException e) {
    // surface raw error
}

// after
if (SaveIO.isSaveValid(file)) {
    SaveIO.load(file);
} else {
    ui.showInfo("This save file is corrupt or from an incompatible version.");
}
Defensive patterns

Strategy: validation

Validate before calling

// Before loading, confirm the save parses end-to-end.
if (!SaveIO.isSaveValid(file)) {
    throw new IllegalArgumentException("Save file is corrupt or truncated: " + file);
}
// SaveIO.load(Fi) already retries the -backup file on SaveException.

Type guard

null

Try / catch

try {
    SaveIO.load(file);
} catch (SaveException e) {
    // both primary and backup failed; report to user
    ui.showInfo(bundle.get("save.corrupt"));
}

Prevention

When it happens

Trigger: Called from SaveVersion.readMap (SaveVersion.java:373) when a tile had an entity in the save but the block no longer has Building IO code, so the entity region must be discarded. Also fires anywhere skipChunk is invoked on a stream whose actual remaining bytes are fewer than the length prefix indicates.

Common situations: Loading a save from an incompatible game version where chunk layout shifted; a truncated .msav (disk write interrupted / partial download); a corrupt deflated stream that decompressed to fewer bytes than the length prefix claimed; manual editing of a save file that left the chunk length stale.

Related errors


AI-assisted analysis of Anuken/Mindustry@f695ad7e60 (2026-08-14). Data as JSON: /api/errors/054468e74c613363. Report an issue: GitHub.