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).
2. Two Direct Download Links for .MSAV Files
- https://raw.githubusercontent.com/Anuken/Mindustry/master/core/assets/maps/aegis.msav
- 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).
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;
// }