Somewhat inevitably I went ahead and wrote a python script for displaying a given .map file as a png. It doesn't really do anything that the OP2 Map Imager doesn't but I may as well share what I did anyhow. I got stuck for a bit on a problem with .map files coming in weird chunks of 32 at a time which I ended up having to check the Map Imager source to figure out. The script assumes the tileset files have been converted to png format and requires the imageio python library for image reading/writing (which failed to read the .bmp file). It displays 1, 2, 4, 8, 16, or 32 pixels per tile.
import sys
import struct
import os
import os.path
import numpy as np
import imageio
# The tileset files well0000.png etc. must exist in *png* format
# in the following directory.
# sys.path[0] gives the directory that contains the python script.
tileset_dir = os.path.join(sys.path[0], '..', 'data', 'extracted_art.vol')
si4 = struct.Struct('<i') # 4 byte integer, little endian
si2 = struct.Struct('<h') # 2 byte integer, little endian
class Map:
def __init__(self, data):
self.data = data
self.index = 0
self.N = len(data)
self.read_all()
# static
def read_file(filename):
with open(filename, 'rb') as stream:
data = stream.read()
return Map(data)
## Read a .map file ##
def read_all(self):
self.read_header()
self.read_map()
self.read_clip_region()
self.read_tileset_names()
self.read_tiles()
## Helper functions for reading a .map file ##
def next_k(self, k):
b = self.data[self.index : self.index + k]
self.index += k
assert self.index <= self.N
return b
def read_int4(self):
int4, = si4.unpack(self.next_k(4))
return int4
def read_int2(self):
int2, = si2.unpack(self.next_k(2))
return int2
def read_byte(self):
return self.next_k(1)[0]
def read_header(self):
assert self.read_int4() >= 0x1010
self.read_int4()
self.width = 2 ** self.read_int4()
self.height = self.read_int4()
self.num_tilesets = self.read_int4()
def read_map(self):
start = self.index
end = self.index + 4 * self.width * self.height
assert end <= self.N
raw_map = np.ndarray(shape = (self.width * self.height,),
dtype = '<u4', # little endian
buffer = self.data[start : end])
self.map = np.zeros((self.width, self.height), dtype = raw_map.dtype)
for i in range(0, self.width, 32):
col = raw_map[i * self.height : (i + 32) * self.height]
self.map[i : i + 32, :] = col.reshape((32, self.height), order = 'F')
self.map_tile_index = (self.map >> 5) & ((2 ** 11) - 1)
self.index = end
def read_clip_region(self):
self.read_int4()
self.read_int4()
self.read_int4()
self.read_int4()
def read_tileset_names(self):
self.tileset_filenames = [None] * self.num_tilesets
self.tileset_lengths = [0] * self.num_tilesets
self.num_valid_tilesets = 0
for i in range(self.num_tilesets):
k = self.read_int4()
if k > 0:
filename_base = self.next_k(8).decode('ascii')
self.tileset_filenames[i] = filename_base + '.png'
self.tileset_lengths[i] = self.read_int4()
self.num_valid_tilesets += 1
for i in range(self.num_valid_tilesets):
assert self.tileset_lengths[i] > 0
self.tileset_filenames = self.tileset_filenames[:self.num_valid_tilesets]
self.tileset_lengths = self.tileset_lengths[:self.num_valid_tilesets]
def read_tiles(self):
assert self.next_k(10) == b'TILE SET\x1a\x00'
self.num_tiles = self.read_int4()
self.tiles = [None] * self.num_tiles
for i in range(self.num_tiles):
tileset = self.read_int2()
tileindex = self.read_int2()
self.next_k(4) # animation data
self.tiles[i] = (tileset, tileindex)
def create_image(self, z):
assert z in [1, 2, 4, 8, 16, 32]
t = Tilesets(self)
palette = t.prepare_palette(z)
image = np.zeros((self.height * z, self.width * z, 4), dtype = t.dtype)
for j in range(self.height):
for a in range(z):
image[j * z + a] = palette[a, self.map_tile_index[:, j], :, :].reshape((self.width * z, 4), order = 'C')
return image
class Tilesets:
def __init__(self, m):
self.num_sets = m.num_valid_tilesets
self.filenames = list(m.tileset_filenames)
self.lengths = list(m.tileset_lengths)
self.n = m.num_tiles
self.tiles = list(m.tiles)
self.read_tilesets()
def read_tilesets(self):
self.tilesets = [None] * self.num_sets
for i in range(self.num_sets):
k = self.lengths[i]
data = imageio.imread(os.path.join(tileset_dir, self.filenames[i]))
assert data.shape[:2] == (32 * k, 32)
if len(data.shape) == 2:
data = np.copy(np.broadcast_to(data[:, :, None], (32 * k, 32, 4)))
self.tilesets[i] = data
self.dtype = data.dtype
def prepare_palette(self, z = 8):
zoomed_tilesets = []
for i in range(self.num_sets):
zoomed_tilesets.append(zoom_image(self.tilesets[i], z))
palette = np.zeros((z, self.n, z, 4), dtype = self.dtype)
for i in range(self.n):
w, idx = self.tiles[i]
t = zoomed_tilesets[w]
palette[:, i, :, :] = t[z * idx : z * (idx + 1), :, :]
if False:
palette_flat = np.zeros((z, self.n * z, 4), dtype = self.dtype)
for i in range(z):
palette_flat[i] = palette[i].reshape((self.n * z, 4), order = 'C')
imageio.imwrite('palette.png', palette_flat)
return palette
def zoom_image(data, z):
if z == 32:
return data
assert z in [1, 2, 4, 8, 16, 32]
r = 32 // z
data1_ = np.cumsum(data, axis = 0)[r - 1 :: r]
data1 = np.copy(data1_)
data1[1:] -= data1_[:-1]
data2_ = np.cumsum(data1, axis = 1)[:, r - 1 :: r]
data2 = np.copy(data2_)
data2[:, 1:] -= data2_[:, :-1]
return data2
def run(pixels_per_tile, filename):
assert os.path.isfile(filename)
m = Map.read_file(filename)
print(m.width, m.height)
image = m.create_image(pixels_per_tile)
imageio.imwrite('map.png', image)
if __name__ == "__main__":
pixels_per_tile = int(sys.argv[1])
filename = sys.argv[2]
run(pixels_per_tile, filename)