Task 426: .MSCH File Format

Task 426: .MSCH File Format

1. List of properties of the .MSCH file format

The .MSCH file format is used for Mindustry schematics. It is a binary format with big-endian byte order for multi-byte values. The intrinsic properties (fields) are:

  • Magic identifier: 4 bytes, ASCII string "msch".
  • Version: 1 byte, integer (typically 1 or 2).
  • Width: 2 bytes, short integer, the width of the schematic grid.
  • Height: 2 bytes, short integer, the height of the schematic grid.
  • Tags: A map of strings (key-value pairs).
  • Count: 4 bytes, int, number of tag entries.
  • For each entry:
  • Key length: 4 bytes, int.
  • Key bytes: UTF-8 encoded string.
  • Value length: 4 bytes, int.
  • Value bytes: UTF-8 encoded string.
  • Tiles: List of tile (block placement) data.
  • Count: 4 bytes, int, number of tiles.
  • For each tile:
  • X position: 2 bytes, short integer.
  • Y position: 2 bytes, short integer.
  • Block ID: 4 bytes, int (references the block type by its content ID in Mindustry).
  • Rotation: 1 byte, integer (0-3 typically).
  • Config: Variable bytes, block-specific configuration data (assumed length-prefixed for parsing: 4 bytes int length, followed by that many bytes; decoding further requires knowledge of the block's config type, e.g., point, string, content, code).

Note: Block IDs correspond to Mindustry's internal block indices (e.g., 0 = graphite-press, 1 = multi-press, etc.). Config data is serialized differently per block type (e.g., integers, floats, strings, byte arrays, points, etc.), but for these implementations, it's treated as raw bytes with basic printing as hex.

3. Ghost blog embedded HTML JavaScript for drag and drop .MSCH file dumper

This is an HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post. It creates a drag-and-drop area; when a .MSCH file is dropped, it parses the file and displays all properties on the screen.

Drag and drop a .MSCH file here

4. Python class for .MSCH handling

import struct
import binascii

class MSCHHandler:
    def __init__(self):
        pass

    def read(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        offset = 0
        # Magic
        magic = data[offset:offset+4].decode('ascii')
        offset += 4
        # Version
        version, = struct.unpack('>B', data[offset:offset+1])
        offset += 1
        # Width, Height
        width, = struct.unpack('>H', data[offset:offset+2])
        offset += 2
        height, = struct.unpack('>H', data[offset:offset+2])
        offset += 2
        # Tags
        tag_count, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        tags = {}
        for _ in range(tag_count):
            key_len, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            key = data[offset:offset+key_len].decode('utf-8')
            offset += key_len
            val_len, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            val = data[offset:offset+val_len].decode('utf-8')
            offset += val_len
            tags[key] = val
        # Tiles
        tile_count, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        tiles = []
        for _ in range(tile_count):
            x, = struct.unpack('>h', data[offset:offset+2])
            offset += 2
            y, = struct.unpack('>h', data[offset:offset+2])
            offset += 2
            block_id, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            rotation, = struct.unpack('>B', data[offset:offset+1])
            offset += 1
            config_len, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            config_bytes = data[offset:offset+config_len]
            config_hex = binascii.hexlify(config_bytes).decode('ascii')
            offset += config_len
            tiles.append({'x': x, 'y': y, 'block_id': block_id, 'rotation': rotation, 'config_hex': config_hex})
        return {
            'magic': magic,
            'version': version,
            'width': width,
            'height': height,
            'tags': tags,
            'tiles': tiles
        }

    def print_properties(self, props):
        print(f"Magic: {props['magic']}")
        print(f"Version: {props['version']}")
        print(f"Width: {props['width']}")
        print(f"Height: {props['height']}")
        print("Tags:")
        for k, v in props['tags'].items():
            print(f"  {k}: {v}")
        print("Tiles:")
        for t in props['tiles']:
            print(f"  X: {t['x']}, Y: {t['y']}, Block ID: {t['block_id']}, Rotation: {t['rotation']}, Config (hex): {t['config_hex']}")

    def write(self, filename, props):
        with open(filename, 'wb') as f:
            # Magic
            f.write(props['magic'].encode('ascii'))
            # Version
            f.write(struct.pack('>B', props['version']))
            # Width, Height
            f.write(struct.pack('>H', props['width']))
            f.write(struct.pack('>H', props['height']))
            # Tags
            f.write(struct.pack('>I', len(props['tags'])))
            for k, v in props['tags'].items():
                f.write(struct.pack('>I', len(k)))
                f.write(k.encode('utf-8'))
                f.write(struct.pack('>I', len(v)))
                f.write(v.encode('utf-8'))
            # Tiles
            f.write(struct.pack('>I', len(props['tiles'])))
            for t in props['tiles']:
                f.write(struct.pack('>h', t['x']))
                f.write(struct.pack('>h', t['y']))
                f.write(struct.pack('>I', t['block_id']))
                f.write(struct.pack('>B', t['rotation']))
                config_bytes = binascii.unhexlify(t['config_hex'])
                f.write(struct.pack('>I', len(config_bytes)))
                f.write(config_bytes)

# Example usage:
# handler = MSCHHandler()
# props = handler.read('example.msch')
# handler.print_properties(props)
# handler.write('new.msch', props)

5. Java class for .MSCH handling

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

public class MSCHHandler {
    public Map<String, Object> read(String filename) throws IOException {
        try (FileInputStream fis = new FileInputStream(filename);
             DataInputStream dis = new DataInputStream(fis)) {
            Map<String, Object> props = new HashMap<>();
            // Magic
            byte[] magicBytes = new byte[4];
            dis.readFully(magicBytes);
            props.put("magic", new String(magicBytes, "ASCII"));
            // Version
            props.put("version", dis.readUnsignedByte());
            // Width, Height
            props.put("width", dis.readUnsignedShort());
            props.put("height", dis.readUnsignedShort());
            // Tags
            int tagCount = dis.readInt();
            Map<String, String> tags = new HashMap<>();
            for (int i = 0; i < tagCount; i++) {
                int keyLen = dis.readInt();
                byte[] keyBytes = new byte[keyLen];
                dis.readFully(keyBytes);
                String key = new String(keyBytes, "UTF-8");
                int valLen = dis.readInt();
                byte[] valBytes = new byte[valLen];
                dis.readFully(valBytes);
                String val = new String(valBytes, "UTF-8");
                tags.put(key, val);
            }
            props.put("tags", tags);
            // Tiles
            int tileCount = dis.readInt();
            List<Map<String, Object>> tiles = new ArrayList<>();
            for (int i = 0; i < tileCount; i++) {
                Map<String, Object> tile = new HashMap<>();
                tile.put("x", dis.readShort());
                tile.put("y", dis.readShort());
                tile.put("block_id", dis.readUnsignedInt());
                tile.put("rotation", dis.readUnsignedByte());
                int configLen = dis.readInt();
                byte[] configBytes = new byte[configLen];
                dis.readFully(configBytes);
                tile.put("config_hex", bytesToHex(configBytes));
                tiles.add(tile);
            }
            props.put("tiles", tiles);
            return props;
        }
    }

    public void printProperties(Map<String, Object> props) {
        System.out.println("Magic: " + props.get("magic"));
        System.out.println("Version: " + props.get("version"));
        System.out.println("Width: " + props.get("width"));
        System.out.println("Height: " + props.get("height"));
        System.out.println("Tags:");
        Map<String, String> tags = (Map<String, String>) props.get("tags");
        for (Map.Entry<String, String> entry : tags.entrySet()) {
            System.out.println("  " + entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("Tiles:");
        List<Map<String, Object>> tiles = (List<Map<String, Object>>) props.get("tiles");
        for (Map<String, Object> t : tiles) {
            System.out.println("  X: " + t.get("x") + ", Y: " + t.get("y") + ", Block ID: " + t.get("block_id") +
                    ", Rotation: " + t.get("rotation") + ", Config (hex): " + t.get("config_hex"));
        }
    }

    public void write(String filename, Map<String, Object> props) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filename);
             DataOutputStream dos = new DataOutputStream(fos)) {
            // Magic
            dos.writeBytes((String) props.get("magic"));
            // Version
            dos.writeByte((Integer) props.get("version"));
            // Width, Height
            dos.writeShort((Integer) props.get("width"));
            dos.writeShort((Integer) props.get("height"));
            // Tags
            Map<String, String> tags = (Map<String, String>) props.get("tags");
            dos.writeInt(tags.size());
            for (Map.Entry<String, String> entry : tags.entrySet()) {
                byte[] keyBytes = entry.getKey().getBytes("UTF-8");
                dos.writeInt(keyBytes.length);
                dos.write(keyBytes);
                byte[] valBytes = entry.getValue().getBytes("UTF-8");
                dos.writeInt(valBytes.length);
                dos.write(valBytes);
            }
            // Tiles
            List<Map<String, Object>> tiles = (List<Map<String, Object>>) props.get("tiles");
            dos.writeInt(tiles.size());
            for (Map<String, Object> t : tiles) {
                dos.writeShort((Short) t.get("x"));
                dos.writeShort((Short) t.get("y"));
                dos.writeInt((Integer) t.get("block_id"));
                dos.writeByte((Integer) t.get("rotation"));
                byte[] configBytes = hexToBytes((String) t.get("config_hex"));
                dos.writeInt(configBytes.length);
                dos.write(configBytes);
            }
        }
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x ", b));
        }
        return sb.toString().trim();
    }

    private byte[] hexToBytes(String hex) {
        String[] hexBytes = hex.split(" ");
        byte[] bytes = new byte[hexBytes.length];
        for (int i = 0; i < hexBytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt(hexBytes[i], 16);
        }
        return bytes;
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     MSCHHandler handler = new MSCHHandler();
    //     Map<String, Object> props = handler.read("example.msch");
    //     handler.printProperties(props);
    //     handler.write("new.msch", props);
    // }
}

6. JavaScript class for .MSCH handling

class MSCHHandler {
  constructor() {}

  async read(filename) {
    // Note: For Node.js, use fs module
    const fs = require('fs');
    const buffer = fs.readFileSync(filename);
    const view = new DataView(buffer.buffer);
    let offset = 0;

    // Magic
    const magic = String.fromCharCode(view.getUint8(offset++), view.getUint8(offset++), view.getUint8(offset++), view.getUint8(offset++));
    // Version
    const version = view.getUint8(offset++);
    // Width, Height
    const width = view.getUint16(offset, false); offset += 2;
    const height = view.getUint16(offset, false); offset += 2;
    // Tags
    const tagCount = view.getUint32(offset, false); offset += 4;
    const tags = {};
    for (let i = 0; i < tagCount; i++) {
      const keyLen = view.getUint32(offset, false); offset += 4;
      const key = new TextDecoder().decode(new Uint8Array(buffer.buffer, offset, keyLen)); offset += keyLen;
      const valLen = view.getUint32(offset, false); offset += 4;
      const val = new TextDecoder().decode(new Uint8Array(buffer.buffer, offset, valLen)); offset += valLen;
      tags[key] = val;
    }
    // Tiles
    const tileCount = view.getUint32(offset, false); offset += 4;
    const tiles = [];
    for (let i = 0; i < tileCount; i++) {
      const x = view.getInt16(offset, false); offset += 2;
      const y = view.getInt16(offset, false); offset += 2;
      const blockId = view.getUint32(offset, false); offset += 4;
      const rotation = view.getUint8(offset++);
      const configLen = view.getUint32(offset, false); offset += 4;
      const configBytes = new Uint8Array(buffer.buffer, offset, configLen);
      const configHex = Array.from(configBytes).map(b => b.toString(16).padStart(2, '0')).join(' ');
      offset += configLen;
      tiles.push({ x, y, blockId, rotation, configHex });
    }
    return { magic, version, width, height, tags, tiles };
  }

  printProperties(props) {
    console.log(`Magic: ${props.magic}`);
    console.log(`Version: ${props.version}`);
    console.log(`Width: ${props.width}`);
    console.log(`Height: ${props.height}`);
    console.log('Tags:');
    console.log(props.tags);
    console.log('Tiles:');
    props.tiles.forEach(t => {
      console.log(`  X: ${t.x}, Y: ${t.y}, Block ID: ${t.blockId}, Rotation: ${t.rotation}, Config (hex): ${t.configHex}`);
    });
  }

  write(filename, props) {
    // Calculate size and create buffer
    let size = 4 + 1 + 2 + 2 + 4; // magic + version + width + height + tagCount
    for (const [key, val] of Object.entries(props.tags)) {
      size += 4 + key.length + 4 + val.length;
    }
    size += 4; // tileCount
    props.tiles.forEach(t => {
      const configBytes = this.hexToBytes(t.configHex);
      size += 2 + 2 + 4 + 1 + 4 + configBytes.length;
    });
    const buffer = new ArrayBuffer(size);
    const view = new DataView(buffer);
    let offset = 0;

    // Magic
    'msch'.split('').forEach(c => view.setUint8(offset++, c.charCodeAt(0)));
    // Version
    view.setUint8(offset++, props.version);
    // Width, Height
    view.setUint16(offset, props.width, false); offset += 2;
    view.setUint16(offset, props.height, false); offset += 2;
    // Tags
    view.setUint32(offset, Object.keys(props.tags).length, false); offset += 4;
    for (const [key, val] of Object.entries(props.tags)) {
      view.setUint32(offset, key.length, false); offset += 4;
      new TextEncoder().encode(key).forEach(b => view.setUint8(offset++, b));
      view.setUint32(offset, val.length, false); offset += 4;
      new TextEncoder().encode(val).forEach(b => view.setUint8(offset++, b));
    }
    // Tiles
    view.setUint32(offset, props.tiles.length, false); offset += 4;
    props.tiles.forEach(t => {
      view.setInt16(offset, t.x, false); offset += 2;
      view.setInt16(offset, t.y, false); offset += 2;
      view.setUint32(offset, t.blockId, false); offset += 4;
      view.setUint8(offset++, t.rotation);
      const configBytes = this.hexToBytes(t.configHex);
      view.setUint32(offset, configBytes.length, false); offset += 4;
      configBytes.forEach(b => view.setUint8(offset++, b));
    });

    const fs = require('fs');
    fs.writeFileSync(filename, new Uint8Array(buffer));
  }

  hexToBytes(hex) {
    return hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
  }
}

// Example usage (Node.js):
// const handler = new MSCHHandler();
// const props = await handler.read('example.msch');
// handler.printProperties(props);
// handler.write('new.msch', props);

7. C++ class for .MSCH handling

#include <fstream>
#include <iostream>
#include <vector>
#include <map>
#include <sstream>
#include <iomanip>
#include <cstdint>

class MSCHHandler {
public:
    struct Tile {
        int16_t x;
        int16_t y;
        uint32_t block_id;
        uint8_t rotation;
        std::string config_hex;
    };

    std::map<std::string, std::string> tags;
    std::vector<Tile> tiles;
    std::string magic;
    uint8_t version;
    uint16_t width;
    uint16_t height;

    void read(const std::string& filename) {
        std::ifstream file(filename, std::ios::binary);
        if (!file) {
            std::cerr << "Failed to open file." << std::endl;
            return;
        }
        std::vector<char> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        size_t offset = 0;

        // Magic
        magic = std::string(data.data() + offset, 4);
        offset += 4;
        // Version
        version = *reinterpret_cast<uint8_t*>(data.data() + offset);
        offset += 1;
        // Width, Height
        width = be16toh(*reinterpret_cast<uint16_t*>(data.data() + offset));
        offset += 2;
        height = be16toh(*reinterpret_cast<uint16_t*>(data.data() + offset));
        offset += 2;
        // Tags
        uint32_t tag_count = be32toh(*reinterpret_cast<uint32_t*>(data.data() + offset));
        offset += 4;
        tags.clear();
        for (uint32_t i = 0; i < tag_count; ++i) {
            uint32_t key_len = be32toh(*reinterpret_cast<uint32_t*>(data.data() + offset));
            offset += 4;
            std::string key(data.data() + offset, key_len);
            offset += key_len;
            uint32_t val_len = be32toh(*reinterpret_cast<uint32_t*>(data.data() + offset));
            offset += 4;
            std::string val(data.data() + offset, val_len);
            offset += val_len;
            tags[key] = val;
        }
        // Tiles
        uint32_t tile_count = be32toh(*reinterpret_cast<uint32_t*>(data.data() + offset));
        offset += 4;
        tiles.clear();
        for (uint32_t i = 0; i < tile_count; ++i) {
            Tile t;
            t.x = be16toh(*reinterpret_cast<int16_t*>(data.data() + offset));
            offset += 2;
            t.y = be16toh(*reinterpret_cast<int16_t*>(data.data() + offset));
            offset += 2;
            t.block_id = be32toh(*reinterpret_cast<uint32_t*>(data.data() + offset));
            offset += 4;
            t.rotation = *reinterpret_cast<uint8_t*>(data.data() + offset);
            offset += 1;
            uint32_t config_len = be32toh(*reinterpret_cast<uint32_t*>(data.data() + offset));
            offset += 4;
            std::stringstream ss;
            for (uint32_t j = 0; j < config_len; ++j) {
                ss << std::hex << std::setw(2) << std::setfill('0') << (unsigned int)(unsigned char)data[offset + j] << " ";
            }
            t.config_hex = ss.str();
            if (!t.config_hex.empty()) t.config_hex.pop_back(); // Remove trailing space
            offset += config_len;
            tiles.push_back(t);
        }
    }

    void printProperties() {
        std::cout << "Magic: " << magic << std::endl;
        std::cout << "Version: " << (int)version << std::endl;
        std::cout << "Width: " << width << std::endl;
        std::cout << "Height: " << height << std::endl;
        std::cout << "Tags:" << std::endl;
        for (const auto& pair : tags) {
            std::cout << "  " << pair.first << ": " << pair.second << std::endl;
        }
        std::cout << "Tiles:" << std::endl;
        for (const auto& t : tiles) {
            std::cout << "  X: " << t.x << ", Y: " << t.y << ", Block ID: " << t.block_id
                      << ", Rotation: " << (int)t.rotation << ", Config (hex): " << t.config_hex << std::endl;
        }
    }

    void write(const std::string& filename) {
        std::ofstream file(filename, std::ios::binary);
        if (!file) {
            std::cerr << "Failed to open file for writing." << std::endl;
            return;
        }
        // Magic
        file.write(magic.c_str(), 4);
        // Version
        file.put(version);
        // Width, Height
        uint16_t be_width = htobe16(width);
        file.write(reinterpret_cast<char*>(&be_width), 2);
        uint16_t be_height = htobe16(height);
        file.write(reinterpret_cast<char*>(&be_height), 2);
        // Tags
        uint32_t be_tag_count = htobe32(tags.size());
        file.write(reinterpret_cast<char*>(&be_tag_count), 4);
        for (const auto& pair : tags) {
            uint32_t be_key_len = htobe32(pair.first.size());
            file.write(reinterpret_cast<char*>(&be_key_len), 4);
            file.write(pair.first.c_str(), pair.first.size());
            uint32_t be_val_len = htobe32(pair.second.size());
            file.write(reinterpret_cast<char*>(&be_val_len), 4);
            file.write(pair.second.c_str(), pair.second.size());
        }
        // Tiles
        uint32_t be_tile_count = htobe32(tiles.size());
        file.write(reinterpret_cast<char*>(&be_tile_count), 4);
        for (const auto& t : tiles) {
            int16_t be_x = htobe16(t.x);
            file.write(reinterpret_cast<char*>(&be_x), 2);
            int16_t be_y = htobe16(t.y);
            file.write(reinterpret_cast<char*>(&be_y), 2);
            uint32_t be_block_id = htobe32(t.block_id);
            file.write(reinterpret_cast<char*>(&be_block_id), 4);
            file.put(t.rotation);
            std::vector<uint8_t> config_bytes = hexToBytes(t.config_hex);
            uint32_t be_config_len = htobe32(config_bytes.size());
            file.write(reinterpret_cast<char*>(&be_config_len), 4);
            for (uint8_t b : config_bytes) {
                file.put(b);
            }
        }
    }

private:
    std::vector<uint8_t> hexToBytes(const std::string& hex) {
        std::vector<uint8_t> bytes;
        std::stringstream ss(hex);
        std::string byte_str;
        while (ss >> byte_str) {
            uint8_t b = static_cast<uint8_t>(std::stoi(byte_str, nullptr, 16));
            bytes.push_back(b);
        }
        return bytes;
    }
};

// Example usage:
// int main() {
//     MSCHHandler handler;
//     handler.read("example.msch");
//     handler.printProperties();
//     handler.write("new.msch");
//     return 0;
// }

Note: For C++, this assumes POSIX for be16toh/htobe16 (include <endian.h>). Config is printed as space-separated hex. Full config decoding would require Mindustry-specific block logic, not implemented here.