Task 424: .MSAV File Format

Task 424: .MSAV File Format

.MSAV File Format Specifications

The .MSAV file format is a binary format used by Mindustry, an open-source tower defense game, to store map data (including saved games or custom maps). There is no official public documentation for the format, but its structure can be inferred from the game's open-source Java code (specifically in MapIO.java and related classes like SaveIO.java). The format uses big-endian byte order, strings are encoded as modified UTF-8 (prefixed with a short length), and tile layers (floor, ore, etc.) are compressed using Deflate (ZIP-compatible compression). The format includes a header with magic bytes, a version, metadata tags, map dimensions, and then compressed tile and entity data.

1. List of All Properties Intrinsic to the File Format

Based on the format structure, the intrinsic properties (header fields and metadata that define the map's characteristics) are:

  • Magic Bytes: 4-byte string "mind" (hex: 6D 69 6E 64)
  • Version: Short integer (2 bytes) indicating the save format version (typically 5 in recent versions)
  • Metadata Tags: A collection of key-value pairs (variable number, each key and value is a length-prefixed UTF-8 string). Common keys include:
  • "name": The map's name (string)
  • "author": The creator's name (string)
  • "description": A text description of the map (string)
  • "build": The game build number when the map was saved (string representation of an integer)
  • "size": The map size hint (string, often redundant with width/height)
  • "rules": Serialized game rules (JSON-like string or base64-encoded object, including settings like wave intervals, banned items, spawn points, etc.)
  • Other dynamic tags may exist depending on the map (e.g., custom mods or settings)
  • Width: Short integer (2 bytes) for the map's width in tiles
  • Height: Short integer (2 bytes) for the map's height in tiles
  • Floor Layer: Compressed (Deflate) byte array of floor tile IDs (one byte per tile, decompressed size = width * height)
  • Ore Layer: Compressed (Deflate) byte array of ore/overlay tile IDs (one byte per tile, decompressed size = width * height)
  • Block Layer: Compressed (Deflate) data for block tiles (variable structure per tile: short block ID, byte team ID, byte rotation, optional config data as serialized object)
  • Entities: Number of entities (int), followed by serialized entity data (each entity includes type, position, health, etc.; variable length and compressed)

These properties are "intrinsic" as they define the core structure and content of the map file, independent of external file system metadata (e.g., file size or creation date).

  1. https://raw.githubusercontent.com/Anuken/Mindustry/master/core/assets/maps/aegis.msav
  2. https://raw.githubusercontent.com/Anuken/Mindustry/master/core/assets/maps/atlas.msav

These are official maps from the Mindustry repository.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .MSAV File Dump

Here's an HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post. It creates a drag-and-drop area. When a .MSAV file is dropped, it parses the file using a FileReader and DataView, extracts the properties, and dumps them to the screen (as a pre-formatted text block). It handles the header and tags but summarizes tile/entity data (as dumping full arrays would be impractical for large maps). Compression uses pako.js (include via CDN for inflate).

Drag and drop a .MSAV file here


4. Python Class for .MSAV File Handling

Here's a Python class that can open, decode, read, write, and print the properties to console. It uses struct for binary parsing and zlib for decompression. The write method recreates the file from parsed data.

import struct
import zlib

class MSAVHandler:
    def __init__(self, filename):
        self.filename = filename
        self.magic = None
        self.version = None
        self.tags = {}
        self.width = None
        self.height = None
        self.floor_data = None  # decompressed
        self.ore_data = None
        self.block_data = None
        self.entity_count = None
        self._raw_floor = None  # compressed for write
        self._raw_ore = None
        self._raw_block = None
        self._raw_entities = b''  # remaining for simplicity

    def read(self):
        with open(self.filename, 'rb') as f:
            data = f.read()
        offset = 0
        self.magic = data[offset:offset+4].decode('utf-8')
        offset += 4
        self.version = struct.unpack('>h', data[offset:offset+2])[0]
        offset += 2
        tag_count = struct.unpack('>i', data[offset:offset+4])[0]
        offset += 4
        for _ in range(tag_count):
            key_len = struct.unpack('>h', data[offset:offset+2])[0]
            offset += 2
            key = data[offset:offset+key_len].decode('utf-8')
            offset += key_len
            val_len = struct.unpack('>h', data[offset:offset+2])[0]
            offset += 2
            val = data[offset:offset+val_len].decode('utf-8')
            offset += val_len
            self.tags[key] = val
        self.width = struct.unpack('>h', data[offset:offset+2])[0]
        offset += 2
        self.height = struct.unpack('>h', data[offset:offset+2])[0]
        offset += 2
        floor_len = struct.unpack('>i', data[offset:offset+4])[0]
        offset += 4
        self._raw_floor = data[offset:offset+floor_len]
        self.floor_data = zlib.decompress(self._raw_floor)
        offset += floor_len
        ore_len = struct.unpack('>i', data[offset:offset+4])[0]
        offset += 4
        self._raw_ore = data[offset:offset+ore_len]
        self.ore_data = zlib.decompress(self._raw_ore)
        offset += ore_len
        block_len = struct.unpack('>i', data[offset:offset+4])[0]
        offset += 4
        self._raw_block = data[offset:offset+block_len]
        self.block_data = zlib.decompress(self._raw_block)
        offset += block_len
        self.entity_count = struct.unpack('>i', data[offset:offset+4])[0]
        # Remaining data not parsed in detail for this example
        self._raw_entities = data[offset+4:]

    def print_properties(self):
        print(f"Magic: {self.magic}")
        print(f"Version: {self.version}")
        print("Tags:")
        for k, v in self.tags.items():
            print(f"  {k}: {v}")
        print(f"Width: {self.width}")
        print(f"Height: {self.height}")
        print(f"Floor Layer: {len(self.floor_data)} bytes decompressed")
        print(f"Ore Layer: {len(self.ore_data)} bytes decompressed")
        print(f"Block Layer: {len(self.block_data)} bytes decompressed")
        print(f"Entities: {self.entity_count}")

    def write(self, new_filename=None):
        if not new_filename:
            new_filename = self.filename
        with open(new_filename, 'wb') as f:
            f.write(self.magic.encode('utf-8'))
            f.write(struct.pack('>h', self.version))
            f.write(struct.pack('>i', len(self.tags)))
            for k, v in self.tags.items():
                key_b = k.encode('utf-8')
                val_b = v.encode('utf-8')
                f.write(struct.pack('>h', len(key_b)))
                f.write(key_b)
                f.write(struct.pack('>h', len(val_b)))
                f.write(val_b)
            f.write(struct.pack('>h', self.width))
            f.write(struct.pack('>h', self.height))
            f.write(struct.pack('>i', len(self._raw_floor)))
            f.write(self._raw_floor)
            f.write(struct.pack('>i', len(self._raw_ore)))
            f.write(self._raw_ore)
            f.write(struct.pack('>i', len(self._raw_block)))
            f.write(self._raw_block)
            f.write(struct.pack('>i', self.entity_count))
            f.write(self._raw_entities)

# Example usage
# handler = MSAVHandler('example.msav')
# handler.read()
# handler.print_properties()
# handler.write('output.msav')

5. Java Class for .MSAV File Handling

Here's a Java class using DataInputStream and DataOutputStream for reading/writing, and Inflater for decompression.

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.InflaterInputStream;
import java.util.zip.DeflaterOutputStream;

public class MSAVHandler {
    private String filename;
    private String magic;
    private short version;
    private Map<String, String> tags = new HashMap<>();
    private short width;
    private short height;
    private byte[] floorData;  // decompressed
    private byte[] oreData;
    private byte[] blockData;
    private int entityCount;
    private byte[] rawFloor;  // compressed
    private byte[] rawOre;
    private byte[] rawBlock;
    private byte[] rawEntities;

    public MSAVHandler(String filename) {
        this.filename = filename;
    }

    public void read() throws IOException {
        try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(filename)))) {
            byte[] magicBytes = new byte[4];
            in.readFully(magicBytes);
            magic = new String(magicBytes, "UTF-8");
            version = in.readShort();
            int tagCount = in.readInt();
            for (int i = 0; i < tagCount; i++) {
                String key = in.readUTF();
                String val = in.readUTF();
                tags.put(key, val);
            }
            width = in.readShort();
            height = in.readShort();
            int floorLen = in.readInt();
            rawFloor = new byte[floorLen];
            in.readFully(rawFloor);
            floorData = decompress(rawFloor);
            int oreLen = in.readInt();
            rawOre = new byte[oreLen];
            in.readFully(rawOre);
            oreData = decompress(rawOre);
            int blockLen = in.readInt();
            rawBlock = new byte[blockLen];
            in.readFully(rawBlock);
            blockData = decompress(rawBlock);
            entityCount = in.readInt();
            // Read remaining as raw for write
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                baos.write(buf, 0, len);
            }
            rawEntities = baos.toByteArray();
        }
    }

    private byte[] decompress(byte[] compressed) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(compressed);
        InflaterInputStream iis = new InflaterInputStream(bais);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[1024];
        int len;
        while ((len = iis.read(buf)) > 0) {
            baos.write(buf, 0, len);
        }
        return baos.toByteArray();
    }

    public void printProperties() {
        System.out.println("Magic: " + magic);
        System.out.println("Version: " + version);
        System.out.println("Tags:");
        tags.forEach((k, v) -> System.out.println("  " + k + ": " + v));
        System.out.println("Width: " + width);
        System.out.println("Height: " + height);
        System.out.println("Floor Layer: " + floorData.length + " bytes decompressed");
        System.out.println("Ore Layer: " + oreData.length + " bytes decompressed");
        System.out.println("Block Layer: " + blockData.length + " bytes decompressed");
        System.out.println("Entities: " + entityCount);
    }

    public void write(String newFilename) throws IOException {
        if (newFilename == null) newFilename = filename;
        try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(newFilename)))) {
            out.write(magic.getBytes("UTF-8"));
            out.writeShort(version);
            out.writeInt(tags.size());
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                out.writeUTF(entry.getKey());
                out.writeUTF(entry.getValue());
            }
            out.writeShort(width);
            out.writeShort(height);
            out.writeInt(rawFloor.length);
            out.write(rawFloor);
            out.writeInt(rawOre.length);
            out.write(rawOre);
            out.writeInt(rawBlock.length);
            out.write(rawBlock);
            out.writeInt(entityCount);
            out.write(rawEntities);
        }
    }

    // Example usage
    // public static void main(String[] args) throws IOException {
    //     MSAVHandler handler = new MSAVHandler("example.msav");
    //     handler.read();
    //     handler.printProperties();
    //     handler.write("output.msav");
    // }
}

6. JavaScript Class for .MSAV File Handling

Here's a JavaScript class (Node.js compatible) using fs for file I/O and zlib for decompression. Run with Node.js.

const fs = require('fs');
const zlib = require('zlib');

class MSAVHandler {
  constructor(filename) {
    this.filename = filename;
    this.magic = null;
    this.version = null;
    this.tags = {};
    this.width = null;
    this.height = null;
    this.floorData = null;
    this.oreData = null;
    this.blockData = null;
    this.entityCount = null;
    this.rawFloor = null;
    this.rawOre = null;
    this.rawBlock = null;
    this.rawEntities = null;
  }

  read() {
    const data = fs.readFileSync(this.filename);
    const view = new DataView(data.buffer);
    let offset = 0;
    this.magic = new TextDecoder().decode(data.subarray(offset, offset + 4));
    offset += 4;
    this.version = view.getInt16(offset, false); // big-endian
    offset += 2;
    const tagCount = view.getInt32(offset, false);
    offset += 4;
    for (let i = 0; i < tagCount; i++) {
      const keyLen = view.getUint16(offset, false);
      offset += 2;
      const key = new TextDecoder().decode(data.subarray(offset, offset + keyLen));
      offset += keyLen;
      const valLen = view.getUint16(offset, false);
      offset += 2;
      const val = new TextDecoder().decode(data.subarray(offset, offset + valLen));
      offset += valLen;
      this.tags[key] = val;
    }
    this.width = view.getInt16(offset, false);
    offset += 2;
    this.height = view.getInt16(offset, false);
    offset += 2;
    const floorLen = view.getInt32(offset, false);
    offset += 4;
    this.rawFloor = data.subarray(offset, offset + floorLen);
    this.floorData = zlib.inflateSync(this.rawFloor);
    offset += floorLen;
    const oreLen = view.getInt32(offset, false);
    offset += 4;
    this.rawOre = data.subarray(offset, offset + oreLen);
    this.oreData = zlib.inflateSync(this.rawOre);
    offset += oreLen;
    const blockLen = view.getInt32(offset, false);
    offset += 4;
    this.rawBlock = data.subarray(offset, offset + blockLen);
    this.blockData = zlib.inflateSync(this.rawBlock);
    offset += blockLen;
    this.entityCount = view.getInt32(offset, false);
    offset += 4;
    this.rawEntities = data.subarray(offset);
  }

  printProperties() {
    console.log(`Magic: ${this.magic}`);
    console.log(`Version: ${this.version}`);
    console.log('Tags:');
    for (const [k, v] of Object.entries(this.tags)) {
      console.log(`  ${k}: ${v}`);
    }
    console.log(`Width: ${this.width}`);
    console.log(`Height: ${this.height}`);
    console.log(`Floor Layer: ${this.floorData.length} bytes decompressed`);
    console.log(`Ore Layer: ${this.oreData.length} bytes decompressed`);
    console.log(`Block Layer: ${this.blockData.length} bytes decompressed`);
    console.log(`Entities: ${this.entityCount}`);
  }

  write(newFilename = this.filename) {
    let buffer = Buffer.alloc(0);
    buffer = Buffer.concat([buffer, Buffer.from(this.magic, 'utf-8')]);
    const view = new DataView(new ArrayBuffer(2 + 4));
    view.setInt16(0, this.version, false);
    view.setInt32(2, Object.keys(this.tags).length, false);
    buffer = Buffer.concat([buffer, Buffer.from(view.buffer)]);
    for (const [k, v] of Object.entries(this.tags)) {
      const keyB = Buffer.from(k, 'utf-8');
      const valB = Buffer.from(v, 'utf-8');
      const headerView = new DataView(new ArrayBuffer(4));
      headerView.setUint16(0, keyB.length, false);
      headerView.setUint16(2, valB.length, false);
      buffer = Buffer.concat([buffer, Buffer.from(headerView.buffer), keyB, valB]);
    }
    const dimView = new DataView(new ArrayBuffer(4));
    dimView.setInt16(0, this.width, false);
    dimView.setInt16(2, this.height, false);
    buffer = Buffer.concat([buffer, Buffer.from(dimView.buffer)]);
    const floorView = new DataView(new ArrayBuffer(4));
    floorView.setInt32(0, this.rawFloor.length, false);
    buffer = Buffer.concat([buffer, Buffer.from(floorView.buffer), this.rawFloor]);
    const oreView = new DataView(new ArrayBuffer(4));
    oreView.setInt32(0, this.rawOre.length, false);
    buffer = Buffer.concat([buffer, Buffer.from(oreView.buffer), this.rawOre]);
    const blockView = new DataView(new ArrayBuffer(4));
    blockView.setInt32(0, this.rawBlock.length, false);
    buffer = Buffer.concat([buffer, Buffer.from(blockView.buffer), this.rawBlock]);
    const entityView = new DataView(new ArrayBuffer(4));
    entityView.setInt32(0, this.entityCount, false);
    buffer = Buffer.concat([buffer, Buffer.from(entityView.buffer), this.rawEntities]);
    fs.writeFileSync(newFilename, buffer);
  }
}

// Example usage
// const handler = new MSAVHandler('example.msav');
// handler.read();
// handler.printProperties();
// handler.write('output.msav');

7. C Class for .MSAV File Handling

Here's a C implementation using structs for a class-like structure. It uses fopen for I/O and zlib for decompression (compile with -lz). The "class" is a struct with functions.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <zlib.h>

typedef struct {
  char *filename;
  char magic[5];
  int16_t version;
  struct Tag {
    char *key;
    char *val;
  } *tags;
  size_t tagCount;
  int16_t width;
  int16_t height;
  uint8_t *floorData; size_t floorSize;
  uint8_t *oreData; size_t oreSize;
  uint8_t *blockData; size_t blockSize;
  int32_t entityCount;
  uint8_t *rawFloor; size_t rawFloorSize;
  uint8_t *rawOre; size_t rawOreSize;
  uint8_t *rawBlock; size_t rawBlockSize;
  uint8_t *rawEntities; size_t rawEntitiesSize;
} MSAVHandler;

MSAVHandler* msav_create(const char *filename) {
  MSAVHandler *h = calloc(1, sizeof(MSAVHandler));
  h->filename = strdup(filename);
  return h;
}

void msav_destroy(MSAVHandler *h) {
  for (size_t i = 0; i < h->tagCount; i++) {
    free(h->tags[i].key);
    free(h->tags[i].val);
  }
  free(h->tags);
  free(h->floorData);
  free(h->oreData);
  free(h->blockData);
  free(h->rawFloor);
  free(h->rawOre);
  free(h->rawBlock);
  free(h->rawEntities);
  free(h->filename);
  free(h);
}

int msav_read(MSAVHandler *h) {
  FILE *f = fopen(h->filename, "rb");
  if (!f) return 1;
  fseek(f, 0, SEEK_END);
  size_t fileSize = ftell(f);
  fseek(f, 0, SEEK_SET);
  uint8_t *data = malloc(fileSize);
  fread(data, 1, fileSize, f);
  fclose(f);
  size_t offset = 0;
  memcpy(h->magic, data + offset, 4); h->magic[4] = '\0';
  offset += 4;
  memcpy(&h->version, data + offset, 2); h->version = __builtin_bswap16(h->version); // Assume big-endian
  offset += 2;
  int32_t tagCount32; memcpy(&tagCount32, data + offset, 4); tagCount32 = __builtin_bswap32(tagCount32);
  h->tagCount = tagCount32;
  offset += 4;
  h->tags = malloc(h->tagCount * sizeof(struct Tag));
  for (size_t i = 0; i < h->tagCount; i++) {
    uint16_t keyLen; memcpy(&keyLen, data + offset, 2); keyLen = __builtin_bswap16(keyLen);
    offset += 2;
    h->tags[i].key = malloc(keyLen + 1); memcpy(h->tags[i].key, data + offset, keyLen); h->tags[i].key[keyLen] = '\0';
    offset += keyLen;
    uint16_t valLen; memcpy(&valLen, data + offset, 2); valLen = __builtin_bswap16(valLen);
    offset += 2;
    h->tags[i].val = malloc(valLen + 1); memcpy(h->tags[i].val, data + offset, valLen); h->tags[i].val[valLen] = '\0';
    offset += valLen;
  }
  memcpy(&h->width, data + offset, 2); h->width = __builtin_bswap16(h->width);
  offset += 2;
  memcpy(&h->height, data + offset, 2); h->height = __builtin_bswap16(h->height);
  offset += 2;
  int32_t floorLen; memcpy(&floorLen, data + offset, 4); floorLen = __builtin_bswap32(floorLen);
  offset += 4;
  h->rawFloor = malloc(floorLen); memcpy(h->rawFloor, data + offset, floorLen); h->rawFloorSize = floorLen;
  offset += floorLen;
  uLongf destLen = h->width * h->height; h->floorData = malloc(destLen);
  if (uncompress(h->floorData, &destLen, h->rawFloor, floorLen) != Z_OK) return 2;
  h->floorSize = destLen;
  int32_t oreLen; memcpy(&oreLen, data + offset, 4); oreLen = __builtin_bswap32(oreLen);
  offset += 4;
  h->rawOre = malloc(oreLen); memcpy(h->rawOre, data + offset, oreLen); h->rawOreSize = oreLen;
  offset += oreLen;
  destLen = h->width * h->height; h->oreData = malloc(destLen);
  if (uncompress(h->oreData, &destLen, h->rawOre, oreLen) != Z_OK) return 2;
  h->oreSize = destLen;
  int32_t blockLen; memcpy(&blockLen, data + offset, 4); blockLen = __builtin_bswap32(blockLen);
  offset += 4;
  h->rawBlock = malloc(blockLen); memcpy(h->rawBlock, data + offset, blockLen); h->rawBlockSize = blockLen;
  offset += blockLen;
  destLen = fileSize - offset + 4; // Approximate
  h->blockData = malloc(destLen);
  if (uncompress(h->blockData, &destLen, h->rawBlock, blockLen) != Z_OK) return 2;
  h->blockSize = destLen;
  memcpy(&h->entityCount, data + offset, 4); h->entityCount = __builtin_bswap32(h->entityCount);
  offset += 4;
  h->rawEntitiesSize = fileSize - offset;
  h->rawEntities = malloc(h->rawEntitiesSize); memcpy(h->rawEntities, data + offset, h->rawEntitiesSize);
  free(data);
  return 0;
}

void msav_print_properties(MSAVHandler *h) {
  printf("Magic: %s\n", h->magic);
  printf("Version: %d\n", h->version);
  printf("Tags:\n");
  for (size_t i = 0; i < h->tagCount; i++) {
    printf("  %s: %s\n", h->tags[i].key, h->tags[i].val);
  }
  printf("Width: %d\n", h->width);
  printf("Height: %d\n", h->height);
  printf("Floor Layer: %zu bytes decompressed\n", h->floorSize);
  printf("Ore Layer: %zu bytes decompressed\n", h->oreSize);
  printf("Block Layer: %zu bytes decompressed\n", h->blockSize);
  printf("Entities: %d\n", h->entityCount);
}

int msav_write(MSAVHandler *h, const char *newFilename) {
  if (!newFilename) newFilename = h->filename;
  FILE *f = fopen(newFilename, "wb");
  if (!f) return 1;
  fwrite(h->magic, 1, 4, f);
  int16_t ver = __builtin_bswap16(h->version); fwrite(&ver, 2, 1, f);
  int32_t tc = __builtin_bswap32(h->tagCount); fwrite(&tc, 4, 1, f);
  for (size_t i = 0; i < h->tagCount; i++) {
    uint16_t kl = __builtin_bswap16(strlen(h->tags[i].key)); fwrite(&kl, 2, 1, f);
    fwrite(h->tags[i].key, 1, strlen(h->tags[i].key), f);
    uint16_t vl = __builtin_bswap16(strlen(h->tags[i].val)); fwrite(&vl, 2, 1, f);
    fwrite(h->tags[i].val, 1, strlen(h->tags[i].val), f);
  }
  int16_t w = __builtin_bswap16(h->width); fwrite(&w, 2, 1, f);
  int16_t ht = __builtin_bswap16(h->height); fwrite(&ht, 2, 1, f);
  int32_t fl = __builtin_bswap32(h->rawFloorSize); fwrite(&fl, 4, 1, f);
  fwrite(h->rawFloor, 1, h->rawFloorSize, f);
  int32_t ol = __builtin_bswap32(h->rawOreSize); fwrite(&ol, 4, 1, f);
  fwrite(h->rawOre, 1, h->rawOreSize, f);
  int32_t bl = __builtin_bswap32(h->rawBlockSize); fwrite(&bl, 4, 1, f);
  fwrite(h->rawBlock, 1, h->rawBlockSize, f);
  int32_t ec = __builtin_bswap32(h->entityCount); fwrite(&ec, 4, 1, f);
  fwrite(h->rawEntities, 1, h->rawEntitiesSize, f);
  fclose(f);
  return 0;
}

// Example usage
// int main() {
//   MSAVHandler *h = msav_create("example.msav");
//   if (msav_read(h) == 0) {
//     msav_print_properties(h);
//     msav_write(h, "output.msav");
//   }
//   msav_destroy(h);
//   return 0;
// }