Binary layout of the island code (clipboard / save string) and how to decode and encode it

Island Save Format

An island code is the string the game's dev console copies to the clipboard (saveisland) and reads back (loadisland). It is a serialized IslandGrid.SyncData - the same structure that is network-synced between players.

Status

Fully reverse engineered and validated with decode -> encode -> byte-identical round trips on real islands.

Pipeline

island code (text)
  -> base64 decode
  -> raw DEFLATE inflate         (NO zlib header, NO gzip header)
  -> MLAPI packed-int stream     (IslandGrid.SyncData)

Encoding is the same pipeline in reverse. In JavaScript the browser primitives DecompressionStream('deflate-raw') / CompressionStream('deflate-raw') do exactly this; in C# use DeflateStream (not ZLibStream).

Packed integers

Every number in the stream is an MLAPI UInt64Packed varint. Signed values (int32) are zigzag-encoded first: 0, -1, 1, -2, 2 ... -> 0, 1, 2, 3, 4 ...

First byte b Total size Value
0x00 - 0xF0 1 byte b
0xF1 - 0xF8 2 bytes (b - 0xF1) * 256 + next + 0xF0
0xF9 3 bytes hi * 256 + lo + 0x8F0
0xFA - 0xFF 1 + (b - 0xF7) bytes the following n = b - 0xF7 bytes, little endian

Stream layout

All counts and values below are packed ints (i32 = zigzag, u64 = raw).

buildings[]   : i32 count, then count x Building
remains[]     : i32 count, then count x Building      (destroyed buildings, same layout)
groundTiles   : RLE  (i32 totalLength, then runs of {i32 runLength, i32 value})
natureTiles   : RLE  (same layout)
innerCliffs[] : i32 count, then count x { i32 c, i32 a, i32 b, i32 x, i32 y, i32 orient, i32 mirror }

Building (used by both buildings and remains):

Field Type Meaning
type i32 Building type id (see Buildings)
x, y i32 Grid position of the building origin
orient i32 Rotation, 0-3 (see Footprint and rotation)
mirror i32 0 / 1 - mirrors X
health i32 Current health (max is BuildingType.maxHealth, see Health)
disaster u64 flag, then u64 payload if flag != 0 Nullable disaster state
turn i32 Turn counter

The tile arrays are row-major, index = y * side + x, with totalLength == side * side.

Ground tile values

Value Name
0 WATER
1 WATER_ROCK (a ground tile, not a nature tile)
2 GRASS
3 SAND
4 INNER_CLIFF
5 METEORITE
6 PIER

Nature tile values

Value Name
-1 none
0 TREE
1 ROCK
2 SCORCHED_EARTH
3 LAVA
4 FLOWER
5 FISH

Reference decoder (C#)

using System.IO.Compression;

public sealed class PackedReader
{
    readonly byte[] b; int p;
    public PackedReader(byte[] data) { b = data; }
    public int Position => p;

    byte Byte() => p < b.Length ? b[p++] : throw new EndOfStreamException();

    public ulong U64()
    {
        int first = Byte();
        if (first <= 0xF0) return (ulong)first;
        if (first < 0xF9) return (ulong)((first - 0xF1) * 256 + Byte() + 0xF0);
        if (first == 0xF9) { int hi = Byte(), lo = Byte(); return (ulong)(hi * 256 + lo + 0x8F0); }
        int n = first - 0xF7;
        ulong v = 0;
        for (int i = 0; i < n; i++) v |= (ulong)Byte() << (8 * i);
        return v;
    }

    // zigzag: even -> x/2, odd -> -(x+1)/2
    public int I32() { ulong x = U64(); return (x & 1) == 0 ? (int)(x >> 1) : -(int)((x + 1) >> 1); }
}

public static byte[] Unpack(string islandCode)
{
    byte[] compressed = Convert.FromBase64String(islandCode.Trim());
    using var input = new MemoryStream(compressed);
    using var deflate = new DeflateStream(input, CompressionMode.Decompress); // raw DEFLATE
    using var output = new MemoryStream();
    deflate.CopyTo(output);
    return output.ToArray();
}

Reading the whole structure (works the same inside a BepInEx plugin):

List<Building> ReadBuildings(PackedReader r)
{
    int n = r.I32();
    var list = new List<Building>(n);
    for (int i = 0; i < n; i++)
    {
        var b = new Building {
            Type = r.I32(), X = r.I32(), Y = r.I32(),
            Orient = r.I32(), Mirror = r.I32(), Health = r.I32()
        };
        if (r.U64() != 0) b.Disaster = r.U64();
        b.Turn = r.I32();
        list.Add(b);
    }
    return list;
}

int[] ReadRle(PackedReader r)
{
    int total = r.I32();
    var values = new List<int>(total);
    while (values.Count < total)
    {
        int run = r.I32(), v = r.I32();
        for (int i = 0; i < run; i++) values.Add(v);
    }
    return values.ToArray();
}

Round-trip rule

When editing, always re-encode with the exact same field order. Unknown or unedited lists (remains, innerCliffs) must be written back untouched - the game rejects or mis-loads streams that are even one packed int off.