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.
2. Two direct download links for .MSCH files
- https://raw.githubusercontent.com/Ben55418/Midustry_game_2048/main/game.msch
- https://raw.githubusercontent.com/limonovthesecond2/vela-ai/main/vela-ai-scheme.msch
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.
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.